Major version update. Major bugfixes. Security vulnerabilites fixed
This commit is contained in:
Vendored
+1
-8
@@ -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) {
|
||||
|
||||
Vendored
+7
-5
@@ -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();
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from "commander";
|
||||
export default function (): Command;
|
||||
Vendored
+70
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { Command } from "commander";
|
||||
export default function (): Command;
|
||||
Vendored
+187
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
Vendored
-4
@@ -1,6 +1,2 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* # Declare Global Variables
|
||||
*/
|
||||
declare global { }
|
||||
export {};
|
||||
|
||||
+20
-2
@@ -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);
|
||||
});
|
||||
|
||||
Vendored
+8
-5
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Vendored
+74
-12
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
Vendored
+3
-3
@@ -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!`))}`);
|
||||
|
||||
Vendored
+6
-1
@@ -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"];
|
||||
};
|
||||
|
||||
Vendored
+6
-1
@@ -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
@@ -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 {};
|
||||
Vendored
+21
@@ -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;
|
||||
}
|
||||
Vendored
+18
-2
@@ -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;
|
||||
|
||||
Vendored
+70
-30
@@ -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 };
|
||||
}
|
||||
/**
|
||||
* # 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(", ")}`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Initialization ERROR => ` + error.message);
|
||||
process.exit(1);
|
||||
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
@@ -0,0 +1,6 @@
|
||||
import type { BunSQLiteConfig } from "../types";
|
||||
type Params = {
|
||||
config: BunSQLiteConfig;
|
||||
};
|
||||
export default function setSQLiteClient({ config }: Params): void;
|
||||
export {};
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import grabSQLiteClient from "./grab-sqlite-client";
|
||||
export default function setSQLiteClient({ config }) {
|
||||
global.SQLITE_CLIENT = grabSQLiteClient({ config });
|
||||
}
|
||||
Vendored
+2
@@ -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";
|
||||
|
||||
Vendored
+18
-6
@@ -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";
|
||||
|
||||
Vendored
+18
@@ -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 {};
|
||||
Vendored
+53
@@ -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
@@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(", ")})`;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { Database } from "bun:sqlite";
|
||||
export default function createDBManagerTable({ db }: {
|
||||
db: Database;
|
||||
}): void;
|
||||
+11
@@ -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
@@ -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;
|
||||
Vendored
+66
@@ -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;");
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -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 {};
|
||||
Vendored
+12
@@ -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
@@ -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
@@ -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
@@ -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 {};
|
||||
Vendored
+4
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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()]);
|
||||
}
|
||||
Vendored
+5
@@ -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"];
|
||||
Vendored
+40
@@ -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";
|
||||
}
|
||||
Vendored
+13
@@ -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;
|
||||
Vendored
+49
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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]);
|
||||
}
|
||||
Vendored
+12
@@ -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 {};
|
||||
Vendored
+19
@@ -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,
|
||||
});
|
||||
}
|
||||
Vendored
+9
@@ -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 {};
|
||||
Vendored
+39
@@ -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})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+13
@@ -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 {};
|
||||
Vendored
+88
@@ -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));
|
||||
}
|
||||
}
|
||||
Vendored
+3
-2
@@ -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 {};
|
||||
|
||||
Vendored
+19
-21
@@ -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,
|
||||
},
|
||||
|
||||
Vendored
+3
-2
@@ -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 {};
|
||||
|
||||
Vendored
+12
-8
@@ -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
@@ -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 };
|
||||
Vendored
-425
@@ -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
@@ -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,
|
||||
});
|
||||
|
||||
Vendored
+3
-2
@@ -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 {};
|
||||
|
||||
Vendored
+32
-23
@@ -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;
|
||||
|
||||
Vendored
+3
-2
@@ -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 {};
|
||||
|
||||
Vendored
+7
-23
@@ -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,
|
||||
@@ -24,11 +15,4 @@ export default async function DbSQL({ sql, values }) {
|
||||
sql,
|
||||
},
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+3
-2
@@ -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 {};
|
||||
|
||||
Vendored
+22
-28
@@ -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,
|
||||
},
|
||||
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
declare const DbClient: Database;
|
||||
export default DbClient;
|
||||
Vendored
-20
@@ -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;
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
import type { BUN_SQLITE_DatabaseSchemaType } from "../../types";
|
||||
export declare const DbSchema: BUN_SQLITE_DatabaseSchemaType;
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
import _ from "lodash";
|
||||
export const DbSchema = {
|
||||
dbName: "travis-ai",
|
||||
tables: [],
|
||||
};
|
||||
Vendored
+36
-2
@@ -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.
|
||||
*/
|
||||
|
||||
Vendored
+25
@@ -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>;
|
||||
Vendored
+173
@@ -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 });
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -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 {};
|
||||
|
||||
Vendored
+3
-2
@@ -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 };
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
export default function grabDbSchema(): Promise<import("../types").BUN_SQLITE_DatabaseSchemaType>;
|
||||
export default function grabDbSchema(): Promise<import("..").BUN_SQLITE_DatabaseSchemaType>;
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
import init from "../functions/init";
|
||||
export default async function grabDbSchema() {
|
||||
const { dbSchema } = await init();
|
||||
const { dbSchema } = init();
|
||||
return dbSchema;
|
||||
}
|
||||
|
||||
Vendored
+12
-5
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import type { BunSQLiteConfig } from "../types";
|
||||
type Params = {
|
||||
config: BunSQLiteConfig;
|
||||
};
|
||||
export default function grabSortedExports({ config }: Params): string[];
|
||||
export {};
|
||||
Vendored
+27
@@ -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));
|
||||
}
|
||||
Vendored
+13
@@ -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 {};
|
||||
Vendored
+16
@@ -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
@@ -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
@@ -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),
|
||||
};
|
||||
}
|
||||
Vendored
+12
@@ -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 {};
|
||||
Vendored
+12
@@ -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]);
|
||||
}
|
||||
Vendored
+8
-3
@@ -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 {};
|
||||
|
||||
Vendored
+64
-104
@@ -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,
|
||||
params,
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
str: `${fieldName} = ${finalValue}`,
|
||||
param: finalParams,
|
||||
};
|
||||
}
|
||||
}
|
||||
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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user