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:
2026-10-14 02:27:59 +01:00
co-authored by Claude Opus 5.5
parent b81b09324a
commit ddf431e866
82 changed files with 2140 additions and 1403 deletions
+5
View File
@@ -2,5 +2,10 @@ import { type BunMariaDBConfig } from "../types";
type Params = {
config: BunMariaDBConfig;
};
/**
* Cache key covering everything that affects the connection, not just the
* database name.
*/
export declare function mariadbClientCacheKey(config?: BunMariaDBConfig): string;
export default function grabMariaDBClient({ config, }: Params): Bun.SQL | undefined;
export {};
+22 -7
View File
@@ -2,29 +2,44 @@ import path from "path";
import grabDirNames from "../data/grab-dir-names";
import {} from "../types";
import { SQL } from "bun";
/** Reuse one client per database name to avoid exhausting MariaDB connections. */
/** Reuse one client per distinct connection config to avoid exhausting MariaDB connections. */
const clientCache = new Map();
/**
* Cache key covering everything that affects the connection, not just the
* database name.
*/
export function mariadbClientCacheKey(config) {
return JSON.stringify({
db_name: config?.db_name || "__default__",
db_config: config?.db_config || null,
ssl_ca: config?.ssl_ca || null,
ssl_reject_unauthorized: config?.ssl_reject_unauthorized ?? null,
});
}
export default function grabMariaDBClient({ config, }) {
const cacheKey = config.db_name || "__default__";
const cacheKey = mariadbClientCacheKey(config);
const cached = clientCache.get(cacheKey);
if (cached) {
return cached;
}
// Prefer the process-wide client when it matches the requested database
// Prefer the process-wide client when it was created from the same config
if (global.MARIADB_CLIENT &&
(!global.CONFIG?.db_name || global.CONFIG.db_name === config.db_name)) {
global.CONFIG &&
mariadbClientCacheKey(global.CONFIG) === cacheKey) {
clientCache.set(cacheKey, global.MARIADB_CLIENT);
return global.MARIADB_CLIENT;
}
const { ROOT_DIR } = grabDirNames();
try {
// Verify the server certificate unless explicitly disabled
const rejectUnauthorized = config.ssl_reject_unauthorized !== false;
const tls = config.ssl_ca
? {
ca: Bun.file(path.resolve(ROOT_DIR, config.ssl_ca)),
rejectUnauthorized: false,
rejectUnauthorized,
}
: {
rejectUnauthorized: false,
rejectUnauthorized,
};
const MariaDBClient = new SQL({
hostname: process.env.BUN_MARIADB_SERVER_HOST,
@@ -34,8 +49,8 @@ export default function grabMariaDBClient({ config, }) {
port: process.env.BUN_MARIADB_SERVER_PORT
? Number(process.env.BUN_MARIADB_SERVER_PORT)
: undefined,
...config.db_config,
tls,
...config.db_config,
adapter: "mariadb",
});
clientCache.set(cacheKey, MariaDBClient);
+8
View File
@@ -9,4 +9,12 @@ declare global {
var CONFIG_FILE_PATH: string;
var SCHEMA_FILE_PATH: string;
}
/**
* Load config + schema and set up the shared MariaDB client.
* Throws on any problem (never exits the process).
*/
export default function init(): void;
/**
* Initialize once, on first use.
*/
export declare function ensureInit(): void;
+62 -74
View File
@@ -5,81 +5,69 @@ import grabDirNames from "../data/grab-dir-names";
import { RequiredENVs, } from "../types";
import setMariaDBClient from "./set-mariadb-client";
import { resolveAndLoadDataFile, supportedDataFileNames, } from "../utils/resolve-and-load-data-file";
/**
* Load config + schema and set up the shared MariaDB client.
* Throws on any problem (never exits the process).
*/
export default function init() {
try {
const { ROOT_DIR } = grabDirNames();
const { ConfigFileName } = AppData;
const loadedConfig = resolveAndLoadDataFile(ROOT_DIR, ConfigFileName);
if (!loadedConfig) {
console.error(`Please create a ${supportedDataFileNames(ConfigFileName)} file at the root of your project.`);
process.exit(1);
}
const Config = loadedConfig.data;
if (!Config || typeof Config !== "object") {
console.error(`Invalid config in \`${loadedConfig.path}\`. Expected a config object${loadedConfig.format === "ts" || loadedConfig.format === "js" ? " (export default)" : ""}.`);
process.exit(1);
}
if (!Config.db_name) {
console.error(`\`db_name\` is required in your config`);
process.exit(1);
}
RequiredENVs.forEach((env_key) => {
if (env_key == "BUN_MARIADB_SERVER_PORT" ||
env_key == "BUN_MARIADB_SERVER_SSL_KEY_PATH") {
return;
}
if (!process.env[env_key]) {
console.error(`\`${env_key}\` env variable is required.`);
process.exit(1);
}
});
if (!Config.db_dir) {
console.error(`\`db_dir\` is required in your config. This directory holds all database related configuration. Also note that a ${supportedDataFileNames(AppData.DbSchemaFileName)} file is also required in this directory to define your database schema`);
process.exit(1);
}
const db_dir = path.resolve(ROOT_DIR, Config.db_dir);
if (!fs.existsSync(db_dir)) {
fs.mkdirSync(db_dir, { recursive: true });
}
const loadedSchema = resolveAndLoadDataFile(db_dir, AppData.DbSchemaFileName);
if (!loadedSchema) {
console.error(`Please create a schema file (${supportedDataFileNames(AppData.DbSchemaFileName)}) in \`${db_dir}\`${loadedConfig.format === "ts" || loadedConfig.format === "js" ? ". Don't forget to export a default module from .ts/.js files." : "."}`);
process.exit(1);
}
const DbSchema = loadedSchema.data;
if (!DbSchema || typeof DbSchema !== "object") {
console.error(`Invalid schema in \`${loadedSchema.path}\`. Expected a schema object${loadedSchema.format === "ts" || loadedSchema.format === "js" ? " (export default)" : ""}.`);
process.exit(1);
}
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 });
}
const ExportDir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
if (!fs.existsSync(ExportDir)) {
fs.mkdirSync(ExportDir, { recursive: true });
}
global.CONFIG = Config;
global.DB_SCHEMA = DbSchema;
global.CONFIG_FILE_PATH = loadedConfig.path;
global.SCHEMA_FILE_PATH = loadedSchema.path;
if (!global.CONFIG) {
console.error(`Couldn't grab global Config.`);
process.exit(1);
}
if (!global.DB_SCHEMA) {
console.error(`Couldn't grab Database Schema.`);
process.exit(1);
}
setMariaDBClient({ config: Config });
if (!global.MARIADB_CLIENT) {
console.error(`Couldn't set MariaDB Client.`);
process.exit(1);
}
const { ROOT_DIR } = grabDirNames();
const { ConfigFileName } = AppData;
const loadedConfig = resolveAndLoadDataFile(ROOT_DIR, ConfigFileName);
if (!loadedConfig) {
throw new Error(`Please create a ${supportedDataFileNames(ConfigFileName)} file at the root of your project.`);
}
catch (error) {
console.error(`Initialization ERROR => ` + error.message);
process.exit(1);
const Config = loadedConfig.data;
if (!Config || typeof Config !== "object") {
throw new Error(`Invalid config in \`${loadedConfig.path}\`. Expected a config object${loadedConfig.format === "ts" || loadedConfig.format === "js" ? " (export default)" : ""}.`);
}
if (!Config.db_name) {
throw new Error(`\`db_name\` is required in your config`);
}
RequiredENVs.forEach((env_key) => {
if (env_key == "BUN_MARIADB_SERVER_PORT" ||
env_key == "BUN_MARIADB_SERVER_SSL_KEY_PATH") {
return;
}
if (!process.env[env_key]) {
throw new Error(`\`${env_key}\` env variable is required.`);
}
});
if (!Config.db_dir) {
throw new Error(`\`db_dir\` is required in your config. This directory holds all database related configuration. Also note that a ${supportedDataFileNames(AppData.DbSchemaFileName)} file is also required in this directory to define your database schema`);
}
const db_dir = path.resolve(ROOT_DIR, Config.db_dir);
if (!fs.existsSync(db_dir)) {
fs.mkdirSync(db_dir, { recursive: true });
}
const loadedSchema = resolveAndLoadDataFile(db_dir, AppData.DbSchemaFileName);
if (!loadedSchema) {
throw new Error(`Please create a schema file (${supportedDataFileNames(AppData.DbSchemaFileName)}) in \`${db_dir}\`${loadedConfig.format === "ts" || loadedConfig.format === "js" ? ". Don't forget to export a default module from .ts/.js files." : "."}`);
}
const DbSchema = loadedSchema.data;
if (!DbSchema || typeof DbSchema !== "object") {
throw new Error(`Invalid schema in \`${loadedSchema.path}\`. Expected a schema object${loadedSchema.format === "ts" || loadedSchema.format === "js" ? " (export 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 });
}
const ExportDir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
if (!fs.existsSync(ExportDir)) {
fs.mkdirSync(ExportDir, { recursive: true });
}
global.CONFIG = Config;
global.DB_SCHEMA = DbSchema;
global.CONFIG_FILE_PATH = loadedConfig.path;
global.SCHEMA_FILE_PATH = loadedSchema.path;
setMariaDBClient({ config: Config });
}
/**
* Initialize once, on first use.
*/
export function ensureInit() {
if (global.CONFIG && global.DB_SCHEMA && global.MARIADB_CLIENT !== undefined) {
return;
}
init();
}
+4 -16
View File
@@ -1,21 +1,9 @@
import {} from "../types";
import grabMariaDBClient from "./grab-mariadb-client";
export default function setMariaDBClient({ config }) {
try {
const MariaDBClient = grabMariaDBClient({ config });
if (!MariaDBClient) {
throw new Error(`Couldn't grab MariaDB Client`);
}
global.MARIADB_CLIENT = MariaDBClient;
// const test = await MariaDBClient.unsafe(`SHOW DATABASES`);
// if (!test.count) {
// console.error(`MariaDBClient Error: Database not ready.`);
// console.log(test);
// process.exit(1);
// }
}
catch (error) {
console.error(`ERROR setting MariaDB Client => ` + error.message);
process.exit(1);
const MariaDBClient = grabMariaDBClient({ config });
if (!MariaDBClient) {
throw new Error(`Couldn't grab MariaDB Client`);
}
global.MARIADB_CLIENT = MariaDBClient;
}