Major version update. Major bugfixes. Security vulnerabilites fixed

This commit is contained in:
2026-10-14 03:02:07 +01:00
parent 4e47bd8ce0
commit 7c095f6f16
112 changed files with 2479 additions and 1219 deletions
+1 -8
View File
@@ -1,20 +1,13 @@
import { Command } from "commander";
import init from "../../functions/init";
import grabDBDir from "../../utils/grab-db-dir";
import chalk from "chalk";
import { select } from "@inquirer/prompts";
import { Database } from "bun:sqlite";
import listTables from "./list-tables";
import runSQL from "./run-sql";
import * as sqliteVec from "sqlite-vec";
export default function () {
return new Command("admin")
.description("View Tables and Data, Run SQL Queries, Etc.")
.action(async () => {
const { config } = await init();
const { db_file_path } = grabDBDir({ config });
const db = new Database(db_file_path);
sqliteVec.load(db);
const db = global.SQLITE_CLIENT;
console.log(chalk.bold(chalk.blue("\nBun SQLite Admin\n")));
try {
while (true) {
+7 -5
View File
@@ -1,20 +1,22 @@
import { Command } from "commander";
import init from "../functions/init";
import path from "path";
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
import chalk from "chalk";
import trimBackups from "../utils/trim-backups";
import snapshotDb from "../utils/snapshot-db";
export default function () {
return new Command("backup")
.description("Backup Database")
.action(async (opts) => {
console.log(`Backing up database ...`);
const { config } = await init();
const { backup_dir, db_file_path } = grabDBDir({ config });
const config = global.CONFIG;
const { backup_dir } = grabDBDir({ config });
const new_db_file_name = grabDBBackupFileName({ config });
fs.cpSync(db_file_path, path.join(backup_dir, new_db_file_name));
snapshotDb({
db: global.SQLITE_CLIENT,
dst: path.join(backup_dir, new_db_file_name),
});
trimBackups({ config });
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))}`);
process.exit();
+2
View File
@@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;
+70
View File
@@ -0,0 +1,70 @@
import { Command } from "commander";
import path from "path";
import fs from "fs";
import chalk from "chalk";
import grabDBDir from "../utils/grab-db-dir";
import grabDirNames from "../data/grab-dir-names";
import snapshotDb from "../utils/snapshot-db";
import { writeExportArchive } from "../utils/export-archive";
import trimExports from "../utils/trim-exports";
import { AppData } from "../data/app-data";
function defaultExportFileName(dbName, format) {
return `${dbName}-${Date.now()}.${format}`;
}
export default function () {
return new Command("export")
.description("Export database file + schema into a portable archive")
.option("-o, --output <path>", "Output archive path (.tar.gz or .zip). Defaults to db exports dir")
.option("-f, --format <format>", "Archive format when --output is omitted: tar.gz | zip", "tar.gz")
.action(async (opts) => {
console.log(`Exporting database ...`);
const config = global.CONFIG;
const { export_dir } = grabDBDir({ config });
const { BUN_SQLITE_TEMP_DIR } = grabDirNames();
if (!fs.existsSync(export_dir)) {
fs.mkdirSync(export_dir, { recursive: true });
}
const formatRaw = String(opts.format || "tar.gz").toLowerCase();
const format = formatRaw === "zip" ? "zip" : "tar.gz";
let outPath;
if (opts.output) {
outPath = path.resolve(opts.output);
fs.mkdirSync(path.dirname(outPath), { recursive: true });
}
else {
outPath = path.join(export_dir, defaultExportFileName(config.db_name, format));
}
const snapshot_path = path.join(BUN_SQLITE_TEMP_DIR, `export-${Date.now()}.db`);
/** Remove the temp database file before exiting */
const exit = (code) => {
fs.rmSync(snapshot_path, { force: true });
process.exit(code);
};
try {
snapshotDb({ db: global.SQLITE_CLIENT, dst: snapshot_path });
/**
* The schema is always stored as `schema.<ext>` so `import`
* can find it, whatever `db_schema_file_name` is.
*/
const schemaFileName = `${AppData.DbSchemaFileName}${path.extname(global.SCHEMA_FILE_PATH)}`;
await writeExportArchive({
contents: {
db: new Uint8Array(fs.readFileSync(snapshot_path)),
schema: fs.readFileSync(global.SCHEMA_FILE_PATH, "utf-8"),
schemaFileName,
},
outPath,
});
if (path.dirname(outPath) === export_dir) {
trimExports({ config });
}
console.log(`${chalk.bold(chalk.green(`DB Export Success!`))} → ${outPath}`);
console.log(chalk.dim(`Contains: database.sqlite + ${schemaFileName}`));
exit(0);
}
catch (error) {
console.error(chalk.red(`Export ERROR => ${error.message}`));
exit(1);
}
});
}
+2
View File
@@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;
+187
View File
@@ -0,0 +1,187 @@
import { Command } from "commander";
import path from "path";
import fs from "fs";
import chalk from "chalk";
import { Database } from "bun:sqlite";
import * as sqliteVec from "sqlite-vec";
import { confirm, select } from "@inquirer/prompts";
import grabDBDir from "../utils/grab-db-dir";
import grabDirNames from "../data/grab-dir-names";
import grabSortedExports from "../utils/grab-sorted-exports";
import grabBackupData from "../utils/grab-backup-data";
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
import snapshotDb from "../utils/snapshot-db";
import trimBackups from "../utils/trim-backups";
import replaceDbFile from "../utils/replace-db-file";
import { isArchivePath, isSQLiteFile, isSqlPath, readExportArchive, } from "../utils/export-archive";
import { resolveDataFile } from "../utils/resolve-and-load-data-file";
import { AppData } from "../data/app-data";
function formatChoice(name, index) {
const { backup_date } = grabBackupData({
backup_name: name.replace(/\.(tar\.gz|tgz|tar|zip)$/i, ""),
});
const time = Number.isNaN(backup_date.getTime())
? "unknown date"
: `${backup_date.toDateString()} ${backup_date.getHours()}:${backup_date.getMinutes()}:${backup_date.getSeconds().toString().padStart(2, "0")}`;
return `#${index + 1}: ${name} (${time})`;
}
/**
* # Replace the live database with the database file at `src`
* @description The current database is backed up first.
*/
function swapDatabase(src) {
const config = global.CONFIG;
const { backup_dir, db_file_path } = grabDBDir({ config });
const backup_path = path.join(backup_dir, grabDBBackupFileName({ config }));
snapshotDb({ db: global.SQLITE_CLIENT, dst: backup_path });
trimBackups({ config });
console.log(chalk.dim(`Backed up current database → ${backup_path}`));
global.SQLITE_CLIENT.close();
replaceDbFile({ src, db_file_path });
}
/**
* # Build a database file from an SQL script
*/
function buildDbFromSql({ sql, dst }) {
fs.mkdirSync(path.dirname(dst), { recursive: true });
fs.rmSync(dst, { force: true });
const db = new Database(dst, { create: true });
try {
sqliteVec.load(db);
db.run(sql);
}
finally {
db.close();
}
}
/**
* # Write the schema file from an archive into the project
*/
function writeSchema({ schema, schemaFileName, }) {
const config = global.CONFIG;
const { db_dir } = grabDBDir({ config });
if (config.db_schema_file_name) {
const schemaPath = path.join(db_dir, config.db_schema_file_name);
if (path.extname(schemaPath) !== path.extname(schemaFileName)) {
throw new Error(`Archive schema is \`${schemaFileName}\` but \`db_schema_file_name\` is \`${config.db_schema_file_name}\`. Use --sql-only, or change \`db_schema_file_name\`.`);
}
fs.writeFileSync(schemaPath, schema, "utf-8");
console.log(chalk.green(`Schema written → ${schemaPath}`));
return;
}
const existing = resolveDataFile(db_dir, AppData.DbSchemaFileName);
const schemaPath = path.join(db_dir, schemaFileName);
// Remove a differently-named schema so only one format remains
if (existing && path.resolve(existing.path) !== path.resolve(schemaPath)) {
fs.unlinkSync(existing.path);
console.log(chalk.dim(`Removed previous schema file: ${existing.basename}`));
}
fs.writeFileSync(schemaPath, schema, "utf-8");
console.log(chalk.green(`Schema written → ${schemaPath}`));
}
export default function () {
return new Command("import")
.description("Import a SQLite database file, an SQL script, or a full export archive (database + schema)")
.argument("[file]", "Path to a SQLite file, .sql file, or export archive (.tar.gz / .tar / .zip)")
.option("--sql-only", "When importing an archive, restore the database only (skip writing schema)")
.option("--schema-only", "When importing an archive, write schema only (skip database restore)")
.option("-y, --yes", "Do not ask for confirmation before writing an executable (.ts/.js) schema from the archive")
.action(async (fileArg, opts) => {
console.log(`Importing database ...`);
const config = global.CONFIG;
const { db_dir, export_dir } = grabDBDir({ config });
const { BUN_SQLITE_TEMP_DIR } = grabDirNames();
const temp_db_path = path.join(BUN_SQLITE_TEMP_DIR, `import-${Date.now()}.db`);
/** Remove the temp database file before exiting */
const exit = (code) => {
fs.rmSync(temp_db_path, { force: true });
process.exit(code);
};
try {
let filePath = fileArg ? path.resolve(fileArg) : "";
if (!filePath) {
const candidates = grabSortedExports({ config }).filter((b) => isArchivePath(b));
if (!candidates[0]) {
console.error(`No export archives found in \`${export_dir}\`. Use \`export\`, or pass a file path.`);
exit(1);
}
const selected = await select({
message: "Select an export to import:",
choices: candidates.map((b, i) => ({
name: formatChoice(b, i),
value: b,
})),
});
filePath = path.join(export_dir, selected);
}
if (!fs.existsSync(filePath)) {
console.error(chalk.red(`File not found: ${filePath}`));
exit(1);
}
if (opts.sqlOnly && opts.schemaOnly) {
console.error(chalk.red(`Cannot combine --sql-only and --schema-only.`));
exit(1);
}
if (!isArchivePath(filePath)) {
if (opts.schemaOnly) {
console.error(chalk.red(`--schema-only requires an export archive.`));
exit(1);
}
if (isSqlPath(filePath)) {
buildDbFromSql({
sql: fs.readFileSync(filePath, "utf-8"),
dst: temp_db_path,
});
swapDatabase(temp_db_path);
}
else if (isSQLiteFile(new Uint8Array(fs.readFileSync(filePath)))) {
swapDatabase(filePath);
}
else {
console.error(chalk.red(`Unsupported file: ${filePath}. Use a SQLite database file, .sql, .tar.gz, .tar, or .zip.`));
exit(1);
}
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))} ← ${filePath}`);
exit(0);
}
const { db, schema, schemaFileName } = await readExportArchive(filePath);
/**
* A .ts/.js schema is executed (require) on the next run.
* Confirm before writing code from an archive into the project.
*/
const isExecutableSchema = /\.(ts|js)$/i.test(schemaFileName);
if (!opts.sqlOnly && isExecutableSchema && !opts.yes) {
if (!process.stdin.isTTY) {
console.error(chalk.red(`Archive contains an executable schema (${schemaFileName}). Re-run with --yes to accept it, or --sql-only to skip it.`));
exit(1);
}
console.log(chalk.yellow(`\nThis archive contains an executable schema file (${schemaFileName}).\nIt will run as code the next time bun-sqlite or your app loads it.\nOnly continue if you trust the source of this archive.\n`));
const proceed = await confirm({
message: `Write ${schemaFileName} into ${db_dir}?`,
default: false,
});
if (!proceed) {
console.log(chalk.dim(`Import cancelled.`));
exit(1);
}
}
if (!opts.sqlOnly) {
writeSchema({ schema, schemaFileName });
}
if (!opts.schemaOnly) {
fs.mkdirSync(path.dirname(temp_db_path), {
recursive: true,
});
fs.writeFileSync(temp_db_path, db);
swapDatabase(temp_db_path);
console.log(chalk.green(`Database restored from archive`));
}
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))} ← ${filePath}`);
exit(0);
}
catch (error) {
console.error(chalk.red(`Import ERROR => ${error.message}`));
exit(1);
}
});
}
-4
View File
@@ -1,6 +1,2 @@
#!/usr/bin/env bun
/**
* # Declare Global Variables
*/
declare global { }
export {};
Vendored Regular → Executable
+20 -2
View File
@@ -4,14 +4,27 @@ import schema from "./schema";
import typedef from "./typedef";
import backup from "./backup";
import restore from "./restore";
import exportCmd from "./export";
import importCmd from "./import";
import admin from "./admin";
import init from "../functions/init";
import { readFileSync } from "fs";
try {
init();
}
catch (error) {
console.error(`Initialization ERROR => ${error.message}`);
process.exit(1);
}
/** Resolves to the package root from both src/commands and dist/commands */
const { version } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
/**
* # Describe Program
*/
program
.name(`bun-sqlite`)
.description(`SQLite manager for Bun`)
.version(`1.0.0`);
.version(version);
/**
* # Declare Commands
*/
@@ -19,6 +32,8 @@ program.addCommand(schema());
program.addCommand(typedef());
program.addCommand(backup());
program.addCommand(restore());
program.addCommand(exportCmd());
program.addCommand(importCmd());
program.addCommand(admin());
/**
* # Handle Unavailable Commands
@@ -30,4 +45,7 @@ program.on("command:*", () => {
/**
* # Parse Arguments
*/
program.parse(Bun.argv);
program.parseAsync(Bun.argv).catch((error) => {
console.error(`ERROR => ${error?.message || error}`);
process.exit(1);
});
+8 -5
View File
@@ -1,7 +1,6 @@
import { Command } from "commander";
import init from "../functions/init";
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import replaceDbFile from "../utils/replace-db-file";
import chalk from "chalk";
import grabSortedBackups from "../utils/grab-sorted-backups";
import { select } from "@inquirer/prompts";
@@ -12,7 +11,7 @@ export default function () {
.description("Restore Database")
.action(async (opts) => {
console.log(`Restoring up database ...`);
const { config } = await init();
const config = global.CONFIG;
const { backup_dir, db_file_path } = grabDBDir({ config });
const backups = grabSortedBackups({ config });
if (!backups?.[0]) {
@@ -32,13 +31,17 @@ export default function () {
};
}),
});
fs.cpSync(path.join(backup_dir, selected_backup), db_file_path);
global.SQLITE_CLIENT.close();
replaceDbFile({
src: path.join(backup_dir, selected_backup),
db_file_path,
});
console.log(`${chalk.bold(chalk.green(`DB Restore Success!`))}`);
process.exit();
}
catch (error) {
console.error(`Backup Restore ERROR => ${error.message}`);
process.exit();
process.exit(1);
}
});
}
+74 -12
View File
@@ -1,38 +1,96 @@
import { Command } from "commander";
import { SQLiteSchemaManager } from "../lib/sqlite/db-schema-manager";
import init from "../functions/init";
import createDBSchema from "../lib/schema/create-db-schema";
import grabDirNames from "../data/grab-dir-names";
import path from "path";
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
import _ from "lodash";
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
import chalk from "chalk";
import { writeLiveSchema } from "../functions/live-schema";
import grabDBDir from "../utils/grab-db-dir";
import { cpSync } from "fs";
import snapshotDb from "../utils/snapshot-db";
import replaceDbFile from "../utils/replace-db-file";
import { confirm } from "@inquirer/prompts";
export default function () {
return new Command("schema")
.description("Build DB From Schema")
.option("-v, --vector", "Recreate Vector Tables. This will drop and rebuild all vector tables")
.option("-t, --typedef", "Generate typescript type definitions")
.option("--dry-run", "Print the schema changes that would be applied, without applying them")
.option("-y, --yes", "Apply destructive changes (dropped tables / columns) without confirmation")
.action(async (opts) => {
console.log(`Starting process ...`);
const { config, dbSchema } = await init();
const config = global.CONFIG;
const dbSchema = global.DB_SCHEMA;
const DbClient = global.SQLITE_CLIENT;
const { ROOT_DIR, BUN_SQLITE_TEMP_DB_FILE_PATH } = grabDirNames();
const { db_file_path } = grabDBDir({ config });
cpSync(db_file_path, BUN_SQLITE_TEMP_DB_FILE_PATH);
try {
const isVector = Boolean(opts.vector || opts.v);
const isTypeDef = Boolean(opts.typedef || opts.t);
const finaldbSchema = appendDefaultFieldsToDbSchema({
dbSchema,
});
const manager = new SQLiteSchemaManager({
schema: finaldbSchema,
const params = {
db: DbClient,
db_schema: finaldbSchema,
recreate_vector_table: isVector,
};
/**
* Plan: run the sync in a transaction that is rolled back
*/
const plan = [];
try {
createDBSchema({ ...params, dry_run: true, changes: plan });
}
catch (error) {
console.error(chalk.red(`Schema sync ERROR => ${error.message}`));
process.exit(1);
}
const destructive = plan.filter((change) => change.destructive);
if (opts.dryRun) {
if (!plan.length) {
console.log(chalk.green(`Schema is up to date.`));
}
else {
console.log(chalk.bold(`Planned schema changes:\n`));
for (const change of plan) {
console.log(change.destructive
? chalk.red(` ${change.description}`)
: ` ${change.description}`);
}
if (destructive.length) {
console.log(chalk.red(`\n${destructive.length} destructive change(s) (in red) would discard data.`));
}
}
process.exit(0);
}
if (destructive.length && !opts.yes) {
console.log(chalk.yellow(`\nThese changes will permanently discard data:\n`));
for (const change of destructive) {
console.log(chalk.red(` ${change.description}`));
}
console.log();
if (!process.stdin.isTTY) {
console.error(chalk.red(`Refusing to apply destructive changes non-interactively. Re-run with --yes to apply them.`));
process.exit(1);
}
const proceed = await confirm({
message: `Apply ${destructive.length} destructive change(s)?`,
default: false,
});
await manager.syncSchema();
manager.close();
if (!proceed) {
console.log(chalk.dim(`Schema sync cancelled.`));
process.exit(1);
}
}
snapshotDb({ db: DbClient, dst: BUN_SQLITE_TEMP_DB_FILE_PATH });
try {
createDBSchema({
...params,
allow_destructive: opts.yes
? true
: new Set(destructive.map((c) => c.description)),
});
DbClient.close();
if (isTypeDef && config.typedef_file_path) {
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
dbSchemaToTypeDef({
@@ -47,7 +105,11 @@ export default function () {
}
catch (error) {
console.log(error);
cpSync(BUN_SQLITE_TEMP_DB_FILE_PATH, db_file_path);
DbClient.close();
replaceDbFile({
src: BUN_SQLITE_TEMP_DB_FILE_PATH,
db_file_path,
});
process.exit(1);
}
});
+3 -3
View File
@@ -1,5 +1,4 @@
import { Command } from "commander";
import init from "../functions/init";
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
import path from "path";
import grabDirNames from "../data/grab-dir-names";
@@ -10,7 +9,8 @@ export default function () {
.description("Build DB From Schema")
.action(async (opts) => {
console.log(`Creating Type Definition From DB Schema ...`);
const { config, dbSchema } = await init();
const config = global.CONFIG;
const dbSchema = global.DB_SCHEMA;
const { ROOT_DIR } = grabDirNames();
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
if (config.typedef_file_path) {
@@ -22,7 +22,7 @@ export default function () {
});
}
else {
console.error(``);
console.error(`\`typedef_file_path\` is required in your config`);
process.exit(1);
}
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
+6 -1
View File
@@ -1,6 +1,11 @@
export declare const AppData: {
readonly ConfigFileName: "bun-sqlite.config.ts";
readonly ConfigFileName: "bun-sqlite.config";
readonly MaxBackups: 10;
readonly MaxExports: 10;
readonly DefaultBackupDirName: ".backups";
readonly DefaultExportDirName: ".exports";
readonly DbSchemaManagerTableName: "__db_schema_manager__";
readonly DbSchemaFileName: "schema";
/** Priority order when resolving config/schema files */
readonly SupportedDataFileExtensions: readonly [".ts", ".js", ".json", ".yaml", ".yml"];
};
+6 -1
View File
@@ -1,6 +1,11 @@
export const AppData = {
ConfigFileName: "bun-sqlite.config.ts",
ConfigFileName: "bun-sqlite.config",
MaxBackups: 10,
MaxExports: 10,
DefaultBackupDirName: ".backups",
DefaultExportDirName: ".exports",
DbSchemaManagerTableName: "__db_schema_manager__",
DbSchemaFileName: "schema",
/** Priority order when resolving config/schema files */
SupportedDataFileExtensions: [".ts", ".js", ".json", ".yaml", ".yml"],
};
+7
View File
@@ -0,0 +1,7 @@
import { Database } from "bun:sqlite";
import type { BunSQLiteConfig } from "../types";
type Params = {
config: BunSQLiteConfig;
};
export default function grabSQLiteClient({ config }: Params): Database;
export {};
+21
View File
@@ -0,0 +1,21 @@
import { Database } from "bun:sqlite";
import * as sqliteVec from "sqlite-vec";
import grabDBDir from "../utils/grab-db-dir";
/** Reuse one connection per database file. */
const clientCache = new Map();
export default function grabSQLiteClient({ config }) {
const { db_file_path } = grabDBDir({ config });
const cached = clientCache.get(db_file_path);
if (cached) {
return cached;
}
const SQLiteClient = new Database(db_file_path, {
create: true,
});
sqliteVec.load(SQLiteClient);
if (config.wal_mode) {
SQLiteClient.run("PRAGMA journal_mode = WAL;");
}
clientCache.set(db_file_path, SQLiteClient);
return SQLiteClient;
}
+18 -2
View File
@@ -1,2 +1,18 @@
import type { BunSQLiteConfigReturn } from "../types";
export default function init(): Promise<BunSQLiteConfigReturn>;
import type { Database } from "bun:sqlite";
import type { BunSQLiteConfig, BunSQLiteConfigReturn, BUN_SQLITE_DatabaseSchemaType } from "../types";
/**
* # Declare Global Variables
*/
declare global {
var CONFIG: BunSQLiteConfig;
var DB_SCHEMA: BUN_SQLITE_DatabaseSchemaType;
var SQLITE_CLIENT: Database;
var CONFIG_FILE_PATH: string;
var SCHEMA_FILE_PATH: string;
}
/**
* # Load config, schema and the SQLite client into globals
* @description Runs once per process. Later calls return the loaded
* globals.
*/
export default function init(): BunSQLiteConfigReturn;
+70 -30
View File
@@ -2,45 +2,85 @@ import path from "path";
import fs from "fs";
import { AppData } from "../data/app-data";
import grabDirNames from "../data/grab-dir-names";
export default async function init() {
try {
import setSQLiteClient from "./set-sqlite-client";
import grabDBDir from "../utils/grab-db-dir";
import { loadDataFile, resolveAndLoadDataFile, supportedDataFileNames, } from "../utils/resolve-and-load-data-file";
/**
* # Load config, schema and the SQLite client into globals
* @description Runs once per process. Later calls return the loaded
* globals.
*/
export default function init() {
if (global.CONFIG && global.DB_SCHEMA && global.SQLITE_CLIENT) {
return { config: global.CONFIG, dbSchema: global.DB_SCHEMA };
}
const { ROOT_DIR } = grabDirNames();
const { ConfigFileName } = AppData;
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
if (!fs.existsSync(ConfigFilePath)) {
console.log("ConfigFilePath", ConfigFilePath);
console.error(`Please create a \`${ConfigFileName}\` file at the root of your project.`);
process.exit(1);
const loadedConfig = resolveAndLoadDataFile(ROOT_DIR, ConfigFileName);
if (!loadedConfig) {
throw new Error(`Please create a ${supportedDataFileNames(ConfigFileName)} file at the root of your project. (Looked in ${ROOT_DIR})`);
}
const Config = loadedConfig.data;
if (!Config || typeof Config !== "object") {
throw new Error(`Invalid config in \`${loadedConfig.path}\`. Expected a config object${isCodeFile(loadedConfig) ? " (export default)" : ""}.`);
}
const ConfigImport = await import(ConfigFilePath);
const Config = ConfigImport["default"];
if (!Config.db_name) {
console.error(`\`db_name\` is required in your config`);
process.exit(1);
throw new Error(`\`db_name\` is required in your config`);
}
if (!Config.db_schema_file_name) {
console.error(`\`db_schema_file_name\` is required in your config`);
process.exit(1);
const { db_dir, backup_dir } = grabDBDir({ config: Config });
if (!fs.existsSync(db_dir)) {
fs.mkdirSync(db_dir, { recursive: true });
}
let db_dir = ROOT_DIR;
if (Config.db_dir) {
db_dir = path.resolve(ROOT_DIR, Config.db_dir);
if (!fs.existsSync(Config.db_dir)) {
fs.mkdirSync(Config.db_dir, { recursive: true });
const loadedSchema = loadSchema({ config: Config, db_dir });
const DbSchema = loadedSchema.data;
if (!DbSchema || typeof DbSchema !== "object") {
throw new Error(`Invalid schema in \`${loadedSchema.path}\`. Expected a schema object${isCodeFile(loadedSchema) ? " (export default)" : ""}.`);
}
if (!fs.existsSync(backup_dir)) {
fs.mkdirSync(backup_dir, { recursive: true });
}
const DBSchemaFilePath = path.join(db_dir, Config.db_schema_file_name);
const DbSchemaImport = await import(DBSchemaFilePath);
const DbSchema = DbSchemaImport["default"];
const backup_dir = Config.db_backup_dir || AppData["DefaultBackupDirName"];
const BackupDir = path.resolve(db_dir, backup_dir);
if (!fs.existsSync(BackupDir)) {
fs.mkdirSync(BackupDir, { recursive: true });
}
global.CONFIG = Config;
global.DB_SCHEMA = DbSchema;
global.CONFIG_FILE_PATH = loadedConfig.path;
global.SCHEMA_FILE_PATH = loadedSchema.path;
setSQLiteClient({ config: Config });
return { config: Config, dbSchema: DbSchema };
}
catch (error) {
console.error(`Initialization ERROR => ` + error.message);
process.exit(1);
/**
* # Load the schema file
* @description Uses `db_schema_file_name` when set, otherwise
* `schema.ts|js|json|yaml|yml` in `db_dir`.
*/
function loadSchema({ config, db_dir, }) {
if (config.db_schema_file_name) {
const schema_file_path = path.join(db_dir, config.db_schema_file_name);
const extension = path.extname(schema_file_path);
const supported = AppData.SupportedDataFileExtensions.find((ext) => ext === extension);
if (!supported) {
throw new Error(`Unsupported schema file \`${config.db_schema_file_name}\`. Use one of: ${AppData.SupportedDataFileExtensions.join(", ")}`);
}
if (!fs.existsSync(schema_file_path)) {
throw new Error(`Schema file not found: ${schema_file_path}`);
}
const resolved = {
path: schema_file_path,
basename: path.basename(schema_file_path),
extension: supported,
format: supported === ".yml"
? "yaml"
: supported.slice(1),
};
return {
...resolved,
data: loadDataFile(resolved),
};
}
const loaded = resolveAndLoadDataFile(db_dir, AppData.DbSchemaFileName);
if (!loaded) {
throw new Error(`Please create a schema file (${supportedDataFileNames(AppData.DbSchemaFileName)}) in \`${db_dir}\`, or set \`db_schema_file_name\` in your config.`);
}
return loaded;
}
function isCodeFile(file) {
return file.format === "ts" || file.format === "js";
}
+6
View File
@@ -0,0 +1,6 @@
import type { BunSQLiteConfig } from "../types";
type Params = {
config: BunSQLiteConfig;
};
export default function setSQLiteClient({ config }: Params): void;
export {};
+4
View File
@@ -0,0 +1,4 @@
import grabSQLiteClient from "./grab-sqlite-client";
export default function setSQLiteClient({ config }) {
global.SQLITE_CLIENT = grabSQLiteClient({ config });
}
+2
View File
@@ -17,3 +17,5 @@ declare const BunSQLite: {
};
};
export default BunSQLite;
export type * from "./types";
export { UsersOmitedFields, MariaDBCollations, TextFieldTypesArray, BUN_SQLITE_DATATYPES, ServerQueryOperators, ServerQueryEqualities, SQlComparisons, DataCrudRequestMethods, DataCrudRequestMethodsLowerCase, DsqlCrudActions, QueryFields, DockerComposeServices, IndexTypes, DefaultFields, } from "./types";
+18 -6
View File
@@ -1,3 +1,4 @@
import init from "./functions/init";
import DbDelete from "./lib/sqlite/db-delete";
import DbInsert from "./lib/sqlite/db-insert";
import DbSelect from "./lib/sqlite/db-select";
@@ -5,15 +6,26 @@ import DbSQL from "./lib/sqlite/db-sql";
import DbUpdate from "./lib/sqlite/db-update";
import grabDbSchema from "./utils/grab-db-schema";
import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object";
/**
* Initialize lazily on first call, so importing the library never throws.
* Setup errors (missing config / schema) reject the call instead.
*/
function withInit(fn) {
return (async (...args) => {
init();
return fn(...args);
});
}
const BunSQLite = {
select: DbSelect,
insert: DbInsert,
update: DbUpdate,
delete: DbDelete,
sql: DbSQL,
select: withInit(DbSelect),
insert: withInit(DbInsert),
update: withInit(DbUpdate),
delete: withInit(DbDelete),
sql: withInit(DbSQL),
utils: {
grab_db_schema: grabDbSchema,
grab_db_schema: withInit(grabDbSchema),
grab_join_fields_from_query_object: grabJoinFieldsFromQueryObject,
},
};
export default BunSQLite;
export { UsersOmitedFields, MariaDBCollations, TextFieldTypesArray, BUN_SQLITE_DATATYPES, ServerQueryOperators, ServerQueryEqualities, SQlComparisons, DataCrudRequestMethods, DataCrudRequestMethodsLowerCase, DsqlCrudActions, QueryFields, DockerComposeServices, IndexTypes, DefaultFields, } from "./types";
+18
View File
@@ -0,0 +1,18 @@
import type { APIResponseObject, BunSQLiteConfig, SQLInsertGenValueType } from "../types";
type Param = {
query: string;
values?: SQLInsertGenValueType[];
config?: BunSQLiteConfig;
};
/**
* # Main DB Handler Function
* @description Any statement that produces columns (SELECT, WITH, PRAGMA,
* RETURNING, ...) returns rows. Everything else goes through `run`, which
* also handles multiple statements.
*/
export default function dbHandler<T extends {
[k: string]: any;
} = {
[k: string]: any;
}>({ query, values, config }: Param): APIResponseObject<T>;
export {};
+53
View File
@@ -0,0 +1,53 @@
import init from "../functions/init";
import grabSQLiteClient from "../functions/grab-sqlite-client";
/**
* Resolve the SQLite client. The global client is used unless a `config`
* for another database is passed.
*/
function resolveClient(config) {
if (config) {
return grabSQLiteClient({ config });
}
init();
return global.SQLITE_CLIENT;
}
/**
* # Main DB Handler Function
* @description Any statement that produces columns (SELECT, WITH, PRAGMA,
* RETURNING, ...) returns rows. Everything else goes through `run`, which
* also handles multiple statements.
*/
export default function dbHandler({ query, values, config }) {
try {
const CLIENT = resolveClient(config);
const stmt = CLIENT.prepare(query);
const returnsRows = stmt.columnNames.length > 0;
try {
if (returnsRows) {
const rows = stmt.all(...(values || []));
return {
success: true,
payload: rows,
singleRes: rows[0],
};
}
}
finally {
stmt.finalize();
}
const res = CLIENT.run(query, (values || []));
return {
success: true,
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
};
}
catch (error) {
return {
success: false,
error: error.message,
};
}
}
+17 -4
View File
@@ -1,22 +1,35 @@
import init from "../functions/init";
import { sqlIdent, sqlQuoteIdent } from "../utils/sql-safe";
export default async function ({ sql: passed_sql, table, data }) {
let sql = passed_sql;
const { dbSchema } = await init();
const { dbSchema } = init();
const table_schema = dbSchema.tables.find((t) => t.tableName == table);
const now = Date.now();
if (table_schema?.tableName) {
const set_sql_arr = Object.keys(Array.isArray(data) ? data[0] : data).map((field) => `${field} = excluded.${field}`);
const set_sql_arr = Object.keys(Array.isArray(data) ? data[0] : data)
.filter((field) => field !== "updated_at")
.map((field) => {
const column = sqlIdent(field, "column name");
return `${column} = excluded.${column}`;
});
set_sql_arr.push(`updated_at = ${now}`);
const set_sql = set_sql_arr.join(", ");
const unique_fields = table_schema.fields.filter((f) => f.unique);
for (let i = 0; i < unique_fields.length; i++) {
const field = unique_fields[i];
sql += ` ON CONFLICT(${field?.fieldName}) DO UPDATE SET ${set_sql}`;
if (!field?.fieldName)
continue;
sql += ` ON CONFLICT(${sqlQuoteIdent(field.fieldName)}) DO UPDATE SET ${set_sql}`;
}
if (table_schema.uniqueConstraints?.[0]) {
for (let i = 0; i < table_schema.uniqueConstraints.length; i++) {
const constraint = table_schema.uniqueConstraints[i];
sql += ` ON CONFLICT(${constraint?.constraintTableFields?.map((c) => c.value)?.join(", ")}) DO UPDATE SET ${set_sql}`;
const columns = constraint?.constraintTableFields
?.map((c) => sqlQuoteIdent(String(c.value)))
?.join(", ");
if (!columns)
continue;
sql += ` ON CONFLICT(${columns}) DO UPDATE SET ${set_sql}`;
}
}
}
+5
View File
@@ -0,0 +1,5 @@
import type { BUN_SQLITE_FieldSchemaType } from "../../types";
/**
* Build column definition SQL
*/
export default function buildColumnDefinition(field: BUN_SQLITE_FieldSchemaType, is_vector?: boolean): string;
+52
View File
@@ -0,0 +1,52 @@
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
import mapDataType from "./map-data-types";
/**
* Build column definition SQL
*/
export default function buildColumnDefinition(field, is_vector) {
if (!field.fieldName) {
throw new Error("Field name is required");
}
// vec0 can't parse quoted column names
const fieldName = is_vector
? field.sideCar
? `+${field.fieldName}`
: `${field.fieldName}`
: q(field.fieldName);
const parts = [fieldName];
// Data type mapping
parts.push(mapDataType(field));
// Primary key
if (field.primaryKey) {
parts.push("PRIMARY KEY");
if (field.autoIncrement) {
parts.push("AUTOINCREMENT");
}
}
// Not null
if (field.notNullValue && !field.primaryKey) {
parts.push("NOT NULL");
}
// Unique
if (field.unique && !field.primaryKey) {
parts.push("UNIQUE");
}
if (field.vectorDistanceMetric) {
parts.push(`distance_metric=${field.vectorDistanceMetric}`);
}
// Default value
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "string") {
parts.push(
// Escape single quotes by doubling them to prevent SQL injection and wrap in single quotes
`DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`);
}
else {
parts.push(`DEFAULT ${field.defaultValue}`);
}
}
else if (field.defaultValueLiteral) {
parts.push(`DEFAULT ${field.defaultValueLiteral}`);
}
return parts.join(" ");
}
+10
View File
@@ -0,0 +1,10 @@
import type { BUN_SQLITE_DatabaseSchemaType, BUN_SQLITE_TableSchemaType } from "../../types";
type Params = {
db_schema: BUN_SQLITE_DatabaseSchemaType;
table: BUN_SQLITE_TableSchemaType;
};
/**
* Build the `CREATE TABLE` (or `CREATE VIRTUAL TABLE`) statement for a table
*/
export default function buildCreateTableSql({ db_schema, table }: Params): string;
export {};
+32
View File
@@ -0,0 +1,32 @@
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
import buildColumnDefinition from "./build-column-definition";
import buildForeignKeyConstraint from "./build-foreign-key-constraint";
import resolveTable from "./resolve-table";
/**
* Build the `CREATE TABLE` (or `CREATE VIRTUAL TABLE`) statement for a table
*/
export default function buildCreateTableSql({ db_schema, table }) {
const new_table = resolveTable({ table, db_schema });
const columns = [];
const foreignKeys = [];
for (const field of new_table.fields) {
columns.push(buildColumnDefinition(field, new_table.isVector));
if (field.foreignKey && !new_table.isVector) {
foreignKeys.push(buildForeignKeyConstraint(field));
}
}
// Add unique constraints
for (const constraint of new_table.uniqueConstraints || []) {
if (!constraint.constraintTableFields?.length)
continue;
const fieldNames = constraint.constraintTableFields.map((f) => String(f.value));
const constraintName = constraint.constraintName || `unique_${fieldNames.join(", ")}`;
columns.push(`CONSTRAINT ${q(constraintName)} UNIQUE (${fieldNames
.map(q)
.join(", ")})`);
}
const allConstraints = [...columns, ...foreignKeys];
return new_table.isVector
? `CREATE VIRTUAL TABLE ${q(new_table.tableName)} USING ${new_table.vectorType || "vec0"}(${allConstraints.join(", ")})`
: `CREATE TABLE ${q(new_table.tableName)} (${allConstraints.join(", ")})`;
}
+5
View File
@@ -0,0 +1,5 @@
import type { BUN_SQLITE_FieldSchemaType } from "../../types";
/**
* Build foreign key constraint
*/
export default function buildForeignKeyConstraint(field: BUN_SQLITE_FieldSchemaType): string;
+15
View File
@@ -0,0 +1,15 @@
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
/**
* Build foreign key constraint
*/
export default function buildForeignKeyConstraint(field) {
const fk = field.foreignKey;
let constraint = `FOREIGN KEY (${q(field.fieldName)}) REFERENCES ${q(String(fk.destinationTableName))}(${q(String(fk.destinationTableColumnName))})`;
if (fk.cascadeDelete) {
constraint += " ON DELETE CASCADE";
}
if (fk.cascadeUpdate) {
constraint += " ON UPDATE CASCADE";
}
return constraint;
}
+7
View File
@@ -0,0 +1,7 @@
import type { Database } from "bun:sqlite";
type Params = {
db: Database;
table_name: string;
};
export default function checkIfTableExists({ db, table_name }: Params): boolean;
export {};
+6
View File
@@ -0,0 +1,6 @@
export default function checkIfTableExists({ db, table_name }) {
const tableExists = db
.query(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`)
.get(table_name);
return Boolean(tableExists?.name);
}
+4
View File
@@ -0,0 +1,4 @@
import type { Database } from "bun:sqlite";
export default function createDBManagerTable({ db }: {
db: Database;
}): void;
+11
View File
@@ -0,0 +1,11 @@
import { AppData } from "../../data/app-data";
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
export default function createDBManagerTable({ db }) {
db.run(`
CREATE TABLE IF NOT EXISTS ${q(AppData["DbSchemaManagerTableName"])} (
table_name TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
}
+11
View File
@@ -0,0 +1,11 @@
import type { CreateDBSchemaParams } from "../../types";
/**
* # Sync the database to the schema
* @description Changed tables are rebuilt by copying into a new table and
* dropping the old one. With foreign keys ON, `DROP TABLE` runs an implicit
* `DELETE FROM` that fires `ON DELETE CASCADE` on child tables, so keys are
* disabled for the sync and checked before committing. (`PRAGMA
* foreign_keys` is a no-op inside a transaction.) With `dry_run` the
* transaction is rolled back, so `changes` is the exact plan.
*/
export default function createDBSchema(params: CreateDBSchemaParams): void;
+66
View File
@@ -0,0 +1,66 @@
import _ from "lodash";
import createDBManagerTable from "./create-db-manager-table";
import dropRemovedTables from "./drop-removed-tables";
import getExistingTablesFromTablesManagerTable from "./get-existing-tables-from-tables-manager-table";
import handleDBSchemaTable from "./handle-db-schema-table";
import orderDBSchema from "./order-db-schema";
/**
* # Sync the database to the schema
* @description Changed tables are rebuilt by copying into a new table and
* dropping the old one. With foreign keys ON, `DROP TABLE` runs an implicit
* `DELETE FROM` that fires `ON DELETE CASCADE` on child tables, so keys are
* disabled for the sync and checked before committing. (`PRAGMA
* foreign_keys` is a no-op inside a transaction.) With `dry_run` the
* transaction is rolled back, so `changes` is the exact plan.
*/
export default function createDBSchema(params) {
const { db, dry_run } = params;
/**
* Reorder Tables (parents before children with FKs)
*/
const db_schema = orderDBSchema(params);
db.run("PRAGMA foreign_keys = OFF;");
db.run("BEGIN");
try {
/**
* Create Schema Manager Table
*/
createDBManagerTable({ db });
const existing_tables = getExistingTablesFromTablesManagerTable({
db,
});
/**
* Create or update tables
*/
for (const table of db_schema.tables) {
handleDBSchemaTable({
...params,
db_schema,
table,
existing_tables,
});
}
/**
* Drop tables that no longer exist in schema
*/
dropRemovedTables({
...params,
existing_tables,
schema_tables: db_schema.tables.map((t) => t.tableName),
});
const fkViolations = db.query(`PRAGMA foreign_key_check`).all();
if (fkViolations.length) {
const tables = _.uniq(fkViolations.map((v) => `${v.table} -> ${v.parent}`));
throw new Error(`Foreign key violations after schema sync: ${tables.join(", ")}`);
}
db.run(dry_run ? "ROLLBACK" : "COMMIT");
}
catch (error) {
if (db.inTransaction)
db.run("ROLLBACK");
throw error;
}
finally {
db.run("PRAGMA foreign_keys = ON;");
}
}
+9
View File
@@ -0,0 +1,9 @@
import type { BUN_SQLITE_TableSchemaType, CreateDBSchemaParams } from "../../types";
type Params = CreateDBSchemaParams & {
table: BUN_SQLITE_TableSchemaType;
};
/**
* Create a new table
*/
export default function createTable(params: Params): void;
export {};
+12
View File
@@ -0,0 +1,12 @@
import buildCreateTableSql from "./build-create-table-sql";
import recordSchemaChange from "./record-schema-change";
/**
* Create a new table
*/
export default function createTable(params) {
const { db, db_schema, table } = params;
recordSchemaChange(params, {
description: `Create table "${table.tableName}"`,
});
db.run(buildCreateTableSql({ db_schema, table }));
}
+10
View File
@@ -0,0 +1,10 @@
import type { CreateDBSchemaParams } from "../../types";
type Params = CreateDBSchemaParams & {
existing_tables: string[];
schema_tables: string[];
};
/**
* Drop tables that are no longer in the schema
*/
export default function dropRemovedTables(params: Params): void;
export {};
+34
View File
@@ -0,0 +1,34 @@
import _ from "lodash";
import { readLiveSchema } from "../../functions/live-schema";
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
import removeDBManagerTable from "./remove-db-manager-table";
import recordSchemaChange from "./record-schema-change";
import checkIfTableExists from "./check-if-table-exists";
/**
* Drop tables that are no longer in the schema
*/
export default function dropRemovedTables(params) {
const { db, existing_tables, schema_tables } = params;
const tablesToDrop = existing_tables.filter((t) => !schema_tables.includes(t) &&
!schema_tables.find((scT) => t.startsWith(scT + "_")));
const currentSchema = readLiveSchema();
for (const table of currentSchema?.tables || []) {
if (!table?.tableName)
continue;
if (!schema_tables.includes(table.tableName)) {
tablesToDrop.push(table.tableName);
}
}
for (const tableName of _.uniq(tablesToDrop)) {
if (!checkIfTableExists({ db, table_name: tableName })) {
removeDBManagerTable({ db, table_name: tableName });
continue;
}
recordSchemaChange(params, {
description: `Drop table "${tableName}"`,
destructive: true,
});
db.run(`DROP TABLE IF EXISTS ${q(tableName)}`);
removeDBManagerTable({ db, table_name: tableName });
}
}
@@ -0,0 +1,4 @@
import type { Database } from "bun:sqlite";
export default function getExistingTablesFromTablesManagerTable({ db, }: {
db: Database;
}): string[];
@@ -0,0 +1,8 @@
import { AppData } from "../../data/app-data";
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
export default function getExistingTablesFromTablesManagerTable({ db, }) {
const results = db
.query(`SELECT table_name FROM ${q(AppData["DbSchemaManagerTableName"])}`)
.all();
return results.map((r) => r.table_name);
}
+10
View File
@@ -0,0 +1,10 @@
import type { Database } from "bun:sqlite";
type Params = {
db: Database;
table_name: string;
};
export default function getTableColumns({ db, table_name, }: Params): {
name: string;
type: string;
}[];
export {};
+4
View File
@@ -0,0 +1,4 @@
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
export default function getTableColumns({ db, table_name, }) {
return db.query(`PRAGMA table_info(${q(table_name)})`).all();
}
+10
View File
@@ -0,0 +1,10 @@
import type { BUN_SQLITE_TableSchemaType, CreateDBSchemaParams } from "../../types";
type Params = CreateDBSchemaParams & {
table: BUN_SQLITE_TableSchemaType;
existing_tables: string[];
};
/**
* Sync a single table (rename, create or update, then indexes)
*/
export default function handleDBSchemaTable(params: Params): void;
export {};
+34
View File
@@ -0,0 +1,34 @@
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
import createTable from "./create-table";
import insertDBManagerTable from "./insert-db-manager-table";
import recordSchemaChange from "./record-schema-change";
import removeDBManagerTable from "./remove-db-manager-table";
import syncIndexes from "./sync-indexes";
import updateTable from "./update-table";
/**
* Sync a single table (rename, create or update, then indexes)
*/
export default function handleDBSchemaTable(params) {
const { db, table, existing_tables } = params;
let tableExists = existing_tables.includes(table.tableName);
// Handle table rename
if (table.tableNameOld &&
table.tableNameOld !== table.tableName &&
existing_tables.includes(table.tableNameOld)) {
recordSchemaChange(params, {
description: `Rename table "${table.tableNameOld}" -> "${table.tableName}"`,
});
db.run(`ALTER TABLE ${q(table.tableNameOld)} RENAME TO ${q(table.tableName)}`);
insertDBManagerTable({ db, table_name: table.tableName });
removeDBManagerTable({ db, table_name: table.tableNameOld });
tableExists = true;
}
if (!tableExists) {
createTable(params);
insertDBManagerTable({ db, table_name: table.tableName });
}
else {
updateTable(params);
}
syncIndexes(params);
}
+7
View File
@@ -0,0 +1,7 @@
import type { Database } from "bun:sqlite";
type Params = {
db: Database;
table_name: string;
};
export default function insertDBManagerTable({ db, table_name }: Params): void;
export {};
+5
View File
@@ -0,0 +1,5 @@
import { AppData } from "../../data/app-data";
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
export default function insertDBManagerTable({ db, table_name }) {
db.run(`INSERT INTO ${q(AppData["DbSchemaManagerTableName"])} (table_name,created_at,updated_at) VALUES (?, ?, ?)`, [table_name, Date.now(), Date.now()]);
}
+5
View File
@@ -0,0 +1,5 @@
import type { BUN_SQLITE_DATATYPES, BUN_SQLITE_FieldSchemaType } from "../../types";
/**
* Map DSQL data types to SQLite types
*/
export default function mapDataType(field: BUN_SQLITE_FieldSchemaType): (typeof BUN_SQLITE_DATATYPES)[number]["value"];
+40
View File
@@ -0,0 +1,40 @@
/**
* Map DSQL data types to SQLite types
*/
export default function mapDataType(field) {
const dataType = field.dataType?.toLowerCase() || "text";
const vectorSize = field.vectorSize || 1536;
// Vector Embeddings
if (field.isVector) {
return `FLOAT[${vectorSize}]`;
}
// Integer types
if (dataType.includes("int") ||
dataType === "bigint" ||
dataType === "smallint" ||
dataType === "tinyint") {
return "INTEGER";
}
// Real/Float types
if (dataType.includes("real") ||
dataType.includes("float") ||
dataType.includes("double") ||
dataType === "decimal" ||
dataType === "numeric") {
return "REAL";
}
// Blob types
if (dataType.includes("blob") || dataType.includes("binary")) {
return "BLOB";
}
// Boolean
if (dataType === "boolean" || dataType === "bool") {
return "INTEGER"; // SQLite uses INTEGER for boolean (0/1)
}
// Date/Time types
if (dataType.includes("date") || dataType.includes("time")) {
return "TEXT"; // SQLite stores dates as TEXT or INTEGER
}
// Default to TEXT for all text-based types
return "TEXT";
}
+13
View File
@@ -0,0 +1,13 @@
import type { BUN_SQLITE_DatabaseSchemaType, CreateDBSchemaParams } from "../../types";
/**
* Topologically order tables so referenced (parent) tables come before the
* tables that reference them. Declaration order is kept wherever the FK
* graph allows it.
*
* - Self references (e.g. `comments.parent_comment_id`) are ignored.
* - References to tables outside the schema are ignored.
* - Cycles do not throw: a table already on the current path is skipped,
* so every table is still emitted exactly once. Foreign keys are not
* enforced during the sync, so cycles are safe.
*/
export default function orderDBSchema({ db_schema, }: Pick<CreateDBSchemaParams, "db_schema">): BUN_SQLITE_DatabaseSchemaType;
+49
View File
@@ -0,0 +1,49 @@
import _ from "lodash";
/**
* Topologically order tables so referenced (parent) tables come before the
* tables that reference them. Declaration order is kept wherever the FK
* graph allows it.
*
* - Self references (e.g. `comments.parent_comment_id`) are ignored.
* - References to tables outside the schema are ignored.
* - Cycles do not throw: a table already on the current path is skipped,
* so every table is still emitted exactly once. Foreign keys are not
* enforced during the sync, so cycles are safe.
*/
export default function orderDBSchema({ db_schema, }) {
const new_db_schema = _.cloneDeep(db_schema);
const tables = new_db_schema.tables;
const tables_by_name = new Map();
for (const table of tables) {
if (table)
tables_by_name.set(table.tableName, table);
}
const ordered = [];
const visited = new Set();
const visiting = new Set();
function visit(table) {
if (visited.has(table.tableName) || visiting.has(table.tableName)) {
return;
}
visiting.add(table.tableName);
for (const field of table.fields || []) {
const dst_table_name = field?.foreignKey?.destinationTableName;
if (!dst_table_name || dst_table_name === table.tableName) {
continue;
}
const fk_table = tables_by_name.get(dst_table_name);
if (fk_table) {
visit(fk_table);
}
}
visiting.delete(table.tableName);
visited.add(table.tableName);
ordered.push(table);
}
for (const table of tables) {
if (table)
visit(table);
}
new_db_schema.tables = ordered;
return new_db_schema;
}
+7
View File
@@ -0,0 +1,7 @@
import type { CreateDBSchemaParams, SchemaChange } from "../../types";
/**
* # Record a schema change before it runs
* @description Logs the change (unless this is a dry run) and refuses
* destructive changes that were not approved.
*/
export default function recordSchemaChange(params: Pick<CreateDBSchemaParams, "dry_run" | "changes" | "allow_destructive">, change: SchemaChange): void;
+19
View File
@@ -0,0 +1,19 @@
/**
* # Record a schema change before it runs
* @description Logs the change (unless this is a dry run) and refuses
* destructive changes that were not approved.
*/
export default function recordSchemaChange(params, change) {
params.changes?.push(change);
if (params.dry_run) {
return;
}
const allowed = params.allow_destructive;
if (change.destructive &&
allowed !== undefined &&
allowed !== true &&
!allowed.has(change.description)) {
throw new Error(`Refusing unapproved destructive change: ${change.description}. Re-run with --dry-run to review, or --yes to allow it.`);
}
console.log(change.description);
}
+7
View File
@@ -0,0 +1,7 @@
import type { Database } from "bun:sqlite";
type Params = {
db: Database;
table_name: string;
};
export default function removeDBManagerTable({ db, table_name }: Params): void;
export {};
+5
View File
@@ -0,0 +1,5 @@
import { AppData } from "../../data/app-data";
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
export default function removeDBManagerTable({ db, table_name }) {
db.run(`DELETE FROM ${q(AppData["DbSchemaManagerTableName"])} WHERE table_name = ?`, [table_name]);
}
+12
View File
@@ -0,0 +1,12 @@
import type { BUN_SQLITE_DatabaseSchemaType, BUN_SQLITE_TableSchemaType } from "../../types";
type Params = {
table: BUN_SQLITE_TableSchemaType;
db_schema: BUN_SQLITE_DatabaseSchemaType;
};
/**
* Resolve a table's effective definition. Tables with `parentTableName`
* take their structure from the parent. Always returns a copy: the schema
* objects are never mutated.
*/
export default function resolveTable({ table, db_schema, }: Params): BUN_SQLITE_TableSchemaType;
export {};
+19
View File
@@ -0,0 +1,19 @@
import _ from "lodash";
/**
* Resolve a table's effective definition. Tables with `parentTableName`
* take their structure from the parent. Always returns a copy: the schema
* objects are never mutated.
*/
export default function resolveTable({ table, db_schema, }) {
if (!table.parentTableName) {
return _.cloneDeep(table);
}
const parent_table = db_schema.tables.find((t) => t.tableName === table.parentTableName);
if (!parent_table) {
throw new Error(`Parent table \`${table.parentTableName}\` not found for \`${table.tableName}\``);
}
return _.merge(_.cloneDeep(parent_table), {
tableName: table.tableName,
tableDescription: table.tableDescription,
});
}
+9
View File
@@ -0,0 +1,9 @@
import type { BUN_SQLITE_TableSchemaType, CreateDBSchemaParams } from "../../types";
type Params = CreateDBSchemaParams & {
table: BUN_SQLITE_TableSchemaType;
};
/**
* Sync indexes for a table
*/
export default function syncIndexes(params: Params): void;
export {};
+39
View File
@@ -0,0 +1,39 @@
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
import recordSchemaChange from "./record-schema-change";
/**
* Sync indexes for a table
*/
export default function syncIndexes(params) {
const { db, table } = params;
if (!table.indexes || table.indexes.length === 0) {
return;
}
const existingIndexes = db
.query(`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=? AND name NOT LIKE 'sqlite_%'`)
.all(table.tableName).map((r) => r.name);
// Drop indexes not in schema
for (const indexName of existingIndexes) {
const stillExists = table.indexes.some((idx) => idx.indexName === indexName);
if (!stillExists) {
recordSchemaChange(params, {
description: `Drop index "${indexName}"`,
});
db.run(`DROP INDEX IF EXISTS ${q(indexName)}`);
}
}
// Create new indexes
for (const index of table.indexes) {
if (!index.indexName || !index.indexTableFields?.length) {
continue;
}
if (!existingIndexes.includes(index.indexName)) {
recordSchemaChange(params, {
description: `Create index "${index.indexName}" on "${table.tableName}"`,
});
const fields = index.indexTableFields
.map((f) => q(String(f)))
.join(", ");
db.run(`CREATE INDEX ${q(index.indexName)} ON ${q(table.tableName)} (${fields})`);
}
}
}
+13
View File
@@ -0,0 +1,13 @@
import type { BUN_SQLITE_TableSchemaType, CreateDBSchemaParams } from "../../types";
type Params = CreateDBSchemaParams & {
table: BUN_SQLITE_TableSchemaType;
};
/**
* # Update an existing table
* @description SQLite has limited ALTER TABLE support, so a changed table is
* rebuilt: create a temp table with the new schema, copy the data over,
* drop the old table and rename the temp table. Unchanged tables are left
* alone.
*/
export default function updateTable(params: Params): void;
export {};
+88
View File
@@ -0,0 +1,88 @@
import { sqlQuoteIdent as q } from "../../utils/sql-safe";
import buildCreateTableSql from "./build-create-table-sql";
import checkIfTableExists from "./check-if-table-exists";
import getTableColumns from "./get-table-columns";
import recordSchemaChange from "./record-schema-change";
import resolveTable from "./resolve-table";
/**
* # Update an existing table
* @description SQLite has limited ALTER TABLE support, so a changed table is
* rebuilt: create a temp table with the new schema, copy the data over,
* drop the old table and rename the temp table. Unchanged tables are left
* alone.
*/
export default function updateTable(params) {
const { db, db_schema, table } = params;
if (table.isVector) {
recreateVectorTable(params);
return;
}
/**
* SQLite keeps the `CREATE TABLE` text (with the renamed table name), so
* an identical statement means the table already matches the schema.
*/
const current = db
.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name=?`)
.get(table.tableName);
if (current?.sql === buildCreateTableSql({ db_schema, table })) {
return;
}
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
// Columns are matched by name only: SQLite converts values to the new
// column affinity on copy, so a type change keeps the data instead of
// silently dropping the column.
const existingColumns = getTableColumns({ db, table_name: table.tableName });
const schemaColumns = resolveTable({ table, db_schema }).fields.map((f) => f.fieldName);
const columnsToKeep = existingColumns
.map((c) => c.name)
.filter((name) => schemaColumns.includes(name));
const columnsToDrop = existingColumns
.map((c) => c.name)
.filter((name) => !schemaColumns.includes(name));
recordSchemaChange(params, {
description: `Rebuild table "${table.tableName}"`,
});
for (const column of columnsToDrop) {
recordSchemaChange(params, {
description: `Drop column "${table.tableName}"."${column}"`,
destructive: true,
});
}
// Create temp table with new schema
db.run(buildCreateTableSql({
db_schema,
table: { ...table, tableName: tempTableName },
}));
// Copy data if there are common columns
if (columnsToKeep.length > 0) {
const columnList = columnsToKeep.map(q).join(", ");
db.run(`INSERT INTO ${q(tempTableName)} (${columnList}) SELECT ${columnList} FROM ${q(table.tableName)}`);
}
db.run(`DROP TABLE ${q(table.tableName)}`);
db.run(`ALTER TABLE ${q(tempTableName)} RENAME TO ${q(table.tableName)}`);
}
/**
* Vector tables are only rebuilt when `recreate_vector_table` is set.
*/
function recreateVectorTable(params) {
const { db, db_schema, table, recreate_vector_table } = params;
if (!recreate_vector_table) {
return;
}
recordSchemaChange(params, {
description: `Recreate vector table "${table.tableName}"`,
});
let existingRows = [];
if (checkIfTableExists({ db, table_name: table.tableName })) {
existingRows = db
.query(`SELECT * FROM ${q(table.tableName)}`)
.all();
db.run(`DROP TABLE ${q(table.tableName)}`);
}
db.run(buildCreateTableSql({ db_schema, table }));
for (const row of existingRows) {
const columns = Object.keys(row);
const placeholders = columns.map(() => "?").join(", ");
db.run(`INSERT INTO ${q(table.tableName)} (${columns.map(q).join(", ")}) VALUES (${placeholders})`, Object.values(row));
}
}
+3 -2
View File
@@ -1,4 +1,4 @@
import type { APIResponseObject, ServerQueryParam } from "../../types";
import type { APIResponseObject, BunSQLiteConfig, ServerQueryParam } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
@@ -7,10 +7,11 @@ type Params<Schema extends {
table: Table;
query?: ServerQueryParam<Schema>;
targetId?: number | string;
config?: BunSQLiteConfig;
};
export default function DbDelete<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, query, targetId, }: Params<Schema, Table>): Promise<APIResponseObject>;
}, Table extends string = string>({ table, query, targetId, config, }: Params<Schema, Table>): Promise<APIResponseObject>;
export {};
+19 -21
View File
@@ -1,34 +1,32 @@
import DbClient from ".";
import dbHandler from "../db-handler";
import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator";
export default async function DbDelete({ table, query, targetId, }) {
import { sqlIdent } from "../../utils/sql-safe";
export default async function DbDelete({ table, query, targetId, config, }) {
let sqlObj = null;
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge(finalQuery, {
query: {
id: {
value: String(targetId),
},
},
});
}
const finalQuery = query || {};
sqlObj = sqlGenerator({
tableName: table,
genObject: finalQuery,
genObject: _.pick(finalQuery, ["query", "searchOperator"]),
targetId,
});
const whereClause = sqlObj.string.match(/WHERE .*/)?.[0];
const whereClause = sqlObj.where.string;
if (whereClause) {
let sql = `DELETE FROM ${table} ${whereClause}`;
let sql = `DELETE FROM ${sqlIdent(table, "table name")} ${whereClause}`;
sqlObj.string = sql;
const res = DbClient.run(sql, sqlObj.values);
sqlObj.values = sqlObj.where.values;
const res = dbHandler({
query: sql,
values: sqlObj.values,
config,
});
if (!res.success) {
return { ...res, debug: { sqlObj } };
}
return {
success: Boolean(res.changes),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
success: Boolean(res.postInsertReturn?.affectedRows),
postInsertReturn: res.postInsertReturn,
debug: {
sqlObj,
},
+3 -2
View File
@@ -1,4 +1,4 @@
import type { APIResponseObject } from "../../types";
import type { APIResponseObject, BunSQLiteConfig } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
@@ -7,10 +7,11 @@ type Params<Schema extends {
table: Table;
data: Schema[];
update_on_duplicate?: boolean;
config?: BunSQLiteConfig;
};
export default function DbInsert<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, data, update_on_duplicate, }: Params<Schema, Table>): Promise<APIResponseObject>;
}, Table extends string = string>({ table, data, update_on_duplicate, config, }: Params<Schema, Table>): Promise<APIResponseObject>;
export {};
+12 -8
View File
@@ -1,7 +1,7 @@
import DbClient from ".";
import dbHandler from "../db-handler";
import sqlInsertGenerator from "../../utils/sql-insert-generator";
import grabDuplicateSafeInsertSql from "../grab-duplicate-safe-insert-sql";
export default async function DbInsert({ table, data, update_on_duplicate, }) {
export default async function DbInsert({ table, data, update_on_duplicate, config, }) {
let sqlObj = null;
try {
const finalData = data.map((d) => ({
@@ -19,13 +19,17 @@ export default async function DbInsert({ table, data, update_on_duplicate, }) {
sql = await grabDuplicateSafeInsertSql({ data, table, sql });
}
(sqlObj || {}).query = sql;
const res = DbClient.run(sql, sqlObj?.values || []);
const res = dbHandler({
query: sql,
values: sqlObj?.values || [],
config,
});
if (!res.success) {
return { ...res, debug: { sqlObj } };
}
return {
success: Boolean(Number(res.lastInsertRowid)),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
success: Boolean(res.postInsertReturn?.insertId),
postInsertReturn: res.postInsertReturn,
debug: {
sqlObj,
},
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env bun
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
declare class SQLiteSchemaManager {
private db;
private db_manager_table_name;
private recreate_vector_table;
private db_schema;
constructor({ schema, recreate_vector_table, }: {
schema: BUN_SQLITE_DatabaseSchemaType;
recreate_vector_table?: boolean;
});
private createDbManagerTable;
private insertDbManagerTable;
private removeDbManagerTable;
/**
* Main synchronization method
*/
syncSchema(): Promise<void>;
/**
* Get list of existing tables in the database
*/
private getExistingTables;
/**
* Drop tables that are no longer in the schema
*/
private dropRemovedTables;
/**
* Sync a single table (create or update)
*/
private syncTable;
/**
* Create a new table
*/
private createTable;
/**
* Update an existing table
*/
private updateTable;
/**
* Get existing columns for a table
*/
private getTableColumns;
/**
* Add a new column to existing table
*/
private addColumn;
private checkIfTableExists;
/**
* Recreate table (for complex schema changes)
*/
private recreateTable;
/**
* Build column definition SQL
*/
private buildColumnDefinition;
/**
* Map DSQL data types to SQLite types
*/
private mapDataType;
/**
* Build foreign key constraint
*/
private buildForeignKeyConstraint;
/**
* Sync indexes for a table
*/
private syncIndexes;
/**
* Close database connection
*/
close(): void;
}
export { SQLiteSchemaManager };
-425
View File
@@ -1,425 +0,0 @@
#!/usr/bin/env bun
import { Database } from "bun:sqlite";
import _ from "lodash";
import DbClient from ".";
import { AppData } from "../../data/app-data";
import { readLiveSchema } from "../../functions/live-schema";
// Schema Manager Class
class SQLiteSchemaManager {
db;
db_manager_table_name;
recreate_vector_table;
db_schema;
constructor({ schema, recreate_vector_table = false, }) {
this.db = DbClient;
this.db_manager_table_name = AppData["DbSchemaManagerTableName"];
this.db.run("PRAGMA foreign_keys = ON;");
this.recreate_vector_table = recreate_vector_table;
this.createDbManagerTable();
this.db_schema = schema;
}
createDbManagerTable() {
this.db.run(`
CREATE TABLE IF NOT EXISTS ${this.db_manager_table_name} (
table_name TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)
`);
}
insertDbManagerTable(tableName) {
this.db.run(`INSERT INTO ${this.db_manager_table_name} (table_name,created_at,updated_at) VALUES (?, ?, ?)`, [tableName, Date.now(), Date.now()]);
}
removeDbManagerTable(tableName) {
this.db.run(`DELETE FROM ${this.db_manager_table_name} WHERE table_name = ?`, [tableName]);
}
/**
* Main synchronization method
*/
async syncSchema() {
console.log("Starting schema synchronization...");
const existingTables = this.getExistingTables();
const schemaTables = this.db_schema.tables.map((t) => t.tableName);
// 2. Create or update tables
for (const table of this.db_schema.tables) {
await this.syncTable(table, existingTables);
}
// 1. Drop tables that no longer exist in schema
await this.dropRemovedTables(existingTables, schemaTables);
console.log("Schema synchronization complete!");
}
/**
* Get list of existing tables in the database
*/
getExistingTables() {
let sql = `SELECT table_name FROM ${this.db_manager_table_name}`;
const query = this.db.query(sql);
const results = query.all();
return results.map((r) => r.table_name);
}
/**
* Drop tables that are no longer in the schema
*/
async dropRemovedTables(existingTables, schemaTables) {
console.log(`Cleaning up tables ...`);
const tablesToDrop = existingTables.filter((t) => !schemaTables.includes(t) &&
!schemaTables.find((scT) => t.startsWith(scT + "_")));
const currentSchema = readLiveSchema();
if (currentSchema?.tables?.[0]) {
for (let i = 0; i < currentSchema.tables.length; i++) {
const table = currentSchema.tables[i];
if (!table?.tableName)
continue;
const does_table_exist = schemaTables.find((t) => t == table.tableName);
if (!does_table_exist) {
tablesToDrop.push(table.tableName);
}
}
}
for (const tableName of tablesToDrop) {
console.log(`Dropping table: ${tableName}`);
this.db.run(`DROP TABLE IF EXISTS "${tableName}"`);
this.db.run(`DELETE FROM ${this.db_manager_table_name} WHERE table_name = "${tableName}"`);
}
}
/**
* Sync a single table (create or update)
*/
async syncTable(table, existingTables) {
let tableExists = existingTables.includes(table.tableName);
// Handle table rename
if (table.tableNameOld && table.tableNameOld !== table.tableName) {
if (existingTables.includes(table.tableNameOld)) {
console.log(`Renaming table: ${table.tableNameOld} -> ${table.tableName}`);
this.db.run(`ALTER TABLE "${table.tableNameOld}" RENAME TO "${table.tableName}"`);
this.insertDbManagerTable(table.tableName);
this.removeDbManagerTable(table.tableNameOld);
tableExists = true;
}
}
if (!tableExists) {
// Create new table
await this.createTable(table);
this.insertDbManagerTable(table.tableName);
}
else {
// Update existing table
await this.updateTable(table);
}
// Sync indexes
await this.syncIndexes(table);
}
/**
* Create a new table
*/
async createTable(table) {
if (!table.tableName.match(/_temp_\d+$/)) {
console.log(`Creating table: ${table.tableName}`);
}
let new_table = _.cloneDeep(table);
if (new_table.parentTableName) {
const parent_table = this.db_schema.tables.find((t) => t.tableName === new_table.parentTableName);
if (!parent_table) {
throw new Error(`Parent table \`${new_table.parentTableName}\` not found for \`${new_table.tableName}\``);
}
new_table = _.merge(parent_table, {
tableName: new_table.tableName,
tableDescription: new_table.tableDescription,
});
}
const columns = [];
const foreignKeys = [];
for (const field of new_table.fields) {
const columnDef = this.buildColumnDefinition(field, table.isVector);
columns.push(columnDef);
if (field.foreignKey && !table.isVector) {
foreignKeys.push(this.buildForeignKeyConstraint(field));
}
}
// Add unique constraints
if (new_table.uniqueConstraints) {
for (const constraint of new_table.uniqueConstraints) {
if (constraint.constraintTableFields &&
constraint.constraintTableFields.length > 0) {
const fields = constraint.constraintTableFields
.map((f) => `"${f.value}"`)
.join(", ");
const constraintName = constraint.constraintName ||
`unique_${fields.replace(/"/g, "")}`;
columns.push(`CONSTRAINT "${constraintName}" UNIQUE (${fields})`);
}
}
}
const allConstraints = [...columns, ...foreignKeys];
const sql = new_table.isVector
? `CREATE VIRTUAL TABLE "${new_table.tableName}" USING ${new_table.vectorType || "vec0"}(${allConstraints.join(", ")})`
: `CREATE TABLE "${new_table.tableName}" (${allConstraints.join(", ")})`;
this.db.run(sql);
}
/**
* Update an existing table
*/
async updateTable(table) {
console.log(`Updating table: ${table.tableName}`);
// const existingColumns = this.getTableColumns(table.tableName);
// const schemaColumns = table.fields.map((f) => f.fieldName || "");
// // SQLite has limited ALTER TABLE support
// // We need to use the recreation strategy for complex changes
// const columnsToAdd = table.fields.filter(
// (f) =>
// f.fieldName &&
// !existingColumns.find(
// (c) =>
// c.name == f.fieldName && c.type == this.mapDataType(f),
// ),
// );
// // const columnsToRemove = existingColumns.filter(
// // (c) => !schemaColumns.includes(c.name),
// // );
// // const columnsToUpdate = table.fields.filter(
// // (f) =>
// // f.fieldName &&
// // f.updatedField &&
// // existingColumns.find(
// // (c) =>
// // c.name == f.fieldName && c.type == this.mapDataType(f),
// // ),
// // );
// for (const field of columnsToAdd) {
// await this.addColumn(table.tableName, field);
// }
// // // Simple case: only adding columns
// // if (columnsToRemove.length === 0 && columnsToUpdate.length === 0) {
// // for (const field of columnsToAdd) {
// // await this.addColumn(table.tableName, field);
// // }
// // } else {
// // // Complex case: need to recreate table
// // }
await this.recreateTable(table);
}
/**
* Get existing columns for a table
*/
getTableColumns(tableName) {
const query = this.db.query(`PRAGMA table_info("${tableName}")`);
const results = query.all();
return results;
}
/**
* Add a new column to existing table
*/
async addColumn(tableName, field) {
console.log(`Adding column: ${tableName}.${field.fieldName}`);
const columnDef = this.buildColumnDefinition(field);
// Remove PRIMARY KEY and UNIQUE constraints for ALTER TABLE ADD COLUMN
const cleanDef = columnDef
.replace(/PRIMARY KEY/gi, "")
.replace(/AUTOINCREMENT/gi, "")
.replace(/UNIQUE/gi, "")
.trim();
const sql = `ALTER TABLE "${tableName}" ADD COLUMN ${cleanDef}`;
this.db.run(sql);
}
checkIfTableExists(table) {
const tableExists = this.db
.query(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`)
.get(table);
return Boolean(tableExists?.name);
}
/**
* Recreate table (for complex schema changes)
*/
async recreateTable(table) {
if (table.isVector) {
if (!this.recreate_vector_table) {
return;
}
const does_table_exist = this.checkIfTableExists(table.tableName);
let existingRows = [];
if (does_table_exist) {
existingRows = this.db
.query(`SELECT * FROM "${table.tableName}"`)
.all();
this.db.run(`DROP TABLE "${table.tableName}"`);
}
await this.createTable(table);
if (existingRows.length > 0) {
for (let i = 0; i < existingRows.length; i++) {
const row = existingRows[i];
if (!row)
continue;
const columns = Object.keys(row);
const placeholders = columns.map(() => "?").join(", ");
this.db.run(`INSERT INTO "${table.tableName}" (${columns.join(", ")}) VALUES (${placeholders})`, Object.values(row));
}
}
return;
}
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
// Get existing data
const existingColumns = this.getTableColumns(table.tableName);
const columnsToKeep = table.fields
.filter((f) => f.fieldName &&
existingColumns.find((c) => c.name == f.fieldName &&
c.type == this.mapDataType(f)))
.map((f) => f.fieldName);
// Create temp table with new schema
const tempTable = { ...table, tableName: tempTableName };
await this.createTable(tempTable);
// Copy data if there are common columns
if (columnsToKeep.length > 0) {
const columnList = columnsToKeep.map((c) => `"${c}"`).join(", ");
this.db.run(`INSERT INTO "${tempTableName}" (${columnList}) SELECT ${columnList} FROM "${table.tableName}"`);
}
// Drop old table
this.db.run(`DROP TABLE "${table.tableName}"`);
// Rename temp table
this.db.run(`ALTER TABLE "${tempTableName}" RENAME TO "${table.tableName}"`);
}
/**
* Build column definition SQL
*/
buildColumnDefinition(field, is_vector) {
if (!field.fieldName) {
throw new Error("Field name is required");
}
const fieldName = field.sideCar
? `+${field.fieldName}`
: `${field.fieldName}`;
const parts = [fieldName];
// Data type mapping
const dataType = this.mapDataType(field);
parts.push(dataType);
// Primary key
if (field.primaryKey) {
parts.push("PRIMARY KEY");
if (field.autoIncrement) {
parts.push("AUTOINCREMENT");
}
}
// Not null
if (field.notNullValue || field.primaryKey) {
if (!field.primaryKey) {
parts.push("NOT NULL");
}
}
// Unique
if (field.unique && !field.primaryKey) {
parts.push("UNIQUE");
}
if (field.vectorDistanceMetric) {
parts.push(`distance_metric=${field.vectorDistanceMetric}`);
}
// Default value
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "string") {
parts.push(
// Escape single quotes by doubling them to prevent SQL injection and wrap in single quotes
`DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`);
}
else {
parts.push(`DEFAULT ${field.defaultValue}`);
}
}
else if (field.defaultValueLiteral) {
parts.push(`DEFAULT ${field.defaultValueLiteral}`);
}
return parts.join(" ");
}
/**
* Map DSQL data types to SQLite types
*/
mapDataType(field) {
const dataType = field.dataType?.toLowerCase() || "text";
const vectorSize = field.vectorSize || 1536;
// Vector Embeddings
if (field.isVector) {
return `FLOAT[${vectorSize}]`;
}
// Integer types
if (dataType.includes("int") ||
dataType === "bigint" ||
dataType === "smallint" ||
dataType === "tinyint") {
return "INTEGER";
}
// Real/Float types
if (dataType.includes("real") ||
dataType.includes("float") ||
dataType.includes("double") ||
dataType === "decimal" ||
dataType === "numeric") {
return "REAL";
}
// Blob types
if (dataType.includes("blob") || dataType.includes("binary")) {
return "BLOB";
}
// Boolean
if (dataType === "boolean" || dataType === "bool") {
return "INTEGER"; // SQLite uses INTEGER for boolean (0/1)
}
// Date/Time types
if (dataType.includes("date") || dataType.includes("time")) {
return "TEXT"; // SQLite stores dates as TEXT or INTEGER
}
// Default to TEXT for all text-based types
return "TEXT";
}
/**
* Build foreign key constraint
*/
buildForeignKeyConstraint(field) {
const fk = field.foreignKey;
let constraint = `FOREIGN KEY ("${field.fieldName}") REFERENCES "${fk.destinationTableName}"("${fk.destinationTableColumnName}")`;
if (fk.cascadeDelete) {
constraint += " ON DELETE CASCADE";
}
if (fk.cascadeUpdate) {
constraint += " ON UPDATE CASCADE";
}
return constraint;
}
/**
* Sync indexes for a table
*/
async syncIndexes(table) {
if (!table.indexes || table.indexes.length === 0) {
return;
}
// Get existing indexes
const query = this.db.query(`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='${table.tableName}' AND name NOT LIKE 'sqlite_%'`);
const existingIndexes = query.all().map((r) => r.name);
// Drop indexes not in schema
for (const indexName of existingIndexes) {
const stillExists = table.indexes.some((idx) => idx.indexName === indexName);
if (!stillExists) {
console.log(`Dropping index: ${indexName}`);
this.db.run(`DROP INDEX IF EXISTS "${indexName}"`);
}
}
// Create new indexes
for (const index of table.indexes) {
if (!index.indexName ||
!index.indexTableFields ||
index.indexTableFields.length === 0) {
continue;
}
if (!existingIndexes.includes(index.indexName)) {
console.log(`Creating index: ${index.indexName}`);
const fields = index.indexTableFields
.map((f) => `"${f}"`)
.join(", ");
this.db.run(`CREATE INDEX "${index.indexName}" ON "${table.tableName}" (${fields})`);
}
}
}
/**
* Close database connection
*/
close() {
this.db.close();
}
}
export { SQLiteSchemaManager };
+1 -1
View File
@@ -18,7 +18,7 @@ export default function dbSchemaToType({ config, dbSchema, }) {
if (final_table.parentTableName) {
const parent_table = dbTablesSchemas.find((t) => t.tableName === final_table.parentTableName);
if (parent_table) {
final_table = _.merge(parent_table, {
final_table = _.merge(_.cloneDeep(parent_table), {
tableName: final_table.tableName,
tableDescription: final_table.tableDescription,
});
+3 -2
View File
@@ -1,4 +1,4 @@
import type { APIResponseObject, ServerQueryParam } from "../../types";
import type { APIResponseObject, BunSQLiteConfig, ServerQueryParam } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
@@ -8,10 +8,11 @@ type Params<Schema extends {
table: Table;
count?: boolean;
targetId?: number | string;
config?: BunSQLiteConfig;
};
export default function DbSelect<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, query, count, targetId, }: Params<Schema, Table>): Promise<APIResponseObject<Schema>>;
}, Table extends string = string>({ table, query, count, targetId, config, }: Params<Schema, Table>): Promise<APIResponseObject<Schema>>;
export {};
+32 -23
View File
@@ -1,27 +1,24 @@
import mysql from "mysql";
import DbClient from ".";
import _ from "lodash";
import dbHandler from "../db-handler";
import sqlGenerator from "../../utils/sql-generator";
export default async function DbSelect({ table, query, count, targetId, }) {
export default async function DbSelect({ table, query, count, targetId, config, }) {
let sqlObj = null;
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge(finalQuery, {
query: {
id: {
value: String(targetId),
},
},
});
}
const finalQuery = query || {};
sqlObj = sqlGenerator({
tableName: table,
genObject: finalQuery,
targetId,
});
let sql = mysql.format(sqlObj.string, sqlObj.values);
const res = DbClient.query(sql);
const batchRes = res.all();
const sql = sqlObj.string;
const res = dbHandler({
query: sql,
values: sqlObj.values,
config,
});
if (!res.success) {
return { ...res, debug: { sqlObj, sql } };
}
const batchRes = res.payload || [];
let resp = {
success: Boolean(batchRes[0]),
payload: batchRes,
@@ -32,16 +29,28 @@ export default async function DbSelect({ table, query, count, targetId, }) {
},
};
if (count) {
let count_sql_object = sqlGenerator({
const count_sql_object = sqlGenerator({
tableName: table,
genObject: finalQuery,
targetId,
count,
});
let count_sql = mysql.format(count_sql_object.string, count_sql_object.values);
count_sql = `SELECT COUNT(*) FROM (${count_sql}) as c`;
const count_res = DbClient.query(count_sql).all();
const count_val = count_res[0]?.["COUNT(*)"];
resp["count"] = Number(count_val);
const count_sql = `SELECT COUNT(*) AS count FROM (${count_sql_object.string}) AS c`;
const count_res = dbHandler({
query: count_sql,
values: count_sql_object.values,
config,
});
if (!count_res.success) {
return {
...resp,
success: false,
error: count_res.error,
msg: "Database count failed",
debug: { ...resp.debug, count_sql },
};
}
resp["count"] = Number(count_res.singleRes?.count);
resp["debug"]["count_sql"] = count_sql;
}
return resp;
+3 -2
View File
@@ -1,11 +1,12 @@
import type { APIResponseObject, SQLInsertGenValueType } from "../../types";
import type { APIResponseObject, BunSQLiteConfig, SQLInsertGenValueType } from "../../types";
type Params = {
sql: string;
values?: SQLInsertGenValueType[];
config?: BunSQLiteConfig;
};
export default function DbSQL<T extends {
[k: string]: any;
} = {
[k: string]: any;
}>({ sql, values }: Params): Promise<APIResponseObject<T>>;
}>({ sql, values, config }: Params): Promise<APIResponseObject<T>>;
export {};
+7 -23
View File
@@ -1,21 +1,12 @@
import DbClient from ".";
import _ from "lodash";
export default async function DbSQL({ sql, values }) {
try {
import dbHandler from "../db-handler";
export default async function DbSQL({ sql, values, config }) {
const trimmed_sql = sql.trim();
const res = trimmed_sql.match(/^select/i)
? DbClient.query(trimmed_sql).all(...(values || []))
: DbClient.run(trimmed_sql, values || []);
const res = dbHandler({ query: trimmed_sql, values, config });
if (!res.success) {
return res;
}
return {
success: true,
payload: Array.isArray(res) ? res : undefined,
singleRes: Array.isArray(res) ? res?.[0] : undefined,
postInsertReturn: Array.isArray(res)
? undefined
: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
...res,
debug: {
sqlObj: {
sql: trimmed_sql,
@@ -25,10 +16,3 @@ export default async function DbSQL({ sql, values }) {
},
};
}
catch (error) {
return {
success: false,
error: error.message,
};
}
}
+3 -2
View File
@@ -1,4 +1,4 @@
import type { APIResponseObject, ServerQueryParam } from "../../types";
import type { APIResponseObject, BunSQLiteConfig, ServerQueryParam } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
@@ -8,10 +8,11 @@ type Params<Schema extends {
data: Schema;
query?: ServerQueryParam<Schema>;
targetId?: number | string;
config?: BunSQLiteConfig;
};
export default function DbUpdate<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, data, query, targetId, }: Params<Schema, Table>): Promise<APIResponseObject>;
}, Table extends string = string>({ table, data, query, targetId, config, }: Params<Schema, Table>): Promise<APIResponseObject>;
export {};
+22 -28
View File
@@ -1,27 +1,24 @@
import DbClient from ".";
import dbHandler from "../db-handler";
import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator";
export default async function DbUpdate({ table, data, query, targetId, }) {
let sqlObj = { string: "", values: [] };
import { sqlValue } from "../../utils/sql-insert-generator";
import { sqlIdent } from "../../utils/sql-safe";
export default async function DbUpdate({ table, data, query, targetId, config, }) {
let sqlObj = {
string: "",
values: [],
};
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge(finalQuery, {
query: {
id: {
value: String(targetId),
},
},
});
}
const finalQuery = query || {};
const sqlQueryObj = sqlGenerator({
tableName: table,
genObject: finalQuery,
genObject: _.pick(finalQuery, ["query", "searchOperator"]),
targetId,
});
let values = [];
const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0];
const whereClause = sqlQueryObj.where.string;
if (whereClause) {
let sql = `UPDATE ${table} SET`;
let sql = `UPDATE ${sqlIdent(table, "table name")} SET`;
const finalData = {
updated_at: Date.now(),
...data,
@@ -32,26 +29,23 @@ export default async function DbUpdate({ table, data, query, targetId, }) {
if (!key)
continue;
const isLast = i == keys.length - 1;
sql += ` ${key}=?`;
const value = finalData[key];
values.push(typeof value == "number" || typeof value == "string"
? value
: value || null);
sql += ` ${sqlIdent(key, "column name")}=?`;
values.push(sqlValue(finalData[key]));
if (!isLast) {
sql += `,`;
}
}
sql += ` ${whereClause}`;
values = [...values, ...sqlQueryObj.values];
values = [...values, ...sqlQueryObj.where.values];
sqlObj.string = sql;
sqlObj.values = values;
const res = DbClient.run(sql, values);
const res = dbHandler({ query: sql, values, config });
if (!res.success) {
return { ...res, debug: { sqlObj } };
}
return {
success: Boolean(res.changes),
postInsertReturn: {
affectedRows: res.changes,
insertId: Number(res.lastInsertRowid),
},
success: Boolean(res.postInsertReturn?.affectedRows),
postInsertReturn: res.postInsertReturn,
debug: {
sqlObj,
},
-3
View File
@@ -1,3 +0,0 @@
import { Database } from "bun:sqlite";
declare const DbClient: Database;
export default DbClient;
-20
View File
@@ -1,20 +0,0 @@
import { Database } from "bun:sqlite";
import * as sqliteVec from "sqlite-vec";
import grabDirNames from "../../data/grab-dir-names";
import init from "../../functions/init";
import grabDBDir from "../../utils/grab-db-dir";
const { ROOT_DIR } = grabDirNames();
const { config } = await init();
let db_dir = ROOT_DIR;
if (config.db_dir) {
db_dir = config.db_dir;
}
const { db_file_path } = grabDBDir({ config });
const DbClient = new Database(db_file_path, {
create: true,
});
sqliteVec.load(DbClient);
if (config.wal_mode) {
DbClient.run("PRAGMA journal_mode = WAL;");
}
export default DbClient;
-2
View File
@@ -1,2 +0,0 @@
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
export declare const DbSchema: BUN_SQLITE_DatabaseSchemaType;
-5
View File
@@ -1,5 +0,0 @@
import _ from "lodash";
export const DbSchema = {
dbName: "travis-ai",
tables: [],
};
+36 -2
View File
@@ -1353,14 +1353,19 @@ export type BunSQLiteConfig = {
db_name: string;
/**
* The Name of the Database Schema File. Eg `db_schema.ts`. This is
* relative to `db_dir`, or root dir if `db_dir` is not provided
* relative to `db_dir`, or root dir if `db_dir` is not provided. When
* omitted, `schema.ts|js|json|yaml|yml` in `db_dir` is used.
*/
db_schema_file_name: string;
db_schema_file_name?: string;
/**
* The Directory for backups. Relative to db_dir.
*/
db_backup_dir?: string;
max_backups?: number;
/**
* Max number of archives kept in `{db_dir}/.exports` by `export`
*/
max_exports?: number;
/**
* The Root Directory for the DB file and schema
*/
@@ -1382,6 +1387,35 @@ export type BunSQLiteConfigReturn = {
config: BunSQLiteConfig;
dbSchema: BUN_SQLITE_DatabaseSchemaType;
};
/**
* Params shared by the schema sync functions in `lib/schema`.
*/
export type CreateDBSchemaParams = {
db: import("bun:sqlite").Database;
db_schema: BUN_SQLITE_DatabaseSchemaType;
recreate_vector_table?: boolean;
/**
* Run the sync inside a transaction and roll it back. The planned
* changes are recorded in `changes`.
*/
dry_run?: boolean;
/**
* Collects every change made (or planned) by the sync
*/
changes?: SchemaChange[];
/**
* Destructive changes that may run. `true` (or unset) allows all of
* them; otherwise only changes whose `description` is in the set.
*/
allow_destructive?: true | Set<string>;
};
/**
* A change made by the schema sync. Destructive changes discard data.
*/
export type SchemaChange = {
description: string;
destructive?: boolean;
};
/**
* Default fields automatically suggested for new tables.
*/
+25
View File
@@ -0,0 +1,25 @@
export declare const ExportArchiveMembers: {
readonly DbFileName: "database.sqlite";
readonly SchemaFileName: "schema";
};
export type ExportArchiveContents = {
db: Uint8Array;
schema: string;
/** Archive member basename, e.g. schema.ts / schema.json / schema.yaml */
schemaFileName: string;
};
export declare function isArchivePath(filePath: string): boolean;
export declare function isSqlPath(filePath: string): boolean;
export declare function isSQLiteFile(bytes: Uint8Array): boolean;
/**
* Create a portable export archive (tar / tar.gz / zip) with the database
* file + schema file.
*/
export declare function writeExportArchive({ contents, outPath, }: {
contents: ExportArchiveContents;
outPath: string;
}): Promise<void>;
/**
* Read a portable export archive (tar / tar.gz / zip).
*/
export declare function readExportArchive(archivePath: string): Promise<ExportArchiveContents>;
+173
View File
@@ -0,0 +1,173 @@
import fs from "fs";
import path from "path";
import { AppData } from "../data/app-data";
import grabDirNames from "../data/grab-dir-names";
import { isSchemaFileName } from "./resolve-and-load-data-file";
export const ExportArchiveMembers = {
DbFileName: "database.sqlite",
SchemaFileName: AppData.DbSchemaFileName,
};
const ARCHIVE_EXTENSIONS = [".tar.gz", ".tgz", ".tar", ".zip"];
/** Every SQLite database file starts with this header */
const SQLITE_HEADER = "SQLite format 3\0";
export function isArchivePath(filePath) {
const lower = filePath.toLowerCase();
return ARCHIVE_EXTENSIONS.some((ext) => lower.endsWith(ext));
}
export function isSqlPath(filePath) {
return filePath.toLowerCase().endsWith(".sql");
}
export function isSQLiteFile(bytes) {
return (new TextDecoder().decode(bytes.subarray(0, SQLITE_HEADER.length)) ===
SQLITE_HEADER);
}
/**
* Create a portable export archive (tar / tar.gz / zip) with the database
* file + schema file.
*/
export async function writeExportArchive({ contents, outPath, }) {
const lower = outPath.toLowerCase();
if (lower.endsWith(".zip")) {
await writeZipArchive({ contents, outPath });
return;
}
const members = {
[ExportArchiveMembers.DbFileName]: contents.db,
[contents.schemaFileName]: contents.schema,
};
const gzip = lower.endsWith(".gz") || lower.endsWith(".tgz");
if (gzip) {
await Bun.Archive.write(outPath, members, { compress: "gzip" });
}
else {
await Bun.Archive.write(outPath, members);
}
}
/**
* Read a portable export archive (tar / tar.gz / zip).
*/
export async function readExportArchive(archivePath) {
if (archivePath.toLowerCase().endsWith(".zip")) {
return readZipArchive(archivePath);
}
const bytes = await Bun.file(archivePath).bytes();
const archive = new Bun.Archive(bytes);
const files = await archive.files();
let db = null;
let schemaEntry = null;
for (const [entry, file] of files) {
const base = path.basename(entry);
if (!db && base === ExportArchiveMembers.DbFileName) {
db = await file.bytes();
}
else if (!schemaEntry && isSchemaFileName(base)) {
schemaEntry = {
content: await file.text(),
fileName: schemaMemberName(base),
};
}
}
return validateContents({ db, schemaEntry });
}
function schemaMemberName(base) {
return base === AppData.DbSchemaFileName ? "schema.ts" : base;
}
function validateContents({ db, schemaEntry, }) {
if (!db) {
throw new Error(`Archive is missing the database file (expected \`${ExportArchiveMembers.DbFileName}\`)`);
}
if (!isSQLiteFile(db)) {
throw new Error(`\`${ExportArchiveMembers.DbFileName}\` in the archive is not a SQLite database`);
}
if (!schemaEntry) {
throw new Error(`Archive is missing schema file (expected \`${AppData.DbSchemaFileName}.ts|js|json|yaml|yml\`)`);
}
return {
db,
schema: schemaEntry.content,
schemaFileName: schemaEntry.fileName,
};
}
async function runCommand(cmd, cwd) {
const proc = Bun.spawn(cmd, {
cwd,
stdout: "pipe",
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([
new Response(proc.stderr).text(),
proc.exited,
]);
if (exitCode !== 0) {
throw new Error(`${cmd[0]} failed (exit ${exitCode}): ${stderr || "unknown error"}. Ensure \`${cmd[0]}\` is installed, or use .tar.gz.`);
}
}
function makeTempDir(prefix) {
const { BUN_SQLITE_TEMP_DIR } = grabDirNames();
const tempDir = path.join(BUN_SQLITE_TEMP_DIR, `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
fs.mkdirSync(tempDir, { recursive: true });
return tempDir;
}
async function writeZipArchive({ contents, outPath, }) {
const tempDir = makeTempDir("export");
try {
fs.writeFileSync(path.join(tempDir, ExportArchiveMembers.DbFileName), contents.db);
fs.writeFileSync(path.join(tempDir, contents.schemaFileName), contents.schema, "utf-8");
await runCommand([
"zip",
"-q",
"-j",
path.resolve(outPath),
ExportArchiveMembers.DbFileName,
contents.schemaFileName,
], tempDir);
}
finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
async function readZipArchive(archivePath) {
const tempDir = makeTempDir("import");
try {
await runCommand([
"unzip",
"-q",
"-o",
path.resolve(archivePath),
"-d",
tempDir,
]);
let db = null;
let schemaEntry = null;
const stack = [tempDir];
while (stack.length) {
const current = stack.pop();
for (const entry of fs.readdirSync(current, {
withFileTypes: true,
})) {
const full = path.join(current, entry.name);
// Never follow symlinks from an extracted (untrusted) archive
if (entry.isDirectory()) {
stack.push(full);
}
else if (!entry.isFile()) {
continue;
}
else if (!db &&
entry.name === ExportArchiveMembers.DbFileName) {
db = new Uint8Array(fs.readFileSync(full));
}
else if (!schemaEntry && isSchemaFileName(entry.name)) {
schemaEntry = {
content: fs.readFileSync(full, "utf-8"),
fileName: schemaMemberName(entry.name),
};
}
}
}
return validateContents({ db, schemaEntry });
}
finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}
+1
View File
@@ -5,6 +5,7 @@ type Params = {
export default function grabDBDir({ config }: Params): {
db_dir: string;
backup_dir: string;
export_dir: string;
db_file_path: string;
};
export {};
+3 -2
View File
@@ -5,10 +5,11 @@ export default function grabDBDir({ config }) {
const { ROOT_DIR } = grabDirNames();
let db_dir = ROOT_DIR;
if (config.db_dir) {
db_dir = config.db_dir;
db_dir = path.resolve(ROOT_DIR, config.db_dir);
}
const backup_dir_name = config.db_backup_dir || AppData["DefaultBackupDirName"];
const backup_dir = path.resolve(db_dir, backup_dir_name);
const export_dir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
const db_file_path = path.resolve(db_dir, config.db_name);
return { db_dir, backup_dir, db_file_path };
return { db_dir, backup_dir, export_dir, db_file_path };
}
+1 -1
View File
@@ -1 +1 @@
export default function grabDbSchema(): Promise<import("../types").BUN_SQLITE_DatabaseSchemaType>;
export default function grabDbSchema(): Promise<import("..").BUN_SQLITE_DatabaseSchemaType>;
+1 -1
View File
@@ -1,5 +1,5 @@
import init from "../functions/init";
export default async function grabDbSchema() {
const { dbSchema } = await init();
const { dbSchema } = init();
return dbSchema;
}
+12 -5
View File
@@ -1,18 +1,25 @@
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import _ from "lodash";
export default function grabSortedBackups({ config }) {
const { backup_dir } = grabDBDir({ config });
const backups = fs.readdirSync(backup_dir);
if (!fs.existsSync(backup_dir))
return [];
/**
* Only files named by `grabDBBackupFileName` for this database. Anything
* else in the directory is never listed (or trimmed).
*/
const backup_name_regex = new RegExp(`^${_.escapeRegExp(config.db_name)}-\\d+$`);
const backups = fs
.readdirSync(backup_dir)
.filter((b) => backup_name_regex.test(b));
/**
* Order Backups. Most recent first.
*/
const ordered_backups = backups.sort((a, b) => {
const a_date = Number(a.split("-").pop());
const b_date = Number(b.split("-").pop());
if (a_date > b_date) {
return -1;
}
return 1;
return b_date - a_date;
});
return ordered_backups;
}
+6
View File
@@ -0,0 +1,6 @@
import type { BunSQLiteConfig } from "../types";
type Params = {
config: BunSQLiteConfig;
};
export default function grabSortedExports({ config }: Params): string[];
export {};
+27
View File
@@ -0,0 +1,27 @@
import fs from "fs";
import grabDBDir from "./grab-db-dir";
function stripExportExtension(name) {
const lower = name.toLowerCase();
if (lower.endsWith(".tar.gz"))
return name.slice(0, -7);
if (lower.endsWith(".tgz"))
return name.slice(0, -4);
if (lower.endsWith(".tar"))
return name.slice(0, -4);
if (lower.endsWith(".zip"))
return name.slice(0, -4);
return name;
}
function exportTimestamp(name) {
const base = stripExportExtension(name);
const ts = Number(base.split("-").pop());
return Number.isFinite(ts) ? ts : 0;
}
export default function grabSortedExports({ config }) {
const { export_dir } = grabDBDir({ config });
if (!fs.existsSync(export_dir)) {
return [];
}
const exports = fs.readdirSync(export_dir);
return exports.sort((a, b) => exportTimestamp(b) - exportTimestamp(a));
}
+13
View File
@@ -0,0 +1,13 @@
type Params = {
src: string;
db_file_path: string;
};
/**
* # Replace the database file with a copy of `src`
* @description All connections to `db_file_path` must be closed first.
* Leftover `-wal`/`-shm`/`-journal` files are removed so SQLite doesn't
* replay stale pages onto the new file, and the copy is swapped in with an
* atomic rename.
*/
export default function replaceDbFile({ src, db_file_path }: Params): void;
export {};
+16
View File
@@ -0,0 +1,16 @@
import fs from "fs";
/**
* # Replace the database file with a copy of `src`
* @description All connections to `db_file_path` must be closed first.
* Leftover `-wal`/`-shm`/`-journal` files are removed so SQLite doesn't
* replay stale pages onto the new file, and the copy is swapped in with an
* atomic rename.
*/
export default function replaceDbFile({ src, db_file_path }) {
const tmp_file_path = `${db_file_path}.replace-${Date.now()}`;
fs.copyFileSync(src, tmp_file_path);
for (const suffix of ["-wal", "-shm", "-journal"]) {
fs.rmSync(`${db_file_path}${suffix}`, { force: true });
}
fs.renameSync(tmp_file_path, db_file_path);
}
+27
View File
@@ -0,0 +1,27 @@
import { AppData } from "../data/app-data";
export type DataFileFormat = "ts" | "js" | "json" | "yaml";
export type ResolvedDataFile = {
path: string;
basename: string;
extension: (typeof AppData.SupportedDataFileExtensions)[number];
format: DataFileFormat;
};
export type LoadedDataFile<T> = ResolvedDataFile & {
data: T;
};
/**
* Resolve a basename (no extension) to an existing file among supported formats.
* Priority: .ts > .js > .json > .yaml > .yml
* Errors if multiple matches exist.
*/
export declare function resolveDataFile(dir: string, baseName: string): ResolvedDataFile | null;
export declare function supportedDataFileNames(baseName: string): string;
export declare function isSchemaFileName(name: string): boolean;
/**
* Load a data file as a plain object.
* - .ts / .js: require() and use default export (or module itself)
* - .json: JSON.parse
* - .yaml / .yml: Bun.YAML.parse
*/
export declare function loadDataFile<T>(resolved: ResolvedDataFile): T;
export declare function resolveAndLoadDataFile<T>(dir: string, baseName: string): LoadedDataFile<T> | null;
+86
View File
@@ -0,0 +1,86 @@
import fs from "fs";
import path from "path";
import { AppData } from "../data/app-data";
function extensionToFormat(extension) {
switch (extension) {
case ".ts":
return "ts";
case ".js":
return "js";
case ".json":
return "json";
case ".yaml":
case ".yml":
return "yaml";
}
}
/**
* Resolve a basename (no extension) to an existing file among supported formats.
* Priority: .ts > .js > .json > .yaml > .yml
* Errors if multiple matches exist.
*/
export function resolveDataFile(dir, baseName) {
const matches = [];
for (const extension of AppData.SupportedDataFileExtensions) {
const filePath = path.join(dir, `${baseName}${extension}`);
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
matches.push({
path: filePath,
basename: `${baseName}${extension}`,
extension,
format: extensionToFormat(extension),
});
}
}
if (matches.length === 0) {
return null;
}
if (matches.length > 1) {
const found = matches.map((m) => m.basename).join(", ");
throw new Error(`Multiple \`${baseName}\` files found (${found}). Keep only one of: ${AppData.SupportedDataFileExtensions.join(", ")}`);
}
return matches[0];
}
export function supportedDataFileNames(baseName) {
return AppData.SupportedDataFileExtensions.map((ext) => `\`${baseName}${ext}\``).join(", ");
}
export function isSchemaFileName(name) {
const base = path.basename(name);
if (base === AppData.DbSchemaFileName) {
return true;
}
return AppData.SupportedDataFileExtensions.some((ext) => base === `${AppData.DbSchemaFileName}${ext}`);
}
/**
* Load a data file as a plain object.
* - .ts / .js: require() and use default export (or module itself)
* - .json: JSON.parse
* - .yaml / .yml: Bun.YAML.parse
*/
export function loadDataFile(resolved) {
if (resolved.format === "ts" || resolved.format === "js") {
const imported = require(resolved.path);
const data = imported && typeof imported === "object" && "default" in imported
? imported.default
: imported;
if (data == null) {
throw new Error(`No default export from \`${resolved.path}\`. Please export a default module.`);
}
return data;
}
const text = fs.readFileSync(resolved.path, "utf-8");
if (resolved.format === "json") {
return JSON.parse(text);
}
return Bun.YAML.parse(text);
}
export function resolveAndLoadDataFile(dir, baseName) {
const resolved = resolveDataFile(dir, baseName);
if (!resolved) {
return null;
}
return {
...resolved,
data: loadDataFile(resolved),
};
}
+12
View File
@@ -0,0 +1,12 @@
import type { Database } from "bun:sqlite";
type Params = {
db: Database;
dst: string;
};
/**
* # Write a consistent copy of the database to `dst`
* @description Uses `VACUUM INTO`, which includes changes still in the WAL
* file (a plain file copy misses them).
*/
export default function snapshotDb({ db, dst }: Params): void;
export {};
+12
View File
@@ -0,0 +1,12 @@
import fs from "fs";
import path from "path";
/**
* # Write a consistent copy of the database to `dst`
* @description Uses `VACUUM INTO`, which includes changes still in the WAL
* file (a plain file copy misses them).
*/
export default function snapshotDb({ db, dst }) {
fs.mkdirSync(path.dirname(dst), { recursive: true });
fs.rmSync(dst, { force: true });
db.run("VACUUM INTO ?", [dst]);
}
+8 -3
View File
@@ -6,15 +6,20 @@ type Params = {
queryObj: ServerQueryObject<{
[key: string]: any;
}, string>;
/**
* `value` is an already-validated `table.field` reference rather than a
* literal, so it is inlined instead of bound
*/
isValueFieldValue?: boolean;
};
type Return = {
str?: string;
param?: SQLInsertGenValueType;
str: string;
params: SQLInsertGenValueType[];
};
/**
* # SQL Gen Operator Gen
* @description Generates an SQL operator for node module `mysql` or `serverless-mysql`
* @description Generates an SQL condition for a single field. Literal values
* are always returned in `params` to be bound, never inlined.
*/
export default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }: Params): Return;
export {};
+64 -104
View File
@@ -1,133 +1,93 @@
import sqlEqualityParser from "./sql-equality-parser";
import { sqlIdent } from "./sql-safe";
/**
* # SQL Gen Operator Gen
* @description Generates an SQL operator for node module `mysql` or `serverless-mysql`
* @description Generates an SQL condition for a single field. Literal values
* are always returned in `params` to be bound, never inlined.
*/
export default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }) {
if (queryObj.nullValue) {
return { str: `${fieldName} IS NULL` };
if (queryObj.nullValue || equality == "IS NULL") {
return { str: `${fieldName} IS NULL`, params: [] };
}
if (queryObj.notNullValue) {
return { str: `${fieldName} IS NOT NULL` };
if (queryObj.notNullValue || equality == "IS NOT NULL") {
return { str: `${fieldName} IS NOT NULL`, params: [] };
}
if (value) {
const finalValue = isValueFieldValue ? value : "?";
const finalParams = isValueFieldValue ? undefined : value;
if (equality == "MATCH") {
const finalValue = isValueFieldValue ? String(value) : "?";
const params = isValueFieldValue
? []
: [value ?? ""];
const wrappedValue = isValueFieldValue ? `'%' || ${value} || '%'` : "?";
const wrappedParams = isValueFieldValue
? []
: [`%${value ?? ""}%`];
switch (equality) {
case "MATCH":
case "MATCH_BOOLEAN":
case "FULLTEXT": {
const matchValue = queryObj.vectorFunction && !isValueFieldValue
? `${sqlIdent(queryObj.vectorFunction, "vector function")}(?)`
: finalValue;
return { str: `${fieldName} MATCH ${matchValue}`, params };
}
case "LIKE_LOWER":
return {
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN NATURAL LANGUAGE MODE)`,
param: finalParams,
str: `LOWER(${fieldName}) LIKE LOWER(${wrappedValue})`,
params: wrappedParams,
};
}
else if (equality == "MATCH_BOOLEAN") {
return {
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`,
param: finalParams,
};
}
else if (equality == "LIKE_LOWER") {
case "LIKE_LOWER_RAW":
return {
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
param: `%${finalParams}%`,
params,
};
}
else if (equality == "LIKE_LOWER_RAW") {
case "LIKE":
return {
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
param: finalParams,
str: `${fieldName} LIKE ${wrappedValue}`,
params: wrappedParams,
};
}
else if (equality == "LIKE") {
case "LIKE_RAW":
return { str: `${fieldName} LIKE ${finalValue}`, params };
case "NOT_LIKE_LOWER":
return {
str: `${fieldName} LIKE ${finalValue}`,
param: `%${finalParams}%`,
str: `LOWER(${fieldName}) NOT LIKE LOWER(${wrappedValue})`,
params: wrappedParams,
};
}
else if (equality == "LIKE_RAW") {
return {
str: `${fieldName} LIKE ${finalValue}`,
param: finalParams,
};
}
else if (equality == "NOT_LIKE_LOWER") {
case "NOT_LIKE_LOWER_RAW":
return {
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
param: `%${finalParams}%`,
params,
};
}
else if (equality == "NOT_LIKE_LOWER_RAW") {
case "NOT LIKE":
return {
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
param: finalParams,
str: `${fieldName} NOT LIKE ${wrappedValue}`,
params: wrappedParams,
};
}
else if (equality == "NOT LIKE") {
return {
str: `${fieldName} NOT LIKE ${finalValue}`,
param: finalParams,
};
}
else if (equality == "NOT LIKE_RAW") {
return {
str: `${fieldName} NOT LIKE ${finalValue}`,
param: finalParams,
};
}
else if (equality == "REGEXP") {
case "NOT LIKE_RAW":
return { str: `${fieldName} NOT LIKE ${finalValue}`, params };
case "REGEXP":
/**
* SQLite has no built-in REGEXP function. This only works when
* an extension providing `regexp()` is loaded.
*/
return {
str: `LOWER(${fieldName}) REGEXP LOWER(${finalValue})`,
param: finalParams,
params,
};
}
else if (equality == "FULLTEXT") {
return {
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`,
param: finalParams,
};
}
else if (equality == "NOT EQUAL") {
return {
str: `${fieldName} != ${finalValue}`,
param: finalParams,
};
}
else if (equality == "IS NOT") {
return {
str: `${fieldName} IS NOT ${finalValue}`,
param: finalParams,
};
}
else if (equality) {
case "NOT EQUAL":
return { str: `${fieldName} != ${finalValue}`, params };
case "IS NOT":
return { str: `${fieldName} IS NOT ${finalValue}`, params };
case "IN":
case "NOT IN":
return { str: `${fieldName} ${equality} (${finalValue})`, params };
case "BETWEEN":
case "NOT BETWEEN":
throw new Error(`\`${equality}\` on \`${fieldName}\` requires an array of two values`);
case undefined:
return { str: `${fieldName} = ${finalValue}`, params };
default:
return {
str: `${fieldName} ${sqlEqualityParser(equality)} ${finalValue}`,
param: finalParams,
};
}
else {
return {
str: `${fieldName} = ${finalValue}`,
param: finalParams,
params,
};
}
}
else {
if (equality == "IS NULL") {
return { str: `${fieldName} IS NULL` };
}
else if (equality == "IS NOT NULL") {
return { str: `${fieldName} IS NOT NULL` };
}
else if (equality) {
return {
str: `${fieldName} ${sqlEqualityParser(equality)} ?`,
param: value,
};
}
else {
return {
str: `${fieldName} = ?`,
param: value,
};
}
}
}
+17 -23
View File
@@ -1,3 +1,4 @@
import { sqlIdent, sqlOperator, sqlRawEquality } from "./sql-safe";
export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
let values = [];
if (mtch.__batch) {
@@ -13,7 +14,7 @@ export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
btch_mtch += str;
values.push(...batch_values);
if (i < mtch.__batch.matches.length - 1) {
btch_mtch += ` ${mtch.__batch.operator || "OR"} `;
btch_mtch += ` ${sqlOperator(mtch.__batch.operator, "OR")} `;
}
}
btch_mtch += `)`;
@@ -22,28 +23,8 @@ export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
values,
};
}
const equality = mtch.raw_equality || "=";
const lhs = `${typeof mtch.source == "object" ? mtch.source.tableName : table_name}.${typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source}`;
const rhs = `${(() => {
if (mtch.targetLiteral) {
values.push(mtch.targetLiteral);
// if (typeof mtch.targetLiteral == "number") {
// return `${mtch.targetLiteral}`;
// }
// return `'${mtch.targetLiteral}'`;
return `?`;
}
if (join.alias) {
return `${typeof mtch.target == "object"
? mtch.target.tableName
: join.alias}.${typeof mtch.target == "object"
? mtch.target.fieldName
: mtch.target}`;
}
return `${typeof mtch.target == "object"
? mtch.target.tableName
: join.tableName}.${typeof mtch.target == "object" ? mtch.target.fieldName : mtch.target}`;
})()}`;
const equality = mtch.raw_equality ? sqlRawEquality(mtch.raw_equality) : "=";
const lhs = `${sqlIdent(typeof mtch.source == "object" ? mtch.source.tableName : table_name, "table name")}.${sqlIdent(typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source, "field name")}`;
if (mtch.between) {
values.push(mtch.between.min, mtch.between.max);
return {
@@ -58,6 +39,19 @@ export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
values,
};
}
const rhs = (() => {
if (mtch.targetLiteral !== undefined && mtch.targetLiteral !== null) {
values.push(mtch.targetLiteral);
return `?`;
}
const targetTable = typeof mtch.target == "object"
? mtch.target.tableName
: join.alias || join.tableName;
const targetField = typeof mtch.target == "object"
? mtch.target.fieldName
: mtch.target;
return `${sqlIdent(targetTable, "table name")}.${sqlIdent(targetField, "field name")}`;
})();
return {
str: `${lhs} ${equality} ${rhs}`,
values,
-2
View File
@@ -8,8 +8,6 @@ type Param<T extends {
selectFields?: (keyof T | TableSelectFieldsObject<T>)[];
append_table_names?: boolean;
table_name: string;
full_text_match_str?: string;
full_text_search_str?: string;
};
export default function sqlGenGenQueryStr<T extends {
[key: string]: any;
+45 -53
View File
@@ -2,12 +2,11 @@ import { isUndefined } from "lodash";
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
import sqlGenGenJoinStr from "./sql-generator-gen-join-str";
import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql";
import { sqlIdent, sqlJoinType, sqlOperator } from "./sql-safe";
export default function sqlGenGenQueryStr(params) {
let str = "SELECT";
const genObject = params.genObject;
const table_name = params.table_name;
const full_text_match_str = params.full_text_match_str;
const full_text_search_str = params.full_text_search_str;
let sqlSearhValues = [];
if (genObject?.select_sql) {
str += ` ${genObject.select_sql}`;
@@ -41,93 +40,90 @@ export default function sqlGenGenQueryStr(params) {
const countSubQuery = genObject.countSubQueries[i];
if (!countSubQuery)
continue;
const tableAlias = countSubQuery.table_alias;
const countTable = sqlIdent(countSubQuery.table, "table name");
const tableAlias = countSubQuery.table_alias
? sqlIdent(countSubQuery.table_alias, "alias")
: undefined;
let subQStr = `(SELECT COUNT(*)`;
subQStr += ` FROM ${countSubQuery.table}${tableAlias ? ` ${tableAlias}` : ""}`;
subQStr += ` FROM ${countTable}${tableAlias ? ` ${tableAlias}` : ""}`;
subQStr += ` WHERE (`;
for (let j = 0; j < countSubQuery.srcTrgMap.length; j++) {
const csqSrc = countSubQuery.srcTrgMap[j];
if (!csqSrc)
continue;
subQStr += ` ${tableAlias || countSubQuery.table}.${csqSrc.src}`;
subQStr += ` ${tableAlias || countTable}.${sqlIdent(csqSrc.src, "field name")}`;
if (typeof csqSrc.trg == "string") {
subQStr += ` = ?`;
sqlSearhValues.push(csqSrc.trg);
}
else if (typeof csqSrc.trg == "object") {
subQStr += ` = ${csqSrc.trg.table}.${csqSrc.trg.field}`;
subQStr += ` = ${sqlIdent(csqSrc.trg.table, "table name")}.${sqlIdent(csqSrc.trg.field, "field name")}`;
}
if (j < countSubQuery.srcTrgMap.length - 1) {
subQStr += ` AND `;
}
}
subQStr += ` )) AS ${countSubQuery.alias}`;
subQStr += ` )) AS ${sqlIdent(countSubQuery.alias, "alias")}`;
countSqls.push(subQStr);
}
str += `, ${countSqls.join(",")}`;
}
if (genObject?.join) {
const existingJoinTableNames = [table_name];
str +=
"," +
genObject.join
const joinSelectStr = genObject.join
.flat()
.filter((j) => !isUndefined(j))
.map((joinObj) => {
const joinTableName = joinObj.alias
? joinObj.alias
: joinObj.tableName;
const joinTableName = sqlIdent(joinObj.alias ? joinObj.alias : joinObj.tableName, "join table name");
if (existingJoinTableNames.includes(joinTableName))
return null;
existingJoinTableNames.push(joinTableName);
if (joinObj.group_concat) {
return sqlGenGrabConcatStr({
field: `${joinTableName}.${joinObj.group_concat.field}`,
field: `${joinTableName}.${sqlIdent(joinObj.group_concat.field, "field name")}`,
alias: joinObj.group_concat.alias,
separator: joinObj.group_concat.separator,
distinct: joinObj.group_concat.distinct,
});
}
else if (joinObj.selectFields) {
return joinObj.selectFields
.map((selectField) => {
if (typeof selectField == "string") {
return `${joinTableName}.${selectField}`;
if (typeof selectField != "object") {
return `${joinTableName}.${sqlIdent(selectField, "field name")}`;
}
else if (typeof selectField == "object") {
let aliasSelectField = `${joinTableName}.${selectField.field}`;
if (selectField.count) {
aliasSelectField = `COUNT(${joinTableName}.${selectField.field})`;
}
else if (selectField.sum) {
aliasSelectField = `SUM(${selectField.distinct ? "DISTINCT " : ""}${joinTableName}.${selectField.field})`;
}
else if (selectField.average) {
aliasSelectField = `AVERAGE(${joinTableName}.${selectField.field})`;
}
else if (selectField.max) {
aliasSelectField = `MAX(${joinTableName}.${selectField.field})`;
}
else if (selectField.min) {
aliasSelectField = `MIN(${joinTableName}.${selectField.field})`;
}
else if (selectField.group_concat &&
const field = `${joinTableName}.${sqlIdent(selectField.field, "field name")}`;
if (selectField.group_concat &&
selectField.alias) {
return sqlGenGrabConcatStr({
field: `${joinTableName}.${selectField.field}`,
field,
alias: selectField.alias,
separator: selectField.group_concat
.separator,
distinct: selectField.group_concat
.distinct,
separator: selectField.group_concat.separator,
distinct: selectField.group_concat.distinct,
});
}
let aliasSelectField = field;
if (selectField.count) {
aliasSelectField = `COUNT(${field})`;
}
else if (selectField.sum) {
aliasSelectField = `SUM(${selectField.distinct ? "DISTINCT " : ""}${field})`;
}
else if (selectField.average) {
aliasSelectField = `AVG(${field})`;
}
else if (selectField.max) {
aliasSelectField = `MAX(${field})`;
}
else if (selectField.min) {
aliasSelectField = `MIN(${field})`;
}
else if (selectField.distinct) {
aliasSelectField = `DISTINCT ${joinTableName}.${selectField.field}`;
aliasSelectField = `DISTINCT ${field}`;
}
if (selectField.alias)
aliasSelectField += ` AS ${selectField.alias}`;
aliasSelectField += ` AS ${sqlIdent(selectField.alias, "alias")}`;
return aliasSelectField;
}
})
.join(",");
}
@@ -137,12 +133,9 @@ export default function sqlGenGenQueryStr(params) {
})
.filter((_) => Boolean(_))
.join(",");
if (joinSelectStr) {
str += "," + joinSelectStr;
}
if (genObject?.fullTextSearch &&
full_text_match_str &&
full_text_search_str) {
str += `, ${full_text_match_str} AS ${genObject.fullTextSearch.scoreAlias}`;
sqlSearhValues.push(full_text_search_str);
}
str += ` FROM ${table_name}`;
if (genObject?.join) {
@@ -152,11 +145,12 @@ export default function sqlGenGenQueryStr(params) {
.flat()
.filter((j) => !isUndefined(j))
.map((join) => {
return (join.joinType +
const joinTable = sqlIdent(join.tableName, "table name");
return (sqlJoinType(join.joinType) +
" " +
(join.alias
? `${join.tableName}` + " " + join.alias
: `${join.tableName}`) +
? `${joinTable} ${sqlIdent(join.alias, "alias")}`
: joinTable) +
" ON " +
(() => {
if (Array.isArray(join.match)) {
@@ -171,9 +165,7 @@ export default function sqlGenGenQueryStr(params) {
sqlSearhValues.push(...values);
return str;
})
.join(join.operator
? ` ${join.operator} `
: " AND ") +
.join(` ${sqlOperator(join.operator)} `) +
")");
}
else if (typeof join.match == "object") {

Some files were not shown because too many files have changed in this diff Show More