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 { Command } from "commander";
|
||||||
import init from "../../functions/init";
|
|
||||||
import grabDBDir from "../../utils/grab-db-dir";
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { select } from "@inquirer/prompts";
|
import { select } from "@inquirer/prompts";
|
||||||
import { Database } from "bun:sqlite";
|
|
||||||
import listTables from "./list-tables";
|
import listTables from "./list-tables";
|
||||||
import runSQL from "./run-sql";
|
import runSQL from "./run-sql";
|
||||||
import * as sqliteVec from "sqlite-vec";
|
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("admin")
|
return new Command("admin")
|
||||||
.description("View Tables and Data, Run SQL Queries, Etc.")
|
.description("View Tables and Data, Run SQL Queries, Etc.")
|
||||||
.action(async () => {
|
.action(async () => {
|
||||||
const { config } = await init();
|
const db = global.SQLITE_CLIENT;
|
||||||
const { db_file_path } = grabDBDir({ config });
|
|
||||||
const db = new Database(db_file_path);
|
|
||||||
sqliteVec.load(db);
|
|
||||||
console.log(chalk.bold(chalk.blue("\nBun SQLite Admin\n")));
|
console.log(chalk.bold(chalk.blue("\nBun SQLite Admin\n")));
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
Vendored
+7
-5
@@ -1,20 +1,22 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import init from "../functions/init";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import grabDBDir from "../utils/grab-db-dir";
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
import fs from "fs";
|
|
||||||
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
|
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import trimBackups from "../utils/trim-backups";
|
import trimBackups from "../utils/trim-backups";
|
||||||
|
import snapshotDb from "../utils/snapshot-db";
|
||||||
export default function () {
|
export default function () {
|
||||||
return new Command("backup")
|
return new Command("backup")
|
||||||
.description("Backup Database")
|
.description("Backup Database")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
console.log(`Backing up database ...`);
|
console.log(`Backing up database ...`);
|
||||||
const { config } = await init();
|
const config = global.CONFIG;
|
||||||
const { backup_dir, db_file_path } = grabDBDir({ config });
|
const { backup_dir } = grabDBDir({ config });
|
||||||
const new_db_file_name = grabDBBackupFileName({ 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 });
|
trimBackups({ config });
|
||||||
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))}`);
|
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))}`);
|
||||||
process.exit();
|
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
|
#!/usr/bin/env bun
|
||||||
/**
|
|
||||||
* # Declare Global Variables
|
|
||||||
*/
|
|
||||||
declare global { }
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
+20
-2
@@ -4,14 +4,27 @@ import schema from "./schema";
|
|||||||
import typedef from "./typedef";
|
import typedef from "./typedef";
|
||||||
import backup from "./backup";
|
import backup from "./backup";
|
||||||
import restore from "./restore";
|
import restore from "./restore";
|
||||||
|
import exportCmd from "./export";
|
||||||
|
import importCmd from "./import";
|
||||||
import admin from "./admin";
|
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
|
* # Describe Program
|
||||||
*/
|
*/
|
||||||
program
|
program
|
||||||
.name(`bun-sqlite`)
|
.name(`bun-sqlite`)
|
||||||
.description(`SQLite manager for Bun`)
|
.description(`SQLite manager for Bun`)
|
||||||
.version(`1.0.0`);
|
.version(version);
|
||||||
/**
|
/**
|
||||||
* # Declare Commands
|
* # Declare Commands
|
||||||
*/
|
*/
|
||||||
@@ -19,6 +32,8 @@ program.addCommand(schema());
|
|||||||
program.addCommand(typedef());
|
program.addCommand(typedef());
|
||||||
program.addCommand(backup());
|
program.addCommand(backup());
|
||||||
program.addCommand(restore());
|
program.addCommand(restore());
|
||||||
|
program.addCommand(exportCmd());
|
||||||
|
program.addCommand(importCmd());
|
||||||
program.addCommand(admin());
|
program.addCommand(admin());
|
||||||
/**
|
/**
|
||||||
* # Handle Unavailable Commands
|
* # Handle Unavailable Commands
|
||||||
@@ -30,4 +45,7 @@ program.on("command:*", () => {
|
|||||||
/**
|
/**
|
||||||
* # Parse Arguments
|
* # 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 { Command } from "commander";
|
||||||
import init from "../functions/init";
|
|
||||||
import grabDBDir from "../utils/grab-db-dir";
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
import fs from "fs";
|
import replaceDbFile from "../utils/replace-db-file";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import grabSortedBackups from "../utils/grab-sorted-backups";
|
import grabSortedBackups from "../utils/grab-sorted-backups";
|
||||||
import { select } from "@inquirer/prompts";
|
import { select } from "@inquirer/prompts";
|
||||||
@@ -12,7 +11,7 @@ export default function () {
|
|||||||
.description("Restore Database")
|
.description("Restore Database")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
console.log(`Restoring up database ...`);
|
console.log(`Restoring up database ...`);
|
||||||
const { config } = await init();
|
const config = global.CONFIG;
|
||||||
const { backup_dir, db_file_path } = grabDBDir({ config });
|
const { backup_dir, db_file_path } = grabDBDir({ config });
|
||||||
const backups = grabSortedBackups({ config });
|
const backups = grabSortedBackups({ config });
|
||||||
if (!backups?.[0]) {
|
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!`))}`);
|
console.log(`${chalk.bold(chalk.green(`DB Restore Success!`))}`);
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(`Backup Restore ERROR => ${error.message}`);
|
console.error(`Backup Restore ERROR => ${error.message}`);
|
||||||
process.exit();
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+78
-16
@@ -1,38 +1,96 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import { SQLiteSchemaManager } from "../lib/sqlite/db-schema-manager";
|
import createDBSchema from "../lib/schema/create-db-schema";
|
||||||
import init from "../functions/init";
|
|
||||||
import grabDirNames from "../data/grab-dir-names";
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
|
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
|
||||||
import _ from "lodash";
|
|
||||||
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
|
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { writeLiveSchema } from "../functions/live-schema";
|
import { writeLiveSchema } from "../functions/live-schema";
|
||||||
import grabDBDir from "../utils/grab-db-dir";
|
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 () {
|
export default function () {
|
||||||
return new Command("schema")
|
return new Command("schema")
|
||||||
.description("Build DB From Schema")
|
.description("Build DB From Schema")
|
||||||
.option("-v, --vector", "Recreate Vector Tables. This will drop and rebuild all vector tables")
|
.option("-v, --vector", "Recreate Vector Tables. This will drop and rebuild all vector tables")
|
||||||
.option("-t, --typedef", "Generate typescript type definitions")
|
.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) => {
|
.action(async (opts) => {
|
||||||
console.log(`Starting process ...`);
|
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 { ROOT_DIR, BUN_SQLITE_TEMP_DB_FILE_PATH } = grabDirNames();
|
||||||
const { db_file_path } = grabDBDir({ config });
|
const { db_file_path } = grabDBDir({ config });
|
||||||
cpSync(db_file_path, BUN_SQLITE_TEMP_DB_FILE_PATH);
|
const isVector = Boolean(opts.vector || opts.v);
|
||||||
|
const isTypeDef = Boolean(opts.typedef || opts.t);
|
||||||
|
const finaldbSchema = appendDefaultFieldsToDbSchema({
|
||||||
|
dbSchema,
|
||||||
|
});
|
||||||
|
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 {
|
try {
|
||||||
const isVector = Boolean(opts.vector || opts.v);
|
createDBSchema({ ...params, dry_run: true, changes: plan });
|
||||||
const isTypeDef = Boolean(opts.typedef || opts.t);
|
}
|
||||||
const finaldbSchema = appendDefaultFieldsToDbSchema({
|
catch (error) {
|
||||||
dbSchema,
|
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,
|
||||||
});
|
});
|
||||||
const manager = new SQLiteSchemaManager({
|
if (!proceed) {
|
||||||
schema: finaldbSchema,
|
console.log(chalk.dim(`Schema sync cancelled.`));
|
||||||
recreate_vector_table: isVector,
|
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)),
|
||||||
});
|
});
|
||||||
await manager.syncSchema();
|
DbClient.close();
|
||||||
manager.close();
|
|
||||||
if (isTypeDef && config.typedef_file_path) {
|
if (isTypeDef && config.typedef_file_path) {
|
||||||
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
|
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
|
||||||
dbSchemaToTypeDef({
|
dbSchemaToTypeDef({
|
||||||
@@ -47,7 +105,11 @@ export default function () {
|
|||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.log(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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+3
-3
@@ -1,5 +1,4 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import init from "../functions/init";
|
|
||||||
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
|
import dbSchemaToTypeDef from "../lib/sqlite/schema-to-typedef";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import grabDirNames from "../data/grab-dir-names";
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
@@ -10,7 +9,8 @@ export default function () {
|
|||||||
.description("Build DB From Schema")
|
.description("Build DB From Schema")
|
||||||
.action(async (opts) => {
|
.action(async (opts) => {
|
||||||
console.log(`Creating Type Definition From DB Schema ...`);
|
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 { ROOT_DIR } = grabDirNames();
|
||||||
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
|
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
|
||||||
if (config.typedef_file_path) {
|
if (config.typedef_file_path) {
|
||||||
@@ -22,7 +22,7 @@ export default function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
console.error(``);
|
console.error(`\`typedef_file_path\` is required in your config`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
|
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
|
||||||
|
|||||||
Vendored
+6
-1
@@ -1,6 +1,11 @@
|
|||||||
export declare const AppData: {
|
export declare const AppData: {
|
||||||
readonly ConfigFileName: "bun-sqlite.config.ts";
|
readonly ConfigFileName: "bun-sqlite.config";
|
||||||
readonly MaxBackups: 10;
|
readonly MaxBackups: 10;
|
||||||
|
readonly MaxExports: 10;
|
||||||
readonly DefaultBackupDirName: ".backups";
|
readonly DefaultBackupDirName: ".backups";
|
||||||
|
readonly DefaultExportDirName: ".exports";
|
||||||
readonly DbSchemaManagerTableName: "__db_schema_manager__";
|
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 = {
|
export const AppData = {
|
||||||
ConfigFileName: "bun-sqlite.config.ts",
|
ConfigFileName: "bun-sqlite.config",
|
||||||
MaxBackups: 10,
|
MaxBackups: 10,
|
||||||
|
MaxExports: 10,
|
||||||
DefaultBackupDirName: ".backups",
|
DefaultBackupDirName: ".backups",
|
||||||
|
DefaultExportDirName: ".exports",
|
||||||
DbSchemaManagerTableName: "__db_schema_manager__",
|
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";
|
import type { Database } from "bun:sqlite";
|
||||||
export default function init(): Promise<BunSQLiteConfigReturn>;
|
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
+79
-39
@@ -2,45 +2,85 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { AppData } from "../data/app-data";
|
import { AppData } from "../data/app-data";
|
||||||
import grabDirNames from "../data/grab-dir-names";
|
import grabDirNames from "../data/grab-dir-names";
|
||||||
export default async function init() {
|
import setSQLiteClient from "./set-sqlite-client";
|
||||||
try {
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
const { ROOT_DIR } = grabDirNames();
|
import { loadDataFile, resolveAndLoadDataFile, supportedDataFileNames, } from "../utils/resolve-and-load-data-file";
|
||||||
const { ConfigFileName } = AppData;
|
/**
|
||||||
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
|
* # Load config, schema and the SQLite client into globals
|
||||||
if (!fs.existsSync(ConfigFilePath)) {
|
* @description Runs once per process. Later calls return the loaded
|
||||||
console.log("ConfigFilePath", ConfigFilePath);
|
* globals.
|
||||||
console.error(`Please create a \`${ConfigFileName}\` file at the root of your project.`);
|
*/
|
||||||
process.exit(1);
|
export default function init() {
|
||||||
}
|
if (global.CONFIG && global.DB_SCHEMA && global.SQLITE_CLIENT) {
|
||||||
const ConfigImport = await import(ConfigFilePath);
|
return { config: global.CONFIG, dbSchema: global.DB_SCHEMA };
|
||||||
const Config = ConfigImport["default"];
|
|
||||||
if (!Config.db_name) {
|
|
||||||
console.error(`\`db_name\` is required in your config`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
if (!Config.db_schema_file_name) {
|
|
||||||
console.error(`\`db_schema_file_name\` is required in your config`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
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 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 });
|
|
||||||
}
|
|
||||||
return { config: Config, dbSchema: DbSchema };
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
const { ROOT_DIR } = grabDirNames();
|
||||||
console.error(`Initialization ERROR => ` + error.message);
|
const { ConfigFileName } = AppData;
|
||||||
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)" : ""}.`);
|
||||||
|
}
|
||||||
|
if (!Config.db_name) {
|
||||||
|
throw new Error(`\`db_name\` is required in your config`);
|
||||||
|
}
|
||||||
|
const { db_dir, backup_dir } = grabDBDir({ config: Config });
|
||||||
|
if (!fs.existsSync(db_dir)) {
|
||||||
|
fs.mkdirSync(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 });
|
||||||
|
}
|
||||||
|
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(", ")}`);
|
||||||
|
}
|
||||||
|
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 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 DbDelete from "./lib/sqlite/db-delete";
|
||||||
import DbInsert from "./lib/sqlite/db-insert";
|
import DbInsert from "./lib/sqlite/db-insert";
|
||||||
import DbSelect from "./lib/sqlite/db-select";
|
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 DbUpdate from "./lib/sqlite/db-update";
|
||||||
import grabDbSchema from "./utils/grab-db-schema";
|
import grabDbSchema from "./utils/grab-db-schema";
|
||||||
import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object";
|
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 = {
|
const BunSQLite = {
|
||||||
select: DbSelect,
|
select: withInit(DbSelect),
|
||||||
insert: DbInsert,
|
insert: withInit(DbInsert),
|
||||||
update: DbUpdate,
|
update: withInit(DbUpdate),
|
||||||
delete: DbDelete,
|
delete: withInit(DbDelete),
|
||||||
sql: DbSQL,
|
sql: withInit(DbSQL),
|
||||||
utils: {
|
utils: {
|
||||||
grab_db_schema: grabDbSchema,
|
grab_db_schema: withInit(grabDbSchema),
|
||||||
grab_join_fields_from_query_object: grabJoinFieldsFromQueryObject,
|
grab_join_fields_from_query_object: grabJoinFieldsFromQueryObject,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export default BunSQLite;
|
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 init from "../functions/init";
|
||||||
|
import { sqlIdent, sqlQuoteIdent } from "../utils/sql-safe";
|
||||||
export default async function ({ sql: passed_sql, table, data }) {
|
export default async function ({ sql: passed_sql, table, data }) {
|
||||||
let sql = passed_sql;
|
let sql = passed_sql;
|
||||||
const { dbSchema } = await init();
|
const { dbSchema } = init();
|
||||||
const table_schema = dbSchema.tables.find((t) => t.tableName == table);
|
const table_schema = dbSchema.tables.find((t) => t.tableName == table);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (table_schema?.tableName) {
|
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}`);
|
set_sql_arr.push(`updated_at = ${now}`);
|
||||||
const set_sql = set_sql_arr.join(", ");
|
const set_sql = set_sql_arr.join(", ");
|
||||||
const unique_fields = table_schema.fields.filter((f) => f.unique);
|
const unique_fields = table_schema.fields.filter((f) => f.unique);
|
||||||
for (let i = 0; i < unique_fields.length; i++) {
|
for (let i = 0; i < unique_fields.length; i++) {
|
||||||
const field = unique_fields[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]) {
|
if (table_schema.uniqueConstraints?.[0]) {
|
||||||
for (let i = 0; i < table_schema.uniqueConstraints.length; i++) {
|
for (let i = 0; i < table_schema.uniqueConstraints.length; i++) {
|
||||||
const constraint = table_schema.uniqueConstraints[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 {
|
type Params<Schema extends {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
} = {
|
} = {
|
||||||
@@ -7,10 +7,11 @@ type Params<Schema extends {
|
|||||||
table: Table;
|
table: Table;
|
||||||
query?: ServerQueryParam<Schema>;
|
query?: ServerQueryParam<Schema>;
|
||||||
targetId?: number | string;
|
targetId?: number | string;
|
||||||
|
config?: BunSQLiteConfig;
|
||||||
};
|
};
|
||||||
export default function DbDelete<Schema extends {
|
export default function DbDelete<Schema extends {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
} = {
|
} = {
|
||||||
[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 {};
|
export {};
|
||||||
|
|||||||
Vendored
+19
-21
@@ -1,34 +1,32 @@
|
|||||||
import DbClient from ".";
|
import dbHandler from "../db-handler";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import sqlGenerator from "../../utils/sql-generator";
|
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;
|
let sqlObj = null;
|
||||||
try {
|
try {
|
||||||
let finalQuery = query || {};
|
const finalQuery = query || {};
|
||||||
if (targetId) {
|
|
||||||
finalQuery = _.merge(finalQuery, {
|
|
||||||
query: {
|
|
||||||
id: {
|
|
||||||
value: String(targetId),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
sqlObj = sqlGenerator({
|
sqlObj = sqlGenerator({
|
||||||
tableName: table,
|
tableName: table,
|
||||||
genObject: finalQuery,
|
genObject: _.pick(finalQuery, ["query", "searchOperator"]),
|
||||||
|
targetId,
|
||||||
});
|
});
|
||||||
const whereClause = sqlObj.string.match(/WHERE .*/)?.[0];
|
const whereClause = sqlObj.where.string;
|
||||||
if (whereClause) {
|
if (whereClause) {
|
||||||
let sql = `DELETE FROM ${table} ${whereClause}`;
|
let sql = `DELETE FROM ${sqlIdent(table, "table name")} ${whereClause}`;
|
||||||
sqlObj.string = sql;
|
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 {
|
return {
|
||||||
success: Boolean(res.changes),
|
success: Boolean(res.postInsertReturn?.affectedRows),
|
||||||
postInsertReturn: {
|
postInsertReturn: res.postInsertReturn,
|
||||||
affectedRows: res.changes,
|
|
||||||
insertId: Number(res.lastInsertRowid),
|
|
||||||
},
|
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
sqlObj,
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+3
-2
@@ -1,4 +1,4 @@
|
|||||||
import type { APIResponseObject } from "../../types";
|
import type { APIResponseObject, BunSQLiteConfig } from "../../types";
|
||||||
type Params<Schema extends {
|
type Params<Schema extends {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
} = {
|
} = {
|
||||||
@@ -7,10 +7,11 @@ type Params<Schema extends {
|
|||||||
table: Table;
|
table: Table;
|
||||||
data: Schema[];
|
data: Schema[];
|
||||||
update_on_duplicate?: boolean;
|
update_on_duplicate?: boolean;
|
||||||
|
config?: BunSQLiteConfig;
|
||||||
};
|
};
|
||||||
export default function DbInsert<Schema extends {
|
export default function DbInsert<Schema extends {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
} = {
|
} = {
|
||||||
[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 {};
|
export {};
|
||||||
|
|||||||
Vendored
+12
-8
@@ -1,7 +1,7 @@
|
|||||||
import DbClient from ".";
|
import dbHandler from "../db-handler";
|
||||||
import sqlInsertGenerator from "../../utils/sql-insert-generator";
|
import sqlInsertGenerator from "../../utils/sql-insert-generator";
|
||||||
import grabDuplicateSafeInsertSql from "../grab-duplicate-safe-insert-sql";
|
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;
|
let sqlObj = null;
|
||||||
try {
|
try {
|
||||||
const finalData = data.map((d) => ({
|
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 });
|
sql = await grabDuplicateSafeInsertSql({ data, table, sql });
|
||||||
}
|
}
|
||||||
(sqlObj || {}).query = 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 {
|
return {
|
||||||
success: Boolean(Number(res.lastInsertRowid)),
|
success: Boolean(res.postInsertReturn?.insertId),
|
||||||
postInsertReturn: {
|
postInsertReturn: res.postInsertReturn,
|
||||||
affectedRows: res.changes,
|
|
||||||
insertId: Number(res.lastInsertRowid),
|
|
||||||
},
|
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
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) {
|
if (final_table.parentTableName) {
|
||||||
const parent_table = dbTablesSchemas.find((t) => t.tableName === final_table.parentTableName);
|
const parent_table = dbTablesSchemas.find((t) => t.tableName === final_table.parentTableName);
|
||||||
if (parent_table) {
|
if (parent_table) {
|
||||||
final_table = _.merge(parent_table, {
|
final_table = _.merge(_.cloneDeep(parent_table), {
|
||||||
tableName: final_table.tableName,
|
tableName: final_table.tableName,
|
||||||
tableDescription: final_table.tableDescription,
|
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 {
|
type Params<Schema extends {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
} = {
|
} = {
|
||||||
@@ -8,10 +8,11 @@ type Params<Schema extends {
|
|||||||
table: Table;
|
table: Table;
|
||||||
count?: boolean;
|
count?: boolean;
|
||||||
targetId?: number | string;
|
targetId?: number | string;
|
||||||
|
config?: BunSQLiteConfig;
|
||||||
};
|
};
|
||||||
export default function DbSelect<Schema extends {
|
export default function DbSelect<Schema extends {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
} = {
|
} = {
|
||||||
[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 {};
|
export {};
|
||||||
|
|||||||
Vendored
+32
-23
@@ -1,27 +1,24 @@
|
|||||||
import mysql from "mysql";
|
import dbHandler from "../db-handler";
|
||||||
import DbClient from ".";
|
|
||||||
import _ from "lodash";
|
|
||||||
import sqlGenerator from "../../utils/sql-generator";
|
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;
|
let sqlObj = null;
|
||||||
try {
|
try {
|
||||||
let finalQuery = query || {};
|
const finalQuery = query || {};
|
||||||
if (targetId) {
|
|
||||||
finalQuery = _.merge(finalQuery, {
|
|
||||||
query: {
|
|
||||||
id: {
|
|
||||||
value: String(targetId),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
sqlObj = sqlGenerator({
|
sqlObj = sqlGenerator({
|
||||||
tableName: table,
|
tableName: table,
|
||||||
genObject: finalQuery,
|
genObject: finalQuery,
|
||||||
|
targetId,
|
||||||
});
|
});
|
||||||
let sql = mysql.format(sqlObj.string, sqlObj.values);
|
const sql = sqlObj.string;
|
||||||
const res = DbClient.query(sql);
|
const res = dbHandler({
|
||||||
const batchRes = res.all();
|
query: sql,
|
||||||
|
values: sqlObj.values,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
if (!res.success) {
|
||||||
|
return { ...res, debug: { sqlObj, sql } };
|
||||||
|
}
|
||||||
|
const batchRes = res.payload || [];
|
||||||
let resp = {
|
let resp = {
|
||||||
success: Boolean(batchRes[0]),
|
success: Boolean(batchRes[0]),
|
||||||
payload: batchRes,
|
payload: batchRes,
|
||||||
@@ -32,16 +29,28 @@ export default async function DbSelect({ table, query, count, targetId, }) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (count) {
|
if (count) {
|
||||||
let count_sql_object = sqlGenerator({
|
const count_sql_object = sqlGenerator({
|
||||||
tableName: table,
|
tableName: table,
|
||||||
genObject: finalQuery,
|
genObject: finalQuery,
|
||||||
|
targetId,
|
||||||
count,
|
count,
|
||||||
});
|
});
|
||||||
let count_sql = mysql.format(count_sql_object.string, count_sql_object.values);
|
const count_sql = `SELECT COUNT(*) AS count FROM (${count_sql_object.string}) AS c`;
|
||||||
count_sql = `SELECT COUNT(*) FROM (${count_sql}) as c`;
|
const count_res = dbHandler({
|
||||||
const count_res = DbClient.query(count_sql).all();
|
query: count_sql,
|
||||||
const count_val = count_res[0]?.["COUNT(*)"];
|
values: count_sql_object.values,
|
||||||
resp["count"] = Number(count_val);
|
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;
|
resp["debug"]["count_sql"] = count_sql;
|
||||||
}
|
}
|
||||||
return resp;
|
return resp;
|
||||||
|
|||||||
Vendored
+3
-2
@@ -1,11 +1,12 @@
|
|||||||
import type { APIResponseObject, SQLInsertGenValueType } from "../../types";
|
import type { APIResponseObject, BunSQLiteConfig, SQLInsertGenValueType } from "../../types";
|
||||||
type Params = {
|
type Params = {
|
||||||
sql: string;
|
sql: string;
|
||||||
values?: SQLInsertGenValueType[];
|
values?: SQLInsertGenValueType[];
|
||||||
|
config?: BunSQLiteConfig;
|
||||||
};
|
};
|
||||||
export default function DbSQL<T extends {
|
export default function DbSQL<T extends {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
} = {
|
} = {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
}>({ sql, values }: Params): Promise<APIResponseObject<T>>;
|
}>({ sql, values, config }: Params): Promise<APIResponseObject<T>>;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Vendored
+16
-32
@@ -1,34 +1,18 @@
|
|||||||
import DbClient from ".";
|
import dbHandler from "../db-handler";
|
||||||
import _ from "lodash";
|
export default async function DbSQL({ sql, values, config }) {
|
||||||
export default async function DbSQL({ sql, values }) {
|
const trimmed_sql = sql.trim();
|
||||||
try {
|
const res = dbHandler({ query: trimmed_sql, values, config });
|
||||||
const trimmed_sql = sql.trim();
|
if (!res.success) {
|
||||||
const res = trimmed_sql.match(/^select/i)
|
return res;
|
||||||
? DbClient.query(trimmed_sql).all(...(values || []))
|
}
|
||||||
: DbClient.run(trimmed_sql, values || []);
|
return {
|
||||||
return {
|
...res,
|
||||||
success: true,
|
debug: {
|
||||||
payload: Array.isArray(res) ? res : undefined,
|
sqlObj: {
|
||||||
singleRes: Array.isArray(res) ? res?.[0] : undefined,
|
sql: trimmed_sql,
|
||||||
postInsertReturn: Array.isArray(res)
|
values,
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
affectedRows: res.changes,
|
|
||||||
insertId: Number(res.lastInsertRowid),
|
|
||||||
},
|
|
||||||
debug: {
|
|
||||||
sqlObj: {
|
|
||||||
sql: trimmed_sql,
|
|
||||||
values,
|
|
||||||
},
|
|
||||||
sql,
|
|
||||||
},
|
},
|
||||||
};
|
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 {
|
type Params<Schema extends {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
} = {
|
} = {
|
||||||
@@ -8,10 +8,11 @@ type Params<Schema extends {
|
|||||||
data: Schema;
|
data: Schema;
|
||||||
query?: ServerQueryParam<Schema>;
|
query?: ServerQueryParam<Schema>;
|
||||||
targetId?: number | string;
|
targetId?: number | string;
|
||||||
|
config?: BunSQLiteConfig;
|
||||||
};
|
};
|
||||||
export default function DbUpdate<Schema extends {
|
export default function DbUpdate<Schema extends {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
} = {
|
} = {
|
||||||
[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 {};
|
export {};
|
||||||
|
|||||||
Vendored
+22
-28
@@ -1,27 +1,24 @@
|
|||||||
import DbClient from ".";
|
import dbHandler from "../db-handler";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import sqlGenerator from "../../utils/sql-generator";
|
import sqlGenerator from "../../utils/sql-generator";
|
||||||
export default async function DbUpdate({ table, data, query, targetId, }) {
|
import { sqlValue } from "../../utils/sql-insert-generator";
|
||||||
let sqlObj = { string: "", values: [] };
|
import { sqlIdent } from "../../utils/sql-safe";
|
||||||
|
export default async function DbUpdate({ table, data, query, targetId, config, }) {
|
||||||
|
let sqlObj = {
|
||||||
|
string: "",
|
||||||
|
values: [],
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
let finalQuery = query || {};
|
const finalQuery = query || {};
|
||||||
if (targetId) {
|
|
||||||
finalQuery = _.merge(finalQuery, {
|
|
||||||
query: {
|
|
||||||
id: {
|
|
||||||
value: String(targetId),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const sqlQueryObj = sqlGenerator({
|
const sqlQueryObj = sqlGenerator({
|
||||||
tableName: table,
|
tableName: table,
|
||||||
genObject: finalQuery,
|
genObject: _.pick(finalQuery, ["query", "searchOperator"]),
|
||||||
|
targetId,
|
||||||
});
|
});
|
||||||
let values = [];
|
let values = [];
|
||||||
const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0];
|
const whereClause = sqlQueryObj.where.string;
|
||||||
if (whereClause) {
|
if (whereClause) {
|
||||||
let sql = `UPDATE ${table} SET`;
|
let sql = `UPDATE ${sqlIdent(table, "table name")} SET`;
|
||||||
const finalData = {
|
const finalData = {
|
||||||
updated_at: Date.now(),
|
updated_at: Date.now(),
|
||||||
...data,
|
...data,
|
||||||
@@ -32,26 +29,23 @@ export default async function DbUpdate({ table, data, query, targetId, }) {
|
|||||||
if (!key)
|
if (!key)
|
||||||
continue;
|
continue;
|
||||||
const isLast = i == keys.length - 1;
|
const isLast = i == keys.length - 1;
|
||||||
sql += ` ${key}=?`;
|
sql += ` ${sqlIdent(key, "column name")}=?`;
|
||||||
const value = finalData[key];
|
values.push(sqlValue(finalData[key]));
|
||||||
values.push(typeof value == "number" || typeof value == "string"
|
|
||||||
? value
|
|
||||||
: value || null);
|
|
||||||
if (!isLast) {
|
if (!isLast) {
|
||||||
sql += `,`;
|
sql += `,`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sql += ` ${whereClause}`;
|
sql += ` ${whereClause}`;
|
||||||
values = [...values, ...sqlQueryObj.values];
|
values = [...values, ...sqlQueryObj.where.values];
|
||||||
sqlObj.string = sql;
|
sqlObj.string = sql;
|
||||||
sqlObj.values = values;
|
sqlObj.values = values;
|
||||||
const res = DbClient.run(sql, values);
|
const res = dbHandler({ query: sql, values, config });
|
||||||
|
if (!res.success) {
|
||||||
|
return { ...res, debug: { sqlObj } };
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: Boolean(res.changes),
|
success: Boolean(res.postInsertReturn?.affectedRows),
|
||||||
postInsertReturn: {
|
postInsertReturn: res.postInsertReturn,
|
||||||
affectedRows: res.changes,
|
|
||||||
insertId: Number(res.lastInsertRowid),
|
|
||||||
},
|
|
||||||
debug: {
|
debug: {
|
||||||
sqlObj,
|
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;
|
db_name: string;
|
||||||
/**
|
/**
|
||||||
* The Name of the Database Schema File. Eg `db_schema.ts`. This is
|
* 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.
|
* The Directory for backups. Relative to db_dir.
|
||||||
*/
|
*/
|
||||||
db_backup_dir?: string;
|
db_backup_dir?: string;
|
||||||
max_backups?: number;
|
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
|
* The Root Directory for the DB file and schema
|
||||||
*/
|
*/
|
||||||
@@ -1382,6 +1387,35 @@ export type BunSQLiteConfigReturn = {
|
|||||||
config: BunSQLiteConfig;
|
config: BunSQLiteConfig;
|
||||||
dbSchema: BUN_SQLITE_DatabaseSchemaType;
|
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.
|
* 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): {
|
export default function grabDBDir({ config }: Params): {
|
||||||
db_dir: string;
|
db_dir: string;
|
||||||
backup_dir: string;
|
backup_dir: string;
|
||||||
|
export_dir: string;
|
||||||
db_file_path: string;
|
db_file_path: string;
|
||||||
};
|
};
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Vendored
+3
-2
@@ -5,10 +5,11 @@ export default function grabDBDir({ config }) {
|
|||||||
const { ROOT_DIR } = grabDirNames();
|
const { ROOT_DIR } = grabDirNames();
|
||||||
let db_dir = ROOT_DIR;
|
let db_dir = ROOT_DIR;
|
||||||
if (config.db_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_name = config.db_backup_dir || AppData["DefaultBackupDirName"];
|
||||||
const backup_dir = path.resolve(db_dir, backup_dir_name);
|
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);
|
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";
|
import init from "../functions/init";
|
||||||
export default async function grabDbSchema() {
|
export default async function grabDbSchema() {
|
||||||
const { dbSchema } = await init();
|
const { dbSchema } = init();
|
||||||
return dbSchema;
|
return dbSchema;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+12
-5
@@ -1,18 +1,25 @@
|
|||||||
import grabDBDir from "../utils/grab-db-dir";
|
import grabDBDir from "../utils/grab-db-dir";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import _ from "lodash";
|
||||||
export default function grabSortedBackups({ config }) {
|
export default function grabSortedBackups({ config }) {
|
||||||
const { backup_dir } = grabDBDir({ 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.
|
* Order Backups. Most recent first.
|
||||||
*/
|
*/
|
||||||
const ordered_backups = backups.sort((a, b) => {
|
const ordered_backups = backups.sort((a, b) => {
|
||||||
const a_date = Number(a.split("-").pop());
|
const a_date = Number(a.split("-").pop());
|
||||||
const b_date = Number(b.split("-").pop());
|
const b_date = Number(b.split("-").pop());
|
||||||
if (a_date > b_date) {
|
return b_date - a_date;
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
});
|
});
|
||||||
return ordered_backups;
|
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<{
|
queryObj: ServerQueryObject<{
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}, string>;
|
}, string>;
|
||||||
|
/**
|
||||||
|
* `value` is an already-validated `table.field` reference rather than a
|
||||||
|
* literal, so it is inlined instead of bound
|
||||||
|
*/
|
||||||
isValueFieldValue?: boolean;
|
isValueFieldValue?: boolean;
|
||||||
};
|
};
|
||||||
type Return = {
|
type Return = {
|
||||||
str?: string;
|
str: string;
|
||||||
param?: SQLInsertGenValueType;
|
params: SQLInsertGenValueType[];
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* # SQL Gen Operator Gen
|
* # 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 default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }: Params): Return;
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Vendored
+63
-103
@@ -1,133 +1,93 @@
|
|||||||
import sqlEqualityParser from "./sql-equality-parser";
|
import sqlEqualityParser from "./sql-equality-parser";
|
||||||
|
import { sqlIdent } from "./sql-safe";
|
||||||
/**
|
/**
|
||||||
* # SQL Gen Operator Gen
|
* # 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, }) {
|
export default function sqlGenOperatorGen({ fieldName, value, equality, queryObj, isValueFieldValue, }) {
|
||||||
if (queryObj.nullValue) {
|
if (queryObj.nullValue || equality == "IS NULL") {
|
||||||
return { str: `${fieldName} IS NULL` };
|
return { str: `${fieldName} IS NULL`, params: [] };
|
||||||
}
|
}
|
||||||
if (queryObj.notNullValue) {
|
if (queryObj.notNullValue || equality == "IS NOT NULL") {
|
||||||
return { str: `${fieldName} IS NOT NULL` };
|
return { str: `${fieldName} IS NOT NULL`, params: [] };
|
||||||
}
|
}
|
||||||
if (value) {
|
const finalValue = isValueFieldValue ? String(value) : "?";
|
||||||
const finalValue = isValueFieldValue ? value : "?";
|
const params = isValueFieldValue
|
||||||
const finalParams = isValueFieldValue ? undefined : value;
|
? []
|
||||||
if (equality == "MATCH") {
|
: [value ?? ""];
|
||||||
return {
|
const wrappedValue = isValueFieldValue ? `'%' || ${value} || '%'` : "?";
|
||||||
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN NATURAL LANGUAGE MODE)`,
|
const wrappedParams = isValueFieldValue
|
||||||
param: finalParams,
|
? []
|
||||||
};
|
: [`%${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 };
|
||||||
}
|
}
|
||||||
else if (equality == "MATCH_BOOLEAN") {
|
case "LIKE_LOWER":
|
||||||
return {
|
return {
|
||||||
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`,
|
str: `LOWER(${fieldName}) LIKE LOWER(${wrappedValue})`,
|
||||||
param: finalParams,
|
params: wrappedParams,
|
||||||
};
|
};
|
||||||
}
|
case "LIKE_LOWER_RAW":
|
||||||
else if (equality == "LIKE_LOWER") {
|
|
||||||
return {
|
return {
|
||||||
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
|
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
|
||||||
param: `%${finalParams}%`,
|
params,
|
||||||
};
|
};
|
||||||
}
|
case "LIKE":
|
||||||
else if (equality == "LIKE_LOWER_RAW") {
|
|
||||||
return {
|
return {
|
||||||
str: `LOWER(${fieldName}) LIKE LOWER(${finalValue})`,
|
str: `${fieldName} LIKE ${wrappedValue}`,
|
||||||
param: finalParams,
|
params: wrappedParams,
|
||||||
};
|
};
|
||||||
}
|
case "LIKE_RAW":
|
||||||
else if (equality == "LIKE") {
|
return { str: `${fieldName} LIKE ${finalValue}`, params };
|
||||||
|
case "NOT_LIKE_LOWER":
|
||||||
return {
|
return {
|
||||||
str: `${fieldName} LIKE ${finalValue}`,
|
str: `LOWER(${fieldName}) NOT LIKE LOWER(${wrappedValue})`,
|
||||||
param: `%${finalParams}%`,
|
params: wrappedParams,
|
||||||
};
|
};
|
||||||
}
|
case "NOT_LIKE_LOWER_RAW":
|
||||||
else if (equality == "LIKE_RAW") {
|
|
||||||
return {
|
|
||||||
str: `${fieldName} LIKE ${finalValue}`,
|
|
||||||
param: finalParams,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (equality == "NOT_LIKE_LOWER") {
|
|
||||||
return {
|
return {
|
||||||
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
|
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
|
||||||
param: `%${finalParams}%`,
|
params,
|
||||||
};
|
};
|
||||||
}
|
case "NOT LIKE":
|
||||||
else if (equality == "NOT_LIKE_LOWER_RAW") {
|
|
||||||
return {
|
return {
|
||||||
str: `LOWER(${fieldName}) NOT LIKE LOWER(${finalValue})`,
|
str: `${fieldName} NOT LIKE ${wrappedValue}`,
|
||||||
param: finalParams,
|
params: wrappedParams,
|
||||||
};
|
};
|
||||||
}
|
case "NOT LIKE_RAW":
|
||||||
else if (equality == "NOT LIKE") {
|
return { str: `${fieldName} NOT LIKE ${finalValue}`, params };
|
||||||
return {
|
case "REGEXP":
|
||||||
str: `${fieldName} NOT LIKE ${finalValue}`,
|
/**
|
||||||
param: finalParams,
|
* SQLite has no built-in REGEXP function. This only works when
|
||||||
};
|
* an extension providing `regexp()` is loaded.
|
||||||
}
|
*/
|
||||||
else if (equality == "NOT LIKE_RAW") {
|
|
||||||
return {
|
|
||||||
str: `${fieldName} NOT LIKE ${finalValue}`,
|
|
||||||
param: finalParams,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (equality == "REGEXP") {
|
|
||||||
return {
|
return {
|
||||||
str: `LOWER(${fieldName}) REGEXP LOWER(${finalValue})`,
|
str: `LOWER(${fieldName}) REGEXP LOWER(${finalValue})`,
|
||||||
param: finalParams,
|
params,
|
||||||
};
|
};
|
||||||
}
|
case "NOT EQUAL":
|
||||||
else if (equality == "FULLTEXT") {
|
return { str: `${fieldName} != ${finalValue}`, params };
|
||||||
return {
|
case "IS NOT":
|
||||||
str: `MATCH(${fieldName}) AGAINST(${finalValue} IN BOOLEAN MODE)`,
|
return { str: `${fieldName} IS NOT ${finalValue}`, params };
|
||||||
param: finalParams,
|
case "IN":
|
||||||
};
|
case "NOT IN":
|
||||||
}
|
return { str: `${fieldName} ${equality} (${finalValue})`, params };
|
||||||
else if (equality == "NOT EQUAL") {
|
case "BETWEEN":
|
||||||
return {
|
case "NOT BETWEEN":
|
||||||
str: `${fieldName} != ${finalValue}`,
|
throw new Error(`\`${equality}\` on \`${fieldName}\` requires an array of two values`);
|
||||||
param: finalParams,
|
case undefined:
|
||||||
};
|
return { str: `${fieldName} = ${finalValue}`, params };
|
||||||
}
|
default:
|
||||||
else if (equality == "IS NOT") {
|
|
||||||
return {
|
|
||||||
str: `${fieldName} IS NOT ${finalValue}`,
|
|
||||||
param: finalParams,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (equality) {
|
|
||||||
return {
|
return {
|
||||||
str: `${fieldName} ${sqlEqualityParser(equality)} ${finalValue}`,
|
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 }) {
|
export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
|
||||||
let values = [];
|
let values = [];
|
||||||
if (mtch.__batch) {
|
if (mtch.__batch) {
|
||||||
@@ -13,7 +14,7 @@ export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
|
|||||||
btch_mtch += str;
|
btch_mtch += str;
|
||||||
values.push(...batch_values);
|
values.push(...batch_values);
|
||||||
if (i < mtch.__batch.matches.length - 1) {
|
if (i < mtch.__batch.matches.length - 1) {
|
||||||
btch_mtch += ` ${mtch.__batch.operator || "OR"} `;
|
btch_mtch += ` ${sqlOperator(mtch.__batch.operator, "OR")} `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btch_mtch += `)`;
|
btch_mtch += `)`;
|
||||||
@@ -22,28 +23,8 @@ export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
|
|||||||
values,
|
values,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const equality = mtch.raw_equality || "=";
|
const equality = mtch.raw_equality ? sqlRawEquality(mtch.raw_equality) : "=";
|
||||||
const lhs = `${typeof mtch.source == "object" ? mtch.source.tableName : table_name}.${typeof mtch.source == "object" ? mtch.source.fieldName : mtch.source}`;
|
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")}`;
|
||||||
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}`;
|
|
||||||
})()}`;
|
|
||||||
if (mtch.between) {
|
if (mtch.between) {
|
||||||
values.push(mtch.between.min, mtch.between.max);
|
values.push(mtch.between.min, mtch.between.max);
|
||||||
return {
|
return {
|
||||||
@@ -58,6 +39,19 @@ export default function sqlGenGenJoinStr({ join, mtch, table_name }) {
|
|||||||
values,
|
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 {
|
return {
|
||||||
str: `${lhs} ${equality} ${rhs}`,
|
str: `${lhs} ${equality} ${rhs}`,
|
||||||
values,
|
values,
|
||||||
|
|||||||
-2
@@ -8,8 +8,6 @@ type Param<T extends {
|
|||||||
selectFields?: (keyof T | TableSelectFieldsObject<T>)[];
|
selectFields?: (keyof T | TableSelectFieldsObject<T>)[];
|
||||||
append_table_names?: boolean;
|
append_table_names?: boolean;
|
||||||
table_name: string;
|
table_name: string;
|
||||||
full_text_match_str?: string;
|
|
||||||
full_text_search_str?: string;
|
|
||||||
};
|
};
|
||||||
export default function sqlGenGenQueryStr<T extends {
|
export default function sqlGenGenQueryStr<T extends {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|||||||
+73
-81
@@ -2,12 +2,11 @@ import { isUndefined } from "lodash";
|
|||||||
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
|
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
|
||||||
import sqlGenGenJoinStr from "./sql-generator-gen-join-str";
|
import sqlGenGenJoinStr from "./sql-generator-gen-join-str";
|
||||||
import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql";
|
import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql";
|
||||||
|
import { sqlIdent, sqlJoinType, sqlOperator } from "./sql-safe";
|
||||||
export default function sqlGenGenQueryStr(params) {
|
export default function sqlGenGenQueryStr(params) {
|
||||||
let str = "SELECT";
|
let str = "SELECT";
|
||||||
const genObject = params.genObject;
|
const genObject = params.genObject;
|
||||||
const table_name = params.table_name;
|
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 = [];
|
let sqlSearhValues = [];
|
||||||
if (genObject?.select_sql) {
|
if (genObject?.select_sql) {
|
||||||
str += ` ${genObject.select_sql}`;
|
str += ` ${genObject.select_sql}`;
|
||||||
@@ -41,108 +40,102 @@ export default function sqlGenGenQueryStr(params) {
|
|||||||
const countSubQuery = genObject.countSubQueries[i];
|
const countSubQuery = genObject.countSubQueries[i];
|
||||||
if (!countSubQuery)
|
if (!countSubQuery)
|
||||||
continue;
|
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(*)`;
|
let subQStr = `(SELECT COUNT(*)`;
|
||||||
subQStr += ` FROM ${countSubQuery.table}${tableAlias ? ` ${tableAlias}` : ""}`;
|
subQStr += ` FROM ${countTable}${tableAlias ? ` ${tableAlias}` : ""}`;
|
||||||
subQStr += ` WHERE (`;
|
subQStr += ` WHERE (`;
|
||||||
for (let j = 0; j < countSubQuery.srcTrgMap.length; j++) {
|
for (let j = 0; j < countSubQuery.srcTrgMap.length; j++) {
|
||||||
const csqSrc = countSubQuery.srcTrgMap[j];
|
const csqSrc = countSubQuery.srcTrgMap[j];
|
||||||
if (!csqSrc)
|
if (!csqSrc)
|
||||||
continue;
|
continue;
|
||||||
subQStr += ` ${tableAlias || countSubQuery.table}.${csqSrc.src}`;
|
subQStr += ` ${tableAlias || countTable}.${sqlIdent(csqSrc.src, "field name")}`;
|
||||||
if (typeof csqSrc.trg == "string") {
|
if (typeof csqSrc.trg == "string") {
|
||||||
subQStr += ` = ?`;
|
subQStr += ` = ?`;
|
||||||
sqlSearhValues.push(csqSrc.trg);
|
sqlSearhValues.push(csqSrc.trg);
|
||||||
}
|
}
|
||||||
else if (typeof csqSrc.trg == "object") {
|
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) {
|
if (j < countSubQuery.srcTrgMap.length - 1) {
|
||||||
subQStr += ` AND `;
|
subQStr += ` AND `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
subQStr += ` )) AS ${countSubQuery.alias}`;
|
subQStr += ` )) AS ${sqlIdent(countSubQuery.alias, "alias")}`;
|
||||||
countSqls.push(subQStr);
|
countSqls.push(subQStr);
|
||||||
}
|
}
|
||||||
str += `, ${countSqls.join(",")}`;
|
str += `, ${countSqls.join(",")}`;
|
||||||
}
|
}
|
||||||
if (genObject?.join) {
|
if (genObject?.join) {
|
||||||
const existingJoinTableNames = [table_name];
|
const existingJoinTableNames = [table_name];
|
||||||
str +=
|
const joinSelectStr = genObject.join
|
||||||
"," +
|
.flat()
|
||||||
genObject.join
|
.filter((j) => !isUndefined(j))
|
||||||
.flat()
|
.map((joinObj) => {
|
||||||
.filter((j) => !isUndefined(j))
|
const joinTableName = sqlIdent(joinObj.alias ? joinObj.alias : joinObj.tableName, "join table name");
|
||||||
.map((joinObj) => {
|
if (existingJoinTableNames.includes(joinTableName))
|
||||||
const joinTableName = joinObj.alias
|
return null;
|
||||||
? joinObj.alias
|
existingJoinTableNames.push(joinTableName);
|
||||||
: joinObj.tableName;
|
if (joinObj.group_concat) {
|
||||||
if (existingJoinTableNames.includes(joinTableName))
|
return sqlGenGrabConcatStr({
|
||||||
return null;
|
field: `${joinTableName}.${sqlIdent(joinObj.group_concat.field, "field name")}`,
|
||||||
existingJoinTableNames.push(joinTableName);
|
alias: joinObj.group_concat.alias,
|
||||||
if (joinObj.group_concat) {
|
separator: joinObj.group_concat.separator,
|
||||||
|
distinct: joinObj.group_concat.distinct,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (joinObj.selectFields) {
|
||||||
|
return joinObj.selectFields
|
||||||
|
.map((selectField) => {
|
||||||
|
if (typeof selectField != "object") {
|
||||||
|
return `${joinTableName}.${sqlIdent(selectField, "field name")}`;
|
||||||
|
}
|
||||||
|
const field = `${joinTableName}.${sqlIdent(selectField.field, "field name")}`;
|
||||||
|
if (selectField.group_concat &&
|
||||||
|
selectField.alias) {
|
||||||
return sqlGenGrabConcatStr({
|
return sqlGenGrabConcatStr({
|
||||||
field: `${joinTableName}.${joinObj.group_concat.field}`,
|
field,
|
||||||
alias: joinObj.group_concat.alias,
|
alias: selectField.alias,
|
||||||
separator: joinObj.group_concat.separator,
|
separator: selectField.group_concat.separator,
|
||||||
|
distinct: selectField.group_concat.distinct,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (joinObj.selectFields) {
|
let aliasSelectField = field;
|
||||||
return joinObj.selectFields
|
if (selectField.count) {
|
||||||
.map((selectField) => {
|
aliasSelectField = `COUNT(${field})`;
|
||||||
if (typeof selectField == "string") {
|
|
||||||
return `${joinTableName}.${selectField}`;
|
|
||||||
}
|
|
||||||
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 &&
|
|
||||||
selectField.alias) {
|
|
||||||
return sqlGenGrabConcatStr({
|
|
||||||
field: `${joinTableName}.${selectField.field}`,
|
|
||||||
alias: selectField.alias,
|
|
||||||
separator: selectField.group_concat
|
|
||||||
.separator,
|
|
||||||
distinct: selectField.group_concat
|
|
||||||
.distinct,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (selectField.distinct) {
|
|
||||||
aliasSelectField = `DISTINCT ${joinTableName}.${selectField.field}`;
|
|
||||||
}
|
|
||||||
if (selectField.alias)
|
|
||||||
aliasSelectField += ` AS ${selectField.alias}`;
|
|
||||||
return aliasSelectField;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.join(",");
|
|
||||||
}
|
}
|
||||||
else {
|
else if (selectField.sum) {
|
||||||
return `${joinTableName}.*`;
|
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 ${field}`;
|
||||||
|
}
|
||||||
|
if (selectField.alias)
|
||||||
|
aliasSelectField += ` AS ${sqlIdent(selectField.alias, "alias")}`;
|
||||||
|
return aliasSelectField;
|
||||||
})
|
})
|
||||||
.filter((_) => Boolean(_))
|
|
||||||
.join(",");
|
.join(",");
|
||||||
}
|
}
|
||||||
if (genObject?.fullTextSearch &&
|
else {
|
||||||
full_text_match_str &&
|
return `${joinTableName}.*`;
|
||||||
full_text_search_str) {
|
}
|
||||||
str += `, ${full_text_match_str} AS ${genObject.fullTextSearch.scoreAlias}`;
|
})
|
||||||
sqlSearhValues.push(full_text_search_str);
|
.filter((_) => Boolean(_))
|
||||||
|
.join(",");
|
||||||
|
if (joinSelectStr) {
|
||||||
|
str += "," + joinSelectStr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
str += ` FROM ${table_name}`;
|
str += ` FROM ${table_name}`;
|
||||||
if (genObject?.join) {
|
if (genObject?.join) {
|
||||||
@@ -152,11 +145,12 @@ export default function sqlGenGenQueryStr(params) {
|
|||||||
.flat()
|
.flat()
|
||||||
.filter((j) => !isUndefined(j))
|
.filter((j) => !isUndefined(j))
|
||||||
.map((join) => {
|
.map((join) => {
|
||||||
return (join.joinType +
|
const joinTable = sqlIdent(join.tableName, "table name");
|
||||||
|
return (sqlJoinType(join.joinType) +
|
||||||
" " +
|
" " +
|
||||||
(join.alias
|
(join.alias
|
||||||
? `${join.tableName}` + " " + join.alias
|
? `${joinTable} ${sqlIdent(join.alias, "alias")}`
|
||||||
: `${join.tableName}`) +
|
: joinTable) +
|
||||||
" ON " +
|
" ON " +
|
||||||
(() => {
|
(() => {
|
||||||
if (Array.isArray(join.match)) {
|
if (Array.isArray(join.match)) {
|
||||||
@@ -171,9 +165,7 @@ export default function sqlGenGenQueryStr(params) {
|
|||||||
sqlSearhValues.push(...values);
|
sqlSearhValues.push(...values);
|
||||||
return str;
|
return str;
|
||||||
})
|
})
|
||||||
.join(join.operator
|
.join(` ${sqlOperator(join.operator)} `) +
|
||||||
? ` ${join.operator} `
|
|
||||||
: " AND ") +
|
|
||||||
")");
|
")");
|
||||||
}
|
}
|
||||||
else if (typeof join.match == "object") {
|
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