Add tests, schema sync safeguards, and query/handler improvements
- Add bun test suite: SQL generator and value unit tests, plus MariaDB integration tests gated on BUN_MARIADB_TEST_DB - schema: add --dry-run and require confirmation (or --yes) before dropping tables/columns; block unapproved destructive statements - Always AND targetId with the query, regardless of searchOperator - Initialize lazily on first call; init throws instead of exiting - Generate IN (?, ?, ...) lists for IN / NOT IN - Share value binding between insert and update (booleans, embeddings, function values, "" -> NULL for non-text columns) - Keep Buffers and convert BigInt/Date without a JSON round-trip - DbSQL: report success for empty SELECT results - Run update() in a transaction with row locking - Key the client cache on the full connection config - Consolidate identifier quoting, drop dead code, merge column lookup - Read CLI version from package.json, pin @types/bun, chmod CLI on compile - Rebuild dist Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Vendored
+22
-1
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import chalk from "chalk";
|
||||
import { select } from "@inquirer/prompts";
|
||||
import { confirm, select } from "@inquirer/prompts";
|
||||
import grabDBDir from "../utils/grab-db-dir";
|
||||
import grabSortedExports from "../utils/grab-sorted-exports";
|
||||
import grabBackupData from "../utils/grab-backup-data";
|
||||
@@ -23,6 +23,7 @@ export default function () {
|
||||
.argument("[file]", "Path to .sql file or export archive (.tar.gz / .tar / .zip)")
|
||||
.option("--sql-only", "When importing an archive, restore SQL only (skip writing schema)")
|
||||
.option("--schema-only", "When importing an archive, write schema only (skip SQL 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;
|
||||
@@ -67,6 +68,26 @@ export default function () {
|
||||
process.exit(1);
|
||||
}
|
||||
const { sql, 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.`));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(chalk.yellow(`\nThis archive contains an executable schema file (${schemaFileName}).\nIt will run as code the next time bun-mariadb 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.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (!opts.schemaOnly) {
|
||||
await restoreDatabase(config, sql);
|
||||
console.log(chalk.green(`SQL restored from archive`));
|
||||
|
||||
Vendored
+11
-2
@@ -8,14 +8,23 @@ import exportCmd from "./export";
|
||||
import importCmd from "./import";
|
||||
import admin from "./admin";
|
||||
import init from "../functions/init";
|
||||
init();
|
||||
import { readFileSync } from "fs";
|
||||
try {
|
||||
init();
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Initialization ERROR => ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
/** Resolves to the package root from both src/commands and dist/commands */
|
||||
const { version } = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
|
||||
/**
|
||||
* # Describe Program
|
||||
*/
|
||||
program
|
||||
.name(`bun-mariadb`)
|
||||
.description(`MariaDB manager for Bun`)
|
||||
.version(`1.0.0`);
|
||||
.version(version);
|
||||
/**
|
||||
* # Declare Commands
|
||||
*/
|
||||
|
||||
Vendored
+60
-4
@@ -6,10 +6,28 @@ import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-
|
||||
import chalk from "chalk";
|
||||
import { writeLiveSchema } from "../functions/live-schema";
|
||||
import createDBSchema from "../lib/schema/create-db-schema";
|
||||
import { isDestructiveSchemaStatement, planSchemaQueries, runSchemaQueriesGuarded, } from "../lib/schema/run-schema-query";
|
||||
import { confirm } from "@inquirer/prompts";
|
||||
import { AppData } from "../data/app-data";
|
||||
/** Silence progress logs while computing a plan */
|
||||
async function quietly(fn) {
|
||||
const { log, warn } = console;
|
||||
console.log = () => { };
|
||||
console.warn = () => { };
|
||||
try {
|
||||
return await fn();
|
||||
}
|
||||
finally {
|
||||
console.log = log;
|
||||
console.warn = warn;
|
||||
}
|
||||
}
|
||||
export default function () {
|
||||
return new Command("schema")
|
||||
.description("Build DB From Schema")
|
||||
.option("-t, --typedef", "Generate typescript type definitions")
|
||||
.option("--dry-run", "Print the schema changes that would be applied, without applying them")
|
||||
.option("-y, --yes", "Apply destructive changes (dropped tables / columns) without confirmation")
|
||||
.action(async (opts) => {
|
||||
console.log(`Starting process ...`);
|
||||
const config = global.CONFIG;
|
||||
@@ -20,10 +38,48 @@ export default function () {
|
||||
const finaldbSchema = appendDefaultFieldsToDbSchema({
|
||||
dbSchema,
|
||||
});
|
||||
await createDBSchema({
|
||||
db_schema: finaldbSchema,
|
||||
config,
|
||||
});
|
||||
const params = { db_schema: finaldbSchema, config };
|
||||
// Hide internal bookkeeping (schema manager rows, FK check toggles)
|
||||
const plan = (await quietly(() => planSchemaQueries(() => createDBSchema(params)))).filter((statement) => !statement.includes(AppData.DbSchemaManagerTableName) &&
|
||||
!/^SET FOREIGN_KEY_CHECKS/i.test(statement));
|
||||
const destructive = plan.filter(isDestructiveSchemaStatement);
|
||||
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 statement of plan) {
|
||||
console.log(isDestructiveSchemaStatement(statement)
|
||||
? chalk.red(` ${statement}`)
|
||||
: ` ${statement}`);
|
||||
}
|
||||
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 statement of destructive) {
|
||||
console.log(chalk.red(` ${statement}`));
|
||||
}
|
||||
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,
|
||||
});
|
||||
if (!proceed) {
|
||||
console.log(chalk.dim(`Schema sync cancelled.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
await runSchemaQueriesGuarded(() => createDBSchema(params), opts.yes ? true : new Set(destructive));
|
||||
if (isTypeDef && config.typedef_file_path) {
|
||||
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
|
||||
dbSchemaToTypeDef({
|
||||
|
||||
Reference in New Issue
Block a user