Update .gitignore, add dist directory

This commit is contained in:
Benjamin Toby 2026-07-20 21:43:05 +01:00
parent 9f2db66760
commit 3bbf00cdb0
153 changed files with 6280 additions and 1 deletions

1
.gitignore vendored
View File

@ -3,7 +3,6 @@ node_modules
# output
out
dist
*.tgz
# code coverage

2
dist/commands/admin/index.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

37
dist/commands/admin/index.js vendored Normal file
View File

@ -0,0 +1,37 @@
import { Command } from "commander";
import chalk from "chalk";
import { select } from "@inquirer/prompts";
import listTables from "./list-tables";
import runSQL from "./run-sql";
export default function () {
return new Command("admin")
.description("View Tables and Data, Run SQL Queries, Etc.")
.action(async () => {
console.log(chalk.bold(chalk.blue("\nBun MariaDB Admin\n")));
try {
while (true) {
const paradigm = await select({
message: "Choose an action:",
choices: [
{ name: "Tables", value: "list_tables" },
{ name: "SQL", value: "run_sql" },
{ name: chalk.dim("✕ Exit"), value: "exit" },
],
});
if (paradigm === "exit")
break;
if (paradigm === "list_tables") {
const result = await listTables();
if (result === "__exit__")
break;
}
if (paradigm === "run_sql")
await runSQL();
}
}
catch (error) {
console.error(error.message);
}
process.exit();
});
}

3
dist/commands/admin/list-tables.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
type Params = {};
export default function listTables(params?: Params): Promise<"__exit__" | void>;
export {};

81
dist/commands/admin/list-tables.js vendored Normal file
View File

@ -0,0 +1,81 @@
import chalk from "chalk";
import { select } from "@inquirer/prompts";
import { AppData } from "../../data/app-data";
import showEntries from "./show-entries";
import showFields from "./show-fields";
import dbHandler from "../../lib/db-handler";
import MariaDBQuoteGen from "../../lib/schema/mariadb-quote-gen";
export default async function listTables(params) {
const tables = (await dbHandler({
query: `SELECT table_name FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])}`,
})).payload;
if (!tables?.length) {
console.log(chalk.yellow("\nNo tables found.\n"));
return;
}
while (true) {
const tableName = await select({
message: "Select a table:",
choices: [
...tables.map((t) => ({
name: t.table_name,
value: t.table_name,
})),
{ name: chalk.dim("← Go Back"), value: "__back__" },
{ name: chalk.dim("✕ Exit"), value: "__exit__" },
],
});
if (tableName === "__back__")
break;
if (tableName === "__exit__")
return "__exit__";
while (true) {
const action = await select({
message: `"${tableName}" — choose an action:`,
choices: [
{ name: "Entries", value: "entries" },
{ name: "Fields", value: "fields" },
{ name: "Schema", value: "schema" },
{ name: chalk.dim("← Go Back"), value: "__back__" },
{ name: chalk.dim("✕ Exit"), value: "__exit__" },
],
});
if (action === "__back__")
break;
if (action === "__exit__")
return "__exit__";
if (action === "entries") {
const result = await showEntries({ tableName });
if (result === "__exit__")
return "__exit__";
}
if (action === "fields") {
const result = await showFields({ tableName });
if (result === "__exit__")
return "__exit__";
}
if (action === "schema") {
const columns = (await dbHandler({
query: `SELECT ORDINAL_POSITION, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
values: [tableName],
})).payload;
console.log(`\n${chalk.bold(`Schema for "${tableName}":`)} \n`);
if (columns?.length) {
console.table(columns.map((c) => ({
"#": c.ORDINAL_POSITION,
Name: c.COLUMN_NAME,
Type: c.COLUMN_TYPE,
Nullable: c.IS_NULLABLE,
Default: c.COLUMN_DEFAULT ?? "(none)",
Key: c.COLUMN_KEY || "",
Extra: c.EXTRA || "",
})));
}
else {
console.log(chalk.yellow("No columns found."));
}
console.log();
}
}
}
}

3
dist/commands/admin/run-sql.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
type Params = {};
export default function runSQL(params?: Params): Promise<void>;
export {};

25
dist/commands/admin/run-sql.js vendored Normal file
View File

@ -0,0 +1,25 @@
import { input } from "@inquirer/prompts";
import chalk from "chalk";
import dbHandler from "../../lib/db-handler";
export default async function runSQL(params) {
const sql = await input({
message: "Enter SQL query:",
validate: (val) => val.trim().length > 0 || "Query cannot be empty",
});
try {
const res = await dbHandler({ query: sql });
if (res.payload?.length) {
console.log(`\n${chalk.bold(`Result (${res.payload.length} row${res.payload.length !== 1 ? "s" : ""}):`)} \n`);
console.table(res.payload);
}
else if (res.success) {
console.log(chalk.green(`\nSuccess! Affected rows: ${res.insert_return?.affected_rows ?? res.count ?? 0}, Last insert ID: ${res.insert_return?.last_insert_id ?? "—"}\n`));
}
else {
console.error(chalk.red(`\nSQL Error: ${res.error || res.msg}\n`));
}
}
catch (error) {
console.error(chalk.red(`\nSQL Error: ${error.message}\n`));
}
}

5
dist/commands/admin/show-entries.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
type Params = {
tableName: string;
};
export default function showEntries({ tableName }: Params): Promise<"__exit__" | undefined>;
export {};

91
dist/commands/admin/show-entries.js vendored Normal file
View File

@ -0,0 +1,91 @@
import chalk from "chalk";
import { select, input } from "@inquirer/prompts";
import dbHandler from "../../lib/db-handler";
import MariaDBQuoteGen from "../../lib/schema/mariadb-quote-gen";
const LIMIT = 50;
export default async function showEntries({ tableName }) {
let page = 0;
let searchField = null;
let searchTerm = null;
const quotedTable = MariaDBQuoteGen(tableName);
while (true) {
const offset = page * LIMIT;
const rows = searchTerm
? (await dbHandler({
query: `SELECT * FROM ${quotedTable} WHERE ${MariaDBQuoteGen(searchField)} LIKE ? LIMIT ${LIMIT} OFFSET ${offset}`,
values: [`%${searchTerm}%`],
})).payload
: (await dbHandler({
query: `SELECT * FROM ${quotedTable} LIMIT ${LIMIT} OFFSET ${offset}`,
})).payload;
const countRow = searchTerm
? (await dbHandler({
query: `SELECT COUNT(*) as count FROM ${quotedTable} WHERE ${MariaDBQuoteGen(searchField)} LIKE ?`,
values: [`%${searchTerm}%`],
})).payload
: (await dbHandler({
query: `SELECT COUNT(*) as count FROM ${quotedTable}`,
})).payload;
const total = Number(countRow?.[0]?.count || 0);
const searchInfo = searchTerm
? chalk.dim(` · searching "${searchField}" = "${searchTerm}"`)
: "";
if (!rows) {
return;
}
console.log(`\n${chalk.bold(tableName)} — Page ${page + 1}${searchInfo} (${rows.length} of ${total}):\n`);
if (rows.length) {
console.table(rows);
}
else {
console.log(chalk.yellow("No rows found."));
console.log();
}
const choices = [];
if (page > 0)
choices.push({ name: "← Previous Page", value: "prev" });
if (offset + rows.length < total)
choices.push({ name: "Next Page →", value: "next" });
choices.push({ name: "Search by Field", value: "search" });
if (searchTerm)
choices.push({ name: "Clear Search", value: "clear_search" });
choices.push({ name: chalk.dim("← Go Back"), value: "__back__" });
choices.push({ name: chalk.dim("✕ Exit"), value: "__exit__" });
const action = await select({ message: "Navigate:", choices });
if (action === "__back__")
break;
if (action === "__exit__")
return "__exit__";
if (action === "next")
page++;
if (action === "prev")
page--;
if (action === "clear_search") {
searchField = null;
searchTerm = null;
page = 0;
}
if (action === "search") {
const columns = (await dbHandler({
query: `SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
values: [tableName],
})).payload;
if (!columns?.length) {
console.log(chalk.yellow("No columns found for search."));
continue;
}
searchField = await select({
message: "Search by field:",
choices: columns.map((c) => ({
name: c.COLUMN_NAME,
value: c.COLUMN_NAME,
})),
});
searchTerm = await input({
message: `Search term for "${searchField}":`,
validate: (v) => v.trim().length > 0 || "Cannot be empty",
});
page = 0;
}
}
}

5
dist/commands/admin/show-fields.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
type Params = {
tableName: string;
};
export default function showFields({ tableName, }: Params): Promise<"__exit__" | void>;
export {};

60
dist/commands/admin/show-fields.js vendored Normal file
View File

@ -0,0 +1,60 @@
import chalk from "chalk";
import { select } from "@inquirer/prompts";
import dbHandler from "../../lib/db-handler";
export default async function showFields({ tableName, }) {
const columns = (await dbHandler({
query: `SELECT ORDINAL_POSITION, COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_KEY, EXTRA, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
values: [tableName],
})).payload;
if (!columns?.length) {
console.log(chalk.yellow(`\nNo columns found for "${tableName}".\n`));
return;
}
const indexes = (await dbHandler({
query: `SELECT INDEX_NAME, COLUMN_NAME, NON_UNIQUE, INDEX_TYPE FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
values: [tableName],
})).payload;
const foreignKeys = (await dbHandler({
query: `SELECT CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND REFERENCED_TABLE_NAME IS NOT NULL`,
values: [tableName],
})).payload;
while (true) {
const fieldName = await select({
message: `"${tableName}" — select a field:`,
choices: [
...columns.map((c) => ({
name: c.COLUMN_NAME,
value: c.COLUMN_NAME,
})),
{ name: chalk.dim("← Go Back"), value: "__back__" },
{ name: chalk.dim("✕ Exit"), value: "__exit__" },
],
});
if (fieldName === "__back__")
break;
if (fieldName === "__exit__")
return "__exit__";
const col = columns.find((c) => c.COLUMN_NAME === fieldName);
const colIndexes = indexes?.filter((i) => i.COLUMN_NAME === fieldName) || [];
const fk = foreignKeys?.find((f) => f.COLUMN_NAME === fieldName);
console.log(`\n${chalk.bold(`Field: "${fieldName}"`)}\n`);
console.log(` ${chalk.dim("Table")} ${tableName}`);
console.log(` ${chalk.dim("Column #")} ${col.ORDINAL_POSITION}`);
console.log(` ${chalk.dim("Type")} ${col.COLUMN_TYPE}`);
console.log(` ${chalk.dim("Primary Key")} ${col.COLUMN_KEY === "PRI" ? chalk.green("YES") : "NO"}`);
console.log(` ${chalk.dim("Not Null")} ${col.IS_NULLABLE === "NO" ? chalk.yellow("YES") : "NO"}`);
console.log(` ${chalk.dim("Default")} ${col.COLUMN_DEFAULT ?? chalk.italic("(none)")}`);
console.log(` ${chalk.dim("Extra")} ${col.EXTRA || chalk.italic("(none)")}`);
console.log(` ${chalk.dim("Indexed")} ${colIndexes.length ? chalk.cyan(colIndexes.map((i) => i.INDEX_NAME).join(", ")) : "NO"}`);
if (fk) {
console.log(` ${chalk.dim("Foreign Key")} ${chalk.magenta(`${fk.REFERENCED_TABLE_NAME}(${fk.REFERENCED_COLUMN_NAME})`)}`);
}
else {
console.log(` ${chalk.dim("Foreign Key")} NO`);
}
if (col.COLUMN_COMMENT) {
console.log(` ${chalk.dim("Comment")} ${col.COLUMN_COMMENT}`);
}
console.log();
}
}

2
dist/commands/backup.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

33
dist/commands/backup.js vendored Normal file
View File

@ -0,0 +1,33 @@
import { Command } from "commander";
import path from "path";
import fs from "fs";
import grabDBDir from "../utils/grab-db-dir";
import grabDBBackupFileName from "../utils/grab-db-backup-file-name";
import chalk from "chalk";
import trimBackups from "../utils/trim-backups";
import { dumpDatabase } from "../utils/mariadb-dump-restore";
export default function () {
return new Command("backup")
.description("Backup MariaDB database (SQL dump)")
.action(async () => {
console.log(`Backing up database ...`);
const config = global.CONFIG;
const { backup_dir } = grabDBDir({ config });
if (!fs.existsSync(backup_dir)) {
fs.mkdirSync(backup_dir, { recursive: true });
}
const backup_file_name = grabDBBackupFileName({ config });
const backup_path = path.join(backup_dir, backup_file_name);
try {
const sql = await dumpDatabase(config);
fs.writeFileSync(backup_path, sql, "utf-8");
trimBackups({ config });
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))}${backup_path}`);
process.exit(0);
}
catch (error) {
console.error(chalk.red(`Backup ERROR => ${error.message}`));
process.exit(1);
}
});
}

2
dist/commands/export.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

62
dist/commands/export.js vendored Normal file
View File

@ -0,0 +1,62 @@
import { Command } from "commander";
import path from "path";
import fs from "fs";
import chalk from "chalk";
import grabDBDir from "../utils/grab-db-dir";
import { AppData } from "../data/app-data";
import { dumpDatabase } from "../utils/mariadb-dump-restore";
import { writeExportArchive } from "../utils/export-archive";
import trimExports from "../utils/trim-exports";
function defaultExportFileName(dbName, format) {
return `${dbName}-${Date.now()}.${format}`;
}
export default function () {
return new Command("export")
.description("Export database SQL dump + schema.ts 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 { db_dir, export_dir } = grabDBDir({ config });
if (!fs.existsSync(export_dir)) {
fs.mkdirSync(export_dir, { recursive: true });
}
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
if (!fs.existsSync(schemaPath)) {
console.error(chalk.red(`Schema file not found: ${schemaPath}`));
process.exit(1);
}
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);
const parent = path.dirname(outPath);
if (!fs.existsSync(parent)) {
fs.mkdirSync(parent, { recursive: true });
}
}
else {
outPath = path.join(export_dir, defaultExportFileName(config.db_name, format));
}
try {
const sql = await dumpDatabase(config);
const schemaTs = fs.readFileSync(schemaPath, "utf-8");
await writeExportArchive({
contents: { sql, schemaTs },
outPath,
});
if (path.dirname(outPath) === export_dir) {
trimExports({ config });
}
console.log(`${chalk.bold(chalk.green(`DB Export Success!`))}${outPath}`);
console.log(chalk.dim(`Contains: dump.sql + ${AppData.DbSchemaFileName}`));
process.exit(0);
}
catch (error) {
console.error(chalk.red(`Export ERROR => ${error.message}`));
process.exit(1);
}
});
}

2
dist/commands/import.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

86
dist/commands/import.js vendored Normal file
View File

@ -0,0 +1,86 @@
import { Command } from "commander";
import path from "path";
import fs from "fs";
import chalk from "chalk";
import { 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";
import { AppData } from "../data/app-data";
import { restoreDatabase } from "../utils/mariadb-dump-restore";
import { isArchivePath, isSqlPath, readExportArchive, } from "../utils/export-archive";
function formatChoice(name, index) {
const { backup_date } = grabBackupData({ backup_name: name });
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})`;
}
export default function () {
return new Command("import")
.description("Import an SQL dump, or a full export archive (SQL + schema.ts)")
.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.ts)")
.option("--schema-only", "When importing an archive, write schema.ts only (skip SQL restore)")
.action(async (fileArg, opts) => {
console.log(`Importing database ...`);
const config = global.CONFIG;
const { db_dir, export_dir } = grabDBDir({ config });
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.`);
process.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}`));
process.exit(1);
}
if (isSqlPath(filePath)) {
if (opts.schemaOnly) {
console.error(chalk.red(`--schema-only requires an export archive, not a plain .sql file.`));
process.exit(1);
}
const sql = fs.readFileSync(filePath, "utf-8");
await restoreDatabase(config, sql);
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))}${filePath}`);
process.exit(0);
}
if (!isArchivePath(filePath)) {
console.error(chalk.red(`Unsupported file type: ${filePath}. Use .sql, .tar.gz, .tar, or .zip.`));
process.exit(1);
}
if (opts.sqlOnly && opts.schemaOnly) {
console.error(chalk.red(`Cannot combine --sql-only and --schema-only.`));
process.exit(1);
}
const { sql, schemaTs } = await readExportArchive(filePath);
if (!opts.schemaOnly) {
await restoreDatabase(config, sql);
console.log(chalk.green(`SQL restored from archive`));
}
if (!opts.sqlOnly) {
const schemaPath = path.join(db_dir, AppData.DbSchemaFileName);
fs.writeFileSync(schemaPath, schemaTs, "utf-8");
console.log(chalk.green(`Schema written → ${schemaPath}`));
}
console.log(`${chalk.bold(chalk.green(`DB Import Success!`))}${filePath}`);
process.exit(0);
}
catch (error) {
console.error(chalk.red(`Import ERROR => ${error.message}`));
process.exit(1);
}
});
}

2
dist/commands/index.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env bun
export {};

39
dist/commands/index.js vendored Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env bun
import { program } from "commander";
import schema from "./schema";
import typedef from "./typedef";
import backup from "./backup";
import restore from "./restore";
import exportCmd from "./export";
import importCmd from "./import";
import admin from "./admin";
import init from "../functions/init";
init();
/**
* # Describe Program
*/
program
.name(`bun-mariadb`)
.description(`MariaDB manager for Bun`)
.version(`1.0.0`);
/**
* # Declare Commands
*/
program.addCommand(schema());
program.addCommand(typedef());
program.addCommand(backup());
program.addCommand(restore());
program.addCommand(exportCmd());
program.addCommand(importCmd());
program.addCommand(admin());
/**
* # Handle Unavailable Commands
*/
program.on("command:*", () => {
console.error("Invalid command: %s\nSee --help for a list of available commands.", program.args.join(" "));
process.exit(1);
});
/**
* # Parse Arguments
*/
program.parse(Bun.argv);

2
dist/commands/restore.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

50
dist/commands/restore.js vendored Normal file
View File

@ -0,0 +1,50 @@
import { Command } from "commander";
import grabDBDir from "../utils/grab-db-dir";
import fs from "fs";
import chalk from "chalk";
import grabSortedBackups from "../utils/grab-sorted-backups";
import { select } from "@inquirer/prompts";
import grabBackupData from "../utils/grab-backup-data";
import path from "path";
import { restoreDatabase } from "../utils/mariadb-dump-restore";
export default function () {
return new Command("restore")
.description("Restore MariaDB database from an SQL dump")
.action(async () => {
console.log(`Restoring database ...`);
const config = global.CONFIG;
const { backup_dir } = grabDBDir({ config });
const backups = grabSortedBackups({ config }).filter((b) => b.endsWith(".sql"));
if (!backups?.[0]) {
console.error(`No SQL backups to restore. Use the \`backup\` command to create a backup.`);
process.exit(1);
}
try {
const selected_backup = await select({
message: "Select a backup:",
choices: backups.map((b, i) => {
const { backup_date } = grabBackupData({
backup_name: b,
});
return {
name: `Backup #${i + 1}: ${backup_date.toDateString()} ${backup_date.getHours()}:${backup_date.getMinutes()}:${backup_date.getSeconds().toString().padStart(2, "0")}`,
value: b,
};
}),
});
const backup_path = path.join(backup_dir, selected_backup);
if (!fs.existsSync(backup_path)) {
console.error(`Backup file not found: ${backup_path}`);
process.exit(1);
}
const sql = fs.readFileSync(backup_path, "utf-8");
await restoreDatabase(config, sql);
console.log(`${chalk.bold(chalk.green(`DB Restore Success!`))}${selected_backup}`);
process.exit(0);
}
catch (error) {
console.error(`Backup Restore ERROR => ${error.message}`);
process.exit(1);
}
});
}

2
dist/commands/schema.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

44
dist/commands/schema.js vendored Normal file
View File

@ -0,0 +1,44 @@
import { Command } from "commander";
import grabDirNames from "../data/grab-dir-names";
import path from "path";
import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef";
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
import chalk from "chalk";
import { writeLiveSchema } from "../functions/live-schema";
import createDBSchema from "../lib/schema/create-db-schema";
export default function () {
return new Command("schema")
.description("Build DB From Schema")
.option("-t, --typedef", "Generate typescript type definitions")
.action(async (opts) => {
console.log(`Starting process ...`);
const config = global.CONFIG;
const dbSchema = global.DB_SCHEMA;
const { ROOT_DIR } = grabDirNames();
try {
const isTypeDef = Boolean(opts.typedef || opts.t);
const finaldbSchema = appendDefaultFieldsToDbSchema({
dbSchema,
});
await createDBSchema({
db_schema: finaldbSchema,
config,
});
if (isTypeDef && config.typedef_file_path) {
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
config,
});
}
writeLiveSchema({ schema: finaldbSchema });
console.log(`${chalk.bold(chalk.green(`DB Schema setup success!`))}`);
process.exit(0);
}
catch (error) {
console.error(error);
process.exit(1);
}
});
}

2
dist/commands/typedef.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import { Command } from "commander";
export default function (): Command;

29
dist/commands/typedef.js vendored Normal file
View File

@ -0,0 +1,29 @@
import { Command } from "commander";
import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef";
import path from "path";
import grabDirNames from "../data/grab-dir-names";
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
import chalk from "chalk";
export default function () {
return new Command("typedef")
.description("Generate TypeScript type definitions from the schema")
.action(async () => {
console.log(`Creating Type Definition From DB Schema ...`);
const config = global.CONFIG;
const dbSchema = global.DB_SCHEMA;
const { ROOT_DIR } = grabDirNames();
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
if (!config.typedef_file_path) {
console.error(`\`typedef_file_path\` is required in bun-mariadb.config.ts to generate types.`);
process.exit(1);
}
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
config,
});
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
process.exit(0);
});
}

11
dist/data/app-data.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
export declare const AppData: {
readonly ConfigFileName: "bun-mariadb.config.ts";
readonly MaxBackups: 10;
readonly MaxExports: 10;
readonly DefaultBackupDirName: ".backups";
readonly DefaultExportDirName: ".exports";
readonly DbSchemaManagerTableName: "__db_schema_manager__";
readonly DbSchemaFileName: "schema.ts";
readonly MaxInitRetries: 50;
readonly InitRetryIntervalMilliseconds: 5000;
};

11
dist/data/app-data.js vendored Normal file
View File

@ -0,0 +1,11 @@
export const AppData = {
ConfigFileName: "bun-mariadb.config.ts",
MaxBackups: 10,
MaxExports: 10,
DefaultBackupDirName: ".backups",
DefaultExportDirName: ".exports",
DbSchemaManagerTableName: "__db_schema_manager__",
DbSchemaFileName: "schema.ts",
MaxInitRetries: 50,
InitRetryIntervalMilliseconds: 5000,
};

11
dist/data/grab-dir-names.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import type { BunMariaDBConfig } from "../types";
type Params = {
config?: BunMariaDBConfig;
};
export default function grabDirNames(params?: Params): {
ROOT_DIR: string;
BUN_MARIADB_DIR: string;
BUN_MARIADB_TEMP_DIR: string;
BUN_MARIADB_LIVE_SCHEMA: string;
};
export {};

13
dist/data/grab-dir-names.js vendored Normal file
View File

@ -0,0 +1,13 @@
import path from "path";
export default function grabDirNames(params) {
const ROOT_DIR = process.cwd();
const BUN_MARIADB_DIR = path.join(ROOT_DIR, ".bun-mariadb");
const BUN_MARIADB_TEMP_DIR = path.join(BUN_MARIADB_DIR, ".tmp");
const BUN_MARIADB_LIVE_SCHEMA = path.join(BUN_MARIADB_DIR, "live-schema.json");
return {
ROOT_DIR,
BUN_MARIADB_DIR,
BUN_MARIADB_TEMP_DIR,
BUN_MARIADB_LIVE_SCHEMA,
};
}

View File

@ -0,0 +1,6 @@
import { type BunMariaDBConfig } from "../types";
type Params = {
config: BunMariaDBConfig;
};
export default function grabMariaDBClient({ config, }: Params): Bun.SQL | undefined;
export {};

48
dist/functions/grab-mariadb-client.js vendored Normal file
View File

@ -0,0 +1,48 @@
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. */
const clientCache = new Map();
export default function grabMariaDBClient({ config, }) {
const cacheKey = config.db_name || "__default__";
const cached = clientCache.get(cacheKey);
if (cached) {
return cached;
}
// Prefer the process-wide client when it matches the requested database
if (global.MARIADB_CLIENT &&
(!global.CONFIG?.db_name || global.CONFIG.db_name === config.db_name)) {
clientCache.set(cacheKey, global.MARIADB_CLIENT);
return global.MARIADB_CLIENT;
}
const { ROOT_DIR } = grabDirNames();
try {
const tls = config.ssl_ca
? {
ca: Bun.file(path.resolve(ROOT_DIR, config.ssl_ca)),
rejectUnauthorized: false,
}
: {
rejectUnauthorized: false,
};
const MariaDBClient = new SQL({
hostname: process.env.BUN_MARIADB_SERVER_HOST,
username: process.env.BUN_MARIADB_SERVER_USERNAME,
password: process.env.BUN_MARIADB_SERVER_PASSWORD,
database: config.db_name,
port: process.env.BUN_MARIADB_SERVER_PORT
? Number(process.env.BUN_MARIADB_SERVER_PORT)
: undefined,
...config.db_config,
tls,
adapter: "mariadb",
});
clientCache.set(cacheKey, MariaDBClient);
return MariaDBClient;
}
catch (error) {
console.error(`Couldn't grab MariaDB Client => ` + error.message);
return;
}
}

10
dist/functions/init.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import { type BunMariaDBConfig, type BUN_MARIADB_DatabaseSchemaType } from "../types";
/**
* # Declare Global Variables
*/
declare global {
var CONFIG: BunMariaDBConfig;
var DB_SCHEMA: BUN_MARIADB_DatabaseSchemaType;
var MARIADB_CLIENT: Bun.SQL;
}
export default function init(): void;

80
dist/functions/init.js vendored Normal file
View File

@ -0,0 +1,80 @@
import path from "path";
import fs from "fs";
import { AppData } from "../data/app-data";
import grabDirNames from "../data/grab-dir-names";
import { RequiredENVs, } from "../types";
import setMariaDBClient from "./set-mariadb-client";
export default function init() {
try {
const { ROOT_DIR } = grabDirNames();
const { ConfigFileName } = AppData;
const ConfigFilePath = path.join(ROOT_DIR, ConfigFileName);
if (!fs.existsSync(ConfigFilePath)) {
console.error(`Please create a \`${ConfigFileName}\` file at the root of your project.`);
process.exit(1);
}
const ConfigImport = require(ConfigFilePath);
const Config = ConfigImport["default"];
if (!Config) {
console.error(`No default export from \`${ConfigFilePath}\`. Please export a default module.`);
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 \`${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 DBSchemaFilePath = path.join(db_dir, AppData["DbSchemaFileName"]);
if (!fs.existsSync(DBSchemaFilePath)) {
console.error(`Please create a schema file at \`${DBSchemaFilePath}\`. Don't forget to export a default module from this file.`);
process.exit(1);
}
const DbSchemaImport = require(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 });
}
const ExportDir = path.resolve(db_dir, AppData["DefaultExportDirName"]);
if (!fs.existsSync(ExportDir)) {
fs.mkdirSync(ExportDir, { recursive: true });
}
global.CONFIG = Config;
global.DB_SCHEMA = DbSchema;
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);
}
}
catch (error) {
console.error(`Initialization ERROR => ` + error.message);
process.exit(1);
}
}

7
dist/functions/live-schema.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import type { BUN_MARIADB_DatabaseSchemaType } from "../types";
type Params = {
schema: BUN_MARIADB_DatabaseSchemaType;
};
export declare function writeLiveSchema({ schema }: Params): void;
export declare function readLiveSchema(): BUN_MARIADB_DatabaseSchemaType | undefined;
export {};

15
dist/functions/live-schema.js vendored Normal file
View File

@ -0,0 +1,15 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import grabDirNames from "../data/grab-dir-names";
import path from "path";
const { BUN_MARIADB_LIVE_SCHEMA } = grabDirNames();
export function writeLiveSchema({ schema }) {
mkdirSync(path.dirname(BUN_MARIADB_LIVE_SCHEMA), { recursive: true });
writeFileSync(BUN_MARIADB_LIVE_SCHEMA, JSON.stringify(schema));
}
export function readLiveSchema() {
if (!existsSync(BUN_MARIADB_LIVE_SCHEMA)) {
return undefined;
}
const live_schema = readFileSync(BUN_MARIADB_LIVE_SCHEMA, "utf-8");
return JSON.parse(live_schema);
}

View File

@ -0,0 +1,6 @@
import { type BunMariaDBConfig } from "../types";
type Params = {
config: BunMariaDBConfig;
};
export default function setMariaDBClient({ config }: Params): void;
export {};

21
dist/functions/set-mariadb-client.js vendored Normal file
View File

@ -0,0 +1,21 @@
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);
}
}

20
dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
import DbDelete from "./lib/mariadb/db-delete";
import DbInsert from "./lib/mariadb/db-insert";
import DbSelect from "./lib/mariadb/db-select";
import DbSQL from "./lib/mariadb/db-sql";
import DbUpdate from "./lib/mariadb/db-update";
import grabDbSchema from "./utils/grab-db-schema";
import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object";
declare const BunMariaDB: {
readonly select: typeof DbSelect;
readonly insert: typeof DbInsert;
readonly update: typeof DbUpdate;
readonly delete: typeof DbDelete;
readonly sql: typeof DbSQL;
readonly utils: {
readonly grab_db_schema: typeof grabDbSchema;
readonly grab_join_fields_from_query_object: typeof grabJoinFieldsFromQueryObject;
};
};
export default BunMariaDB;
export type { BunMariaDBConfig, BUN_MARIADB_DatabaseSchemaType, BUN_MARIADB_TableSchemaType, BUN_MARIADB_FieldSchemaType, BUN_MARIADB_IndexSchemaType, BUN_MARIADB_UniqueConstraintSchemaType, BUN_MARIADB_ForeignKeyType, DBResponseObject, ServerQueryParam, } from "./types";

21
dist/index.js vendored Normal file
View File

@ -0,0 +1,21 @@
import init from "./functions/init";
import DbDelete from "./lib/mariadb/db-delete";
import DbInsert from "./lib/mariadb/db-insert";
import DbSelect from "./lib/mariadb/db-select";
import DbSQL from "./lib/mariadb/db-sql";
import DbUpdate from "./lib/mariadb/db-update";
import grabDbSchema from "./utils/grab-db-schema";
import grabJoinFieldsFromQueryObject from "./utils/grab-join-fields-from-query-object";
init();
const BunMariaDB = {
select: DbSelect,
insert: DbInsert,
update: DbUpdate,
delete: DbDelete,
sql: DbSQL,
utils: {
grab_db_schema: grabDbSchema,
grab_join_fields_from_query_object: grabJoinFieldsFromQueryObject,
},
};
export default BunMariaDB;

15
dist/lib/db-handler.d.ts vendored Normal file
View File

@ -0,0 +1,15 @@
import type { BunMariaDBConfig, DBResponseObject } from "../types";
type Param = {
query: string;
values?: any[];
config?: BunMariaDBConfig;
};
/**
* # Main DB Handler Function
*/
export default function dbHandler<T extends {
[k: string]: any;
} = {
[k: string]: any;
}>({ query, values, config }: Param): Promise<DBResponseObject<T>>;
export {};

61
dist/lib/db-handler.js vendored Normal file
View File

@ -0,0 +1,61 @@
import grabMariaDBClient from "../functions/grab-mariadb-client";
/**
* Resolve a shared MariaDB client. Prefer the global client so schema sync
* and CRUD do not open a new connection per query.
*/
function resolveClient(config) {
const resolvedConfig = config || global.CONFIG;
// Same database as the global client → reuse it
if (global.MARIADB_CLIENT &&
(!resolvedConfig?.db_name ||
!global.CONFIG?.db_name ||
resolvedConfig.db_name === global.CONFIG.db_name)) {
return global.MARIADB_CLIENT;
}
if (resolvedConfig) {
return grabMariaDBClient({ config: resolvedConfig });
}
return global.MARIADB_CLIENT;
}
/**
* # Main DB Handler Function
*/
export default async function dbHandler({ query, values, config }) {
try {
const CLIENT = resolveClient(config);
if (!CLIENT) {
throw new Error(`Couldn't grab MariaDB Client.`);
}
const res = await CLIENT.unsafe(query, values);
const count = res.count;
const res_array = (() => {
try {
return JSON.parse(JSON.stringify(res));
}
catch (error) {
return undefined;
}
})();
const last_insert_id = res_array?.[0] ? res_array[0]?.id : undefined;
const affected_rows = res_array?.[0] ? res_array.length : undefined;
const insert_return = {
count,
last_insert_id,
affected_rows,
};
return {
success: true,
payload: res_array,
single_res: res_array?.[0],
insert_return,
count,
};
}
catch (error) {
return {
success: false,
error: "DB Handler Error => " + error.message,
msg: "DB Handler Error => " + error.message,
};
}
}

View File

@ -0,0 +1,7 @@
type Params = {
sql: string;
table: string;
data: any | any[];
};
export default function ({ sql: passedSql, table, data }: Params): Promise<string>;
export {};

View File

@ -0,0 +1,25 @@
function quoteIdentifier(identifier) {
return `\`${identifier.replace(/`/g, "``")}\``;
}
export default async function ({ sql: passedSql, table, data }) {
const config = global.CONFIG;
const dbSchema = global.DB_SCHEMA;
const tableSchema = dbSchema?.tables.find((t) => t.tableName == table);
if (!tableSchema?.tableName) {
return passedSql;
}
const dataObject = Array.isArray(data) ? data[0] : data;
const dataKeys = Object.keys(dataObject || {});
if (dataKeys.length === 0) {
return passedSql;
}
const hasUniqueField = tableSchema.fields.some((field) => field.unique) || Boolean(tableSchema.uniqueConstraints?.length);
if (!hasUniqueField) {
return passedSql;
}
const setSql = dataKeys
.filter((field) => field !== "updated_at")
.map((field) => `${quoteIdentifier(field)} = VALUES(${quoteIdentifier(field)})`);
setSql.push(`updated_at = ${Date.now()}`);
return `${passedSql} ON DUPLICATE KEY UPDATE ${setSql.join(", ")}`;
}

17
dist/lib/mariadb/db-delete.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string> = {
table: Table;
query?: ServerQueryParam<Schema>;
targetId?: number | string;
config?: BunMariaDBConfig;
};
export default function DbDelete<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, query, targetId, config, }: Params<Schema, Table>): Promise<DBResponseObject>;
export {};

68
dist/lib/mariadb/db-delete.js vendored Normal file
View File

@ -0,0 +1,68 @@
import dbHandler from "../db-handler";
import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator";
function quoteIdentifier(identifier) {
return `\`${identifier.replace(/`/g, "``")}\``;
}
export default async function DbDelete({ table, query, targetId, config, }) {
let sqlObj = null;
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge(finalQuery, {
query: {
id: {
value: String(targetId),
},
},
});
}
sqlObj = sqlGenerator({
tableName: quoteIdentifier(table),
genObject: finalQuery,
});
const whereClause = sqlObj.string.match(/WHERE .*/)?.[0];
if (!whereClause) {
return {
success: false,
msg: `No WHERE clause`,
debug: {
sqlObj,
},
};
}
let sql = `DELETE FROM ${quoteIdentifier(table)} ${whereClause}`;
sql += ` RETURNING *`;
sqlObj.string = sql;
const res = await dbHandler({
query: sql,
values: sqlObj.values,
config,
});
if (!res.success) {
return {
success: false,
msg: "Database delete failed",
debug: {
sqlObj,
},
};
}
return {
..._.omit(res, ["payload"]),
success: Boolean(res.insert_return?.last_insert_id),
debug: {
sqlObj,
},
};
}
catch (error) {
return {
success: false,
error: error.message,
debug: {
sqlObj,
},
};
}
}

View File

@ -0,0 +1,15 @@
import type { BUN_MARIADB_TableSchemaType } from "../../types";
type Param = {
paradigm: "JavaScript" | "TypeScript" | undefined;
table: BUN_MARIADB_TableSchemaType;
query?: any;
typeDefName?: string;
allValuesOptional?: boolean;
addExport?: boolean;
dbName?: string;
};
export default function generateTypeDefinition({ paradigm, table, query, typeDefName, allValuesOptional, addExport, dbName, }: Param): {
typeDefinition: string | null;
tdName: string;
};
export {};

View File

@ -0,0 +1,63 @@
export default function generateTypeDefinition({ paradigm, table, query, typeDefName, allValuesOptional, addExport, dbName, }) {
let typeDefinition = ``;
let tdName = ``;
try {
tdName = typeDefName
? typeDefName
: dbName
? `BUN_MARIADB_${dbName}_${table.tableName}`.toUpperCase()
: `BUN_MARIADB_${query.single}_${query.single_table}`.toUpperCase();
const fields = table.fields;
function typeMap(schemaType) {
if (schemaType.options && schemaType.options.length > 0) {
let opts = schemaType.options.map((opt) => schemaType.dataType?.match(/int/i) || typeof opt == "number"
? `${opt}`
: `"${opt}"`);
opts.push(`""`);
return opts.join(" | ");
}
if (schemaType.dataType?.match(/blob/i)) {
return `Float32Array<ArrayBuffer> | Buffer<ArrayBuffer> | null`;
}
if (schemaType.dataType?.match(/int|double|decimal|real/i)) {
return `number | ""`;
}
if (schemaType.dataType?.match(/text|varchar|timestamp/i)) {
return `string`;
}
if (schemaType.dataType?.match(/boolean/i)) {
return "0 | 1";
}
return "string";
}
const typesArrayTypeScript = [];
const typesArrayJavascript = [];
typesArrayTypeScript.push(`${addExport ? "export " : ""}type ${tdName} = {`);
typesArrayJavascript.push(`/**\n * @typedef {object} ${tdName}`);
fields.forEach((field) => {
if (field.fieldDescription) {
typesArrayTypeScript.push(` /** \n * ${field.fieldDescription}\n */`);
}
const nullValue = allValuesOptional
? "?"
: field.notNullValue
? ""
: "?";
typesArrayTypeScript.push(` ${field.fieldName}${nullValue}: ${typeMap(field)};`);
typesArrayJavascript.push(` * @property {${typeMap(field)}${nullValue}} ${field.fieldName}`);
});
typesArrayTypeScript.push(`}`);
typesArrayJavascript.push(` */`);
if (paradigm?.match(/javascript/i)) {
typeDefinition = typesArrayJavascript.join("\n");
}
if (paradigm?.match(/typescript/i)) {
typeDefinition = typesArrayTypeScript.join("\n");
}
}
catch (error) {
console.log(error.message);
typeDefinition = null;
}
return { typeDefinition, tdName };
}

17
dist/lib/mariadb/db-insert.d.ts vendored Normal file
View File

@ -0,0 +1,17 @@
import type { BunMariaDBConfig, DBResponseObject } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string> = {
table: Table;
data: Schema[];
update_on_duplicate?: boolean;
config?: BunMariaDBConfig;
};
export default function DbInsert<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, data, update_on_duplicate, config, }: Params<Schema, Table>): Promise<DBResponseObject>;
export {};

66
dist/lib/mariadb/db-insert.js vendored Normal file
View File

@ -0,0 +1,66 @@
import dbHandler from "../db-handler";
import sqlInsertGenerator from "../../utils/sql-insert-generator";
import { sanitizeHtmlFieldsBatch } from "../../utils/sanitize-html-fields";
import grabDuplicateSafeInsertSql from "../grab-duplicate-safe-insert-sql";
export default async function DbInsert({ table, data, update_on_duplicate, config, }) {
let sqlObj = null;
try {
const sanitizedData = sanitizeHtmlFieldsBatch({
table,
data,
config,
});
const finalData = sanitizedData.map((d) => ({
created_at: Date.now(),
updated_at: Date.now(),
...d,
}));
sqlObj =
sqlInsertGenerator({
tableName: table,
data: finalData,
}) || null;
if (!sqlObj) {
return {
success: false,
msg: "No insert SQL generated",
};
}
let sql = sqlObj.query;
if (update_on_duplicate && data[0]) {
sql = await grabDuplicateSafeInsertSql({ data, table, sql });
}
sql += ` RETURNING *`;
const res = await dbHandler({
query: sql,
values: sqlObj.values,
config,
});
if (!res.success) {
return {
success: false,
msg: "Database insert failed",
debug: {
sqlObj,
},
};
}
return {
...res,
success: Boolean(res?.insert_return?.affected_rows ||
res.insert_return?.last_insert_id),
debug: {
sqlObj,
},
};
}
catch (error) {
return {
success: false,
error: error.message,
debug: {
sqlObj,
},
};
}
}

View File

@ -0,0 +1,7 @@
import type { BUN_MARIADB_DatabaseSchemaType, BunMariaDBConfig } from "../../types";
type Params = {
dbSchema: BUN_MARIADB_DatabaseSchemaType;
config: BunMariaDBConfig;
};
export default function dbSchemaToType({ config, dbSchema, }: Params): string[] | undefined;
export {};

View File

@ -0,0 +1,44 @@
import _ from "lodash";
import generateTypeDefinition from "./db-generate-type-defs";
export default function dbSchemaToType({ config, dbSchema, }) {
let datasquirelSchema = dbSchema;
if (!datasquirelSchema)
return;
let tableNames = `export const BunMariaDBTables = [\n${datasquirelSchema.tables
.map((tbl) => ` "${tbl.tableName}",`)
.join("\n")}\n] as const`;
const dbTablesSchemas = datasquirelSchema.tables;
const defDbName = config.db_name
?.toUpperCase()
.replace(/[^a-zA-Z0-9]/g, "_");
const defNames = [];
const schemas = dbTablesSchemas
.map((table) => {
let final_table = _.cloneDeep(table);
if (final_table.parentTableName) {
const parent_table = dbTablesSchemas.find((t) => t.tableName === final_table.parentTableName);
if (parent_table) {
final_table = _.merge(parent_table, {
tableName: final_table.tableName,
tableDescription: final_table.tableDescription,
});
}
}
const defObj = generateTypeDefinition({
paradigm: "TypeScript",
table: final_table,
typeDefName: `BUN_MARIADB_${defDbName}_${final_table.tableName.toUpperCase()}`,
allValuesOptional: true,
addExport: true,
});
if (defObj.tdName?.match(/./)) {
defNames.push(defObj.tdName);
}
return defObj.typeDefinition;
})
.filter((schm) => typeof schm == "string");
const allTd = defNames?.[0]
? `export type BUN_MARIADB_${defDbName}_ALL_TYPEDEFS = ${defNames.join(` & `)}`
: ``;
return [tableNames, ...schemas, allTd];
}

18
dist/lib/mariadb/db-select.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string> = {
query?: ServerQueryParam<Schema>;
table: Table;
count?: boolean;
targetId?: number | string;
config?: BunMariaDBConfig;
};
export default function DbSelect<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, query, count, targetId, config, }: Params<Schema, Table>): Promise<DBResponseObject<Schema>>;
export {};

94
dist/lib/mariadb/db-select.js vendored Normal file
View File

@ -0,0 +1,94 @@
import dbHandler from "../db-handler";
import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator";
function quoteIdentifier(identifier) {
return `\`${identifier.replace(/`/g, "``")}\``;
}
export default async function DbSelect({ table, query, count, targetId, config, }) {
let sqlObj = null;
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge(finalQuery, {
query: {
id: {
value: String(targetId),
},
},
});
}
sqlObj = sqlGenerator({
tableName: quoteIdentifier(table),
genObject: finalQuery,
});
const res = await dbHandler({
query: sqlObj.string,
values: sqlObj.values,
config,
});
if (!res.success) {
return {
success: false,
msg: "Database select failed",
debug: {
sqlObj,
sql: sqlObj.string,
},
};
}
const batchRes = (res.payload || []);
let resp = {
success: true,
payload: batchRes,
single_res: batchRes[0],
debug: {
sqlObj,
sql: sqlObj.string,
},
};
if (count) {
const countSqlObject = sqlGenerator({
tableName: quoteIdentifier(table),
genObject: finalQuery,
count,
});
const countSql = `SELECT COUNT(*) AS count FROM (${countSqlObject.string}) AS c`;
const countRes = await dbHandler({
query: countSql,
values: countSqlObject.values,
config,
});
if (!countRes.success) {
return {
...resp,
success: false,
msg: "Database count failed",
debug: {
...resp.debug,
count_sql: countSql,
},
};
}
const countRows = countRes.payload || [];
const countVal = countRows[0]?.count ?? countRows[0]?.["count"];
resp = {
...resp,
count: Number(countVal),
debug: {
...resp.debug,
count_sql: countSql,
},
};
}
return resp;
}
catch (error) {
return {
success: false,
error: error.message,
debug: {
sqlObj,
},
};
}
}

11
dist/lib/mariadb/db-sql.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import type { DBResponseObject, SQLInsertGenValueType } from "../../types";
type Params = {
sql: string;
values?: SQLInsertGenValueType[];
};
export default function DbSQL<T extends {
[k: string]: any;
} = {
[k: string]: any;
}>({ sql, values }: Params): Promise<DBResponseObject<T>>;
export {};

45
dist/lib/mariadb/db-sql.js vendored Normal file
View File

@ -0,0 +1,45 @@
import dbHandler from "../db-handler";
export default async function DbSQL({ sql, values }) {
try {
const trimmedSql = sql.trim();
const isSelect = trimmedSql.match(/^select/i);
const res = await dbHandler({
query: trimmedSql,
values: values,
});
if (!res.success) {
return {
success: false,
msg: "Database query failed",
debug: {
sqlObj: {
sql: trimmedSql,
values,
},
sql,
},
};
}
const payload = isSelect ? (res.payload || []) : undefined;
const single_res = isSelect ? payload?.[0] : res.single_res;
const singleRaw = res.single_res;
return {
success: true,
payload,
single_res,
debug: {
sqlObj: {
sql: trimmedSql,
values,
},
sql,
},
};
}
catch (error) {
return {
success: false,
error: error.message,
};
}
}

18
dist/lib/mariadb/db-update.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
import type { BunMariaDBConfig, DBResponseObject, ServerQueryParam } from "../../types";
type Params<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string> = {
table: Table;
data: Schema;
query?: ServerQueryParam<Schema>;
targetId?: number | string;
config?: BunMariaDBConfig;
};
export default function DbUpdate<Schema extends {
[k: string]: any;
} = {
[k: string]: any;
}, Table extends string = string>({ table, data, query, targetId, config, }: Params<Schema, Table>): Promise<DBResponseObject>;
export {};

109
dist/lib/mariadb/db-update.js vendored Normal file
View File

@ -0,0 +1,109 @@
import dbHandler from "../db-handler";
import _ from "lodash";
import sqlGenerator from "../../utils/sql-generator";
import sanitizeHtmlFields from "../../utils/sanitize-html-fields";
function quoteIdentifier(identifier) {
return `\`${identifier.replace(/`/g, "``")}\``;
}
export default async function DbUpdate({ table, data, query, targetId, config, }) {
let sqlObj = { string: "", values: [] };
try {
let finalQuery = query || {};
if (targetId) {
finalQuery = _.merge(finalQuery, {
query: {
id: {
value: String(targetId),
},
},
});
}
const sqlQueryObj = sqlGenerator({
tableName: quoteIdentifier(table),
genObject: finalQuery,
});
const whereClause = sqlQueryObj.string.match(/WHERE .*/)?.[0];
if (!whereClause) {
return {
success: false,
msg: `No WHERE clause`,
};
}
let values = [];
let sql = ``;
sql += `UPDATE ${quoteIdentifier(table)} SET`;
const sanitizedData = sanitizeHtmlFields({
table,
data,
config,
});
const finalData = {
updated_at: Date.now(),
...sanitizedData,
};
const keys = Object.keys(finalData);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!key)
continue;
const isLast = i == keys.length - 1;
sql += ` ${quoteIdentifier(key)}=?`;
values.push(finalData[key] ?? null);
if (!isLast) {
sql += `,`;
}
}
sql += ` ${whereClause}`;
values = [...values, ...sqlQueryObj.values];
const res = await dbHandler({
query: sql,
values: values,
config,
});
sqlObj.string = sql;
sqlObj.values = values;
let updated_sql = ``;
let updated_sql_values = [];
updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`;
updated_sql_values = [...updated_sql_values, ...sqlQueryObj.values];
updated_sql += ` AND `;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!key)
continue;
if (key == "updated_at")
continue;
const isLast = i == keys.length - 1;
updated_sql += ` ${quoteIdentifier(key)}=?`;
updated_sql_values.push(finalData[key] ?? null);
if (!isLast) {
updated_sql += ` AND `;
}
}
const updated_res = await dbHandler({
query: updated_sql,
values: updated_sql_values,
config,
});
const affected_rows = updated_res.payload?.length;
return {
...res,
success: Boolean(affected_rows),
insert_return: {
affected_rows,
},
debug: {
sqlObj,
},
};
}
catch (error) {
return {
success: false,
error: error.message,
debug: {
sqlObj,
},
};
}
}

View File

@ -0,0 +1,8 @@
import type { BUN_MARIADB_DatabaseSchemaType, BunMariaDBConfig } from "../../types";
type Params = {
dbSchema: BUN_MARIADB_DatabaseSchemaType;
dst_file: string;
config: BunMariaDBConfig;
};
export default function dbSchemaToTypeDef({ dbSchema, dst_file, config, }: Params): void;
export {};

18
dist/lib/mariadb/schema-to-typedef.js vendored Normal file
View File

@ -0,0 +1,18 @@
import path from "node:path";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import dbSchemaToType from "./db-schema-to-typedef";
export default function dbSchemaToTypeDef({ dbSchema, dst_file, config, }) {
try {
if (!dbSchema)
throw new Error("No schema found");
const definitions = dbSchemaToType({ dbSchema, config });
const ourfileDir = path.dirname(dst_file);
if (!existsSync(ourfileDir)) {
mkdirSync(ourfileDir, { recursive: true });
}
writeFileSync(dst_file, definitions?.join("\n\n") || "", "utf-8");
}
catch (error) {
console.log(`Schema to Typedef Error =>`, error.message);
}
}

View File

@ -0,0 +1,2 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
export default function buildColumnDefinition(field: BUN_MARIADB_FieldSchemaType): string;

View File

@ -0,0 +1,43 @@
import isVectorField from "./is-vector-field";
import mapDataType from "./map-data-types";
import MariaDBQuoteGen from "./mariadb-quote-gen";
export default function buildColumnDefinition(field) {
if (!field.fieldName) {
throw new Error("Field name is required");
}
const parts = [MariaDBQuoteGen(field.fieldName)];
parts.push(mapDataType(field));
if (field.autoIncrement) {
parts.push("AUTO_INCREMENT");
}
// Vector columns used in VECTOR INDEX must be NOT NULL
if (field.notNullValue ||
field.primaryKey ||
isVectorField(field)) {
if (!field.primaryKey) {
parts.push("NOT NULL");
}
}
// VECTOR columns cannot be UNIQUE in the usual sense
if (field.unique && !field.primaryKey && !isVectorField(field)) {
parts.push("UNIQUE");
}
if (field.defaultValue !== undefined) {
if (typeof field.defaultValue === "string") {
parts.push(`DEFAULT '${field.defaultValue.replace(/'/g, "''")}'`);
}
else {
parts.push(`DEFAULT ${field.defaultValue}`);
}
}
else if (field.defaultValueLiteral) {
parts.push(`DEFAULT ${field.defaultValueLiteral}`);
}
if (field.onUpdate) {
parts.push(`ON UPDATE ${field.onUpdate}`);
}
else if (field.onUpdateLiteral) {
parts.push(`ON UPDATE ${field.onUpdateLiteral}`);
}
return parts.join(" ");
}

View File

@ -0,0 +1,2 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
export default function buildForeignKeyConstraint(field: BUN_MARIADB_FieldSchemaType): string;

View File

@ -0,0 +1,15 @@
import MariaDBQuoteGen from "./mariadb-quote-gen";
export default function buildForeignKeyConstraint(field) {
const fk = field.foreignKey;
const constraintName = fk.foreignKeyName
? `CONSTRAINT ${MariaDBQuoteGen(fk.foreignKeyName)} `
: "";
let constraint = `${constraintName}FOREIGN KEY (${MariaDBQuoteGen(field.fieldName)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName)}(${MariaDBQuoteGen(fk.destinationTableColumnName)})`;
if (fk.cascadeDelete) {
constraint += " ON DELETE CASCADE";
}
if (fk.cascadeUpdate) {
constraint += " ON UPDATE CASCADE";
}
return constraint;
}

View File

@ -0,0 +1,2 @@
import type { BUN_MARIADB_TableSchemaType } from "../../types";
export default function buildTableOptions(table: BUN_MARIADB_TableSchemaType): string;

View File

@ -0,0 +1,7 @@
export default function buildTableOptions(table) {
const options = ["ENGINE=InnoDB"];
if (table.collation) {
options.push("DEFAULT CHARSET=utf8mb4", `COLLATE ${table.collation}`);
}
return ` ${options.join(" ")}`;
}

View File

@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function createDBManagerTable({ db_schema, config, }: CreateDBSchemaParams): Promise<void>;

View File

@ -0,0 +1,15 @@
import { AppData } from "../../data/app-data";
import dbHandler from "../db-handler";
import MariaDBQuoteGen from "./mariadb-quote-gen";
export default async function createDBManagerTable({ db_schema, config, }) {
let sql = ``;
sql += `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} (\n`;
sql += ` table_name VARCHAR(255) NOT NULL PRIMARY KEY,\n`;
sql += ` created_at BIGINT NOT NULL,\n`;
sql += ` updated_at BIGINT NOT NULL\n`;
sql += `) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci\n`;
await dbHandler({
query: sql,
config,
});
}

2
dist/lib/schema/create-db-schema.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function createDBSchema(params: CreateDBSchemaParams): Promise<void>;

17
dist/lib/schema/create-db-schema.js vendored Normal file
View File

@ -0,0 +1,17 @@
import createDBManagerTable from "./create-db-manager-table";
import handleDBSchemaTables from "./handle-db-schema-tables";
import orderDBSchema from "./order-db-schema";
export default async function createDBSchema(params) {
/**
* Create Schema Manager Table
*/
await createDBManagerTable(params);
/**
* Reorder Tables (parents before children with FKs)
*/
const ordered_db_schema = await orderDBSchema(params);
/**
* Handle Tables (create / update / drop)
*/
await handleDBSchemaTables({ ...params, db_schema: ordered_db_schema });
}

5
dist/lib/schema/create-table.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
export default function createTable({ table, config, }: {
table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig;
}): Promise<void>;

41
dist/lib/schema/create-table.js vendored Normal file
View File

@ -0,0 +1,41 @@
import buildColumnDefinition from "./build-column-definition";
import buildForeignKeyConstraint from "./build-foreign-key-constraint";
import buildTableOptions from "./build-table-options";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import runSchemaQuery from "./run-schema-query";
export default async function createTable({ table, config, }) {
if (!table.tableName.match(/_temp_\d+$/)) {
console.log(`Creating table: ${table.tableName}`);
}
const columnDefinitions = [];
const foreignKeys = [];
const primaryKeys = [];
for (const field of table.fields || []) {
columnDefinitions.push(buildColumnDefinition(field));
if (field.primaryKey && field.fieldName) {
primaryKeys.push(field.fieldName);
}
if (field.foreignKey && !table.isVector) {
foreignKeys.push(buildForeignKeyConstraint(field));
}
}
if (primaryKeys.length > 0) {
const pkCols = primaryKeys.map((k) => MariaDBQuoteGen(k)).join(", ");
columnDefinitions.push(`PRIMARY KEY (${pkCols})`);
}
if (table.uniqueConstraints) {
for (const constraint of table.uniqueConstraints) {
if (constraint.constraintTableFields &&
constraint.constraintTableFields.length > 0) {
const fields = constraint.constraintTableFields
.map((field) => MariaDBQuoteGen(field.value))
.join(", ");
const constraintName = constraint.constraintName ||
`unique_${fields.replace(/`/g, "")}`;
columnDefinitions.push(`CONSTRAINT ${MariaDBQuoteGen(constraintName)} UNIQUE (${fields})`);
}
}
}
const sql = `CREATE TABLE IF NOT EXISTS ${MariaDBQuoteGen(table.tableName)} (${[...columnDefinitions, ...foreignKeys].join(", ")})${buildTableOptions(table)}`;
await runSchemaQuery({ query: sql, config });
}

View File

@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function getExistingTablesFromTablesManagerTable({ config, }: CreateDBSchemaParams): Promise<(string | undefined)[]>;

View File

@ -0,0 +1,10 @@
import { AppData } from "../../data/app-data";
import dbHandler from "../db-handler";
import MariaDBQuoteGen from "./mariadb-quote-gen";
export default async function getExistingTablesFromTablesManagerTable({ config, }) {
const rows = await dbHandler({
query: `SELECT table_name FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])}`,
config,
});
return rows.payload?.map((row) => row.table_name) || [];
}

10
dist/lib/schema/get-table-columns.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import type { BunMariaDBConfig } from "../../types";
export type ColumnInfoRow = {
name: string;
type: string;
comment?: string;
};
export default function getTableColumns({ tableName, config, }: {
tableName: string;
config?: BunMariaDBConfig;
}): Promise<ColumnInfoRow[]>;

15
dist/lib/schema/get-table-columns.js vendored Normal file
View File

@ -0,0 +1,15 @@
import { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
export default async function getTableColumns({ tableName, config, }) {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows({
query: `SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE ${schemaCond.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
values: [...schemaCond.values, tableName],
config,
});
return rows.map((row) => ({
name: row.COLUMN_NAME,
type: row.COLUMN_TYPE,
comment: row.COLUMN_COMMENT,
}));
}

View File

@ -0,0 +1,2 @@
import type { CreateDBSchemaTableHandlerParams } from "../../types";
export default function handleDBSchemaTable({ db_schema, config, table, db_manager_table_name, existing_live_table, }: CreateDBSchemaTableHandlerParams): Promise<void>;

View File

@ -0,0 +1,62 @@
import createTable from "./create-table";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import resolveTable from "./resolve-table";
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
import syncIndexes from "./sync-indexes";
import updateTable from "./update-table";
import upsertDbManagerTable, { removeDbManagerTable, } from "./upsert-db-manager-table";
export default async function handleDBSchemaTable({ db_schema, config, table, db_manager_table_name, existing_live_table, }) {
const resolvedTable = resolveTable(table, db_schema);
let tableExistsTracked = Boolean(db_manager_table_name);
let tableExistsLive = Boolean(existing_live_table?.TABLE_NAME);
let wasRenamed = false;
if (resolvedTable.tableNameOld &&
resolvedTable.tableNameOld !== resolvedTable.tableName) {
// Only hit information_schema when a rename is declared
const schemaCond = schemaCondition(config);
const liveTables = await querySchemaRows({
query: `SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = ?`,
values: [...schemaCond.values, resolvedTable.tableNameOld],
config,
});
if (liveTables.length > 0) {
console.log(`Renaming table: ${resolvedTable.tableNameOld} -> ${resolvedTable.tableName}`);
await runSchemaQuery({
query: `RENAME TABLE ${MariaDBQuoteGen(resolvedTable.tableNameOld)} TO ${MariaDBQuoteGen(resolvedTable.tableName)}`,
config,
});
await upsertDbManagerTable({
tableName: resolvedTable.tableName,
config,
});
await removeDbManagerTable({
tableName: resolvedTable.tableNameOld,
config,
});
tableExistsTracked = true;
tableExistsLive = true;
wasRenamed = true;
}
}
if (!tableExistsTracked && !tableExistsLive) {
await createTable({ table: resolvedTable, config });
await upsertDbManagerTable({
tableName: resolvedTable.tableName,
config,
});
}
else {
if (!wasRenamed) {
await updateTable({
table: resolvedTable,
config,
});
}
await upsertDbManagerTable({
tableName: resolvedTable.tableName,
config,
});
}
await syncIndexes({ table: resolvedTable, config });
}

View File

@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function handleDBSchemaTables(params: CreateDBSchemaParams): Promise<void>;

View File

@ -0,0 +1,93 @@
import _ from "lodash";
import { AppData } from "../../data/app-data";
import handleDBSchemaTable from "./handle-db-schema-table";
import getExistingTablesFromTablesManagerTable from "./get-existing-tables-from-tables-manager-table";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
import { removeDbManagerTable } from "./upsert-db-manager-table";
export default async function handleDBSchemaTables(params) {
const { db_schema, config } = params;
/**
* Grab Tables that have been recorded in the Schema
* Manager Table
*/
const existing_schema_tables = await getExistingTablesFromTablesManagerTable(params);
const schemaCond = schemaCondition(config);
const existing_live_tables = await querySchemaRows({
query: `SELECT TABLE_NAME FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME != ?`,
values: [...schemaCond.values, AppData["DbSchemaManagerTableName"]],
config,
});
for (let i = 0; i < db_schema.tables.length; i++) {
const table = db_schema.tables[i];
if (!table)
continue;
const existing_table = existing_schema_tables.find((t) => t == table.tableName);
const existing_live_table = existing_live_tables.find((t) => t.TABLE_NAME == table.tableName);
await handleDBSchemaTable({
...params,
table,
db_manager_table_name: existing_table,
existing_live_table,
});
}
/**
* Drop tables tracked by the manager but no longer in the schema.
* Skip drops when remaining schema tables still reference the table via FK
* (e.g. external tables like `users` that are referenced but not managed).
*/
const schemaTableNames = db_schema.tables.map((t) => t.tableName);
const tablesToDrop = _.uniq(existing_schema_tables.filter((tableName) => Boolean(tableName) && !schemaTableNames.includes(tableName)));
if (tablesToDrop.length === 0) {
return;
}
const fkRows = await querySchemaRows({
query: `SELECT TABLE_NAME, REFERENCED_TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE ${schemaCond.where} AND REFERENCED_TABLE_NAME IS NOT NULL`,
values: schemaCond.values,
config,
});
const referencedByRemaining = new Map();
for (const row of fkRows) {
if (!row.REFERENCED_TABLE_NAME ||
!tablesToDrop.includes(row.REFERENCED_TABLE_NAME)) {
continue;
}
// Referenced by a table we are keeping
if (schemaTableNames.includes(row.TABLE_NAME) ||
!tablesToDrop.includes(row.TABLE_NAME)) {
const list = referencedByRemaining.get(row.REFERENCED_TABLE_NAME) || [];
if (!list.includes(row.TABLE_NAME)) {
list.push(row.TABLE_NAME);
}
referencedByRemaining.set(row.REFERENCED_TABLE_NAME, list);
}
}
const safeToDrop = [];
for (const tableName of tablesToDrop) {
const dependents = referencedByRemaining.get(tableName);
if (dependents && dependents.length > 0) {
console.warn(`Skipping drop of table \`${tableName}\`: still referenced by ${dependents.join(", ")}. Removing from schema manager tracking only.`);
await removeDbManagerTable({ tableName, config });
continue;
}
safeToDrop.push(tableName);
}
if (safeToDrop.length === 0) {
return;
}
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
try {
for (const tableName of safeToDrop) {
console.log(`Dropping table: ${tableName}`);
await runSchemaQuery({
query: `DROP TABLE IF EXISTS ${MariaDBQuoteGen(tableName)}`,
config,
});
await removeDbManagerTable({ tableName, config });
}
}
finally {
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
}
}

5
dist/lib/schema/is-vector-field.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
/**
* True when a field is a MariaDB native vector column.
*/
export default function isVectorField(field?: BUN_MARIADB_FieldSchemaType): boolean;

8
dist/lib/schema/is-vector-field.js vendored Normal file
View File

@ -0,0 +1,8 @@
/**
* True when a field is a MariaDB native vector column.
*/
export default function isVectorField(field) {
if (!field)
return false;
return field.isVector === true || field.dataType === "VECTOR";
}

2
dist/lib/schema/map-data-types.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
export default function mapDataType(field: BUN_MARIADB_FieldSchemaType): string;

93
dist/lib/schema/map-data-types.js vendored Normal file
View File

@ -0,0 +1,93 @@
export default function mapDataType(field) {
const dataType = field.dataType?.toUpperCase() || "TEXT";
const vectorSize = field.vectorSize || 1536;
// Native MariaDB VECTOR type (11.7+). Prefer this over LONGTEXT storage.
if (field.isVector || dataType === "VECTOR") {
return `VECTOR(${vectorSize})`;
}
switch (dataType) {
case "CHAR":
return `CHAR(${field.integerLength || 255})`;
case "VARCHAR":
return `VARCHAR(${field.integerLength || 255})`;
case "TEXT":
return "TEXT";
case "TINYTEXT":
return "TINYTEXT";
case "MEDIUMTEXT":
return "MEDIUMTEXT";
case "LONGTEXT":
return "LONGTEXT";
case "TINYINT":
return field.integerLength
? `TINYINT(${field.integerLength})`
: "TINYINT";
case "SMALLINT":
return field.integerLength
? `SMALLINT(${field.integerLength})`
: "SMALLINT";
case "MEDIUMINT":
return field.integerLength
? `MEDIUMINT(${field.integerLength})`
: "MEDIUMINT";
case "INT":
return field.integerLength ? `INT(${field.integerLength})` : "INT";
case "BIGINT":
return field.integerLength
? `BIGINT(${field.integerLength})`
: "BIGINT";
case "FLOAT":
return "FLOAT";
case "DOUBLE":
return "DOUBLE";
case "DECIMAL":
if (field.integerLength && field.decimals) {
return `DECIMAL(${field.integerLength}, ${field.decimals})`;
}
return "DECIMAL(10,2)";
case "BINARY":
return `BINARY(${field.integerLength || 1})`;
case "VARBINARY":
return `VARBINARY(${field.integerLength || 255})`;
case "BLOB":
return "BLOB";
case "TINYBLOB":
return "TINYBLOB";
case "MEDIUMBLOB":
return "MEDIUMBLOB";
case "LONGBLOB":
return "LONGBLOB";
case "DATE":
return "DATE";
case "TIME":
return "TIME";
case "DATETIME":
return "DATETIME";
case "TIMESTAMP":
return "TIMESTAMP";
case "YEAR":
return "YEAR";
case "UUID":
return "CHAR(36)"; // MariaDB does not have a native UUID type
case "JSON":
return "JSON";
case "INET6":
return "INET6";
case "BOOLEAN":
return "TINYINT(1)";
case "ENUM": {
const enumVals = (field.options || [])
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
.join(", ");
return `ENUM(${enumVals || "''"})`;
}
case "SET": {
const setVals = (field.options || [])
.map((v) => `'${String(v).replace(/'/g, "''")}'`)
.join(", ");
return `SET(${setVals || "''"})`;
}
default:
return "TEXT";
}
}

View File

@ -0,0 +1 @@
export default function MariaDBQuoteGen(str: string): string;

3
dist/lib/schema/mariadb-quote-gen.js vendored Normal file
View File

@ -0,0 +1,3 @@
export default function MariaDBQuoteGen(str) {
return `\`${str.replace(/`/g, "``")}\``;
}

2
dist/lib/schema/order-db-schema.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import type { BUN_MARIADB_DatabaseSchemaType, CreateDBSchemaParams } from "../../types";
export default function orderDBSchema({ db_schema, }: CreateDBSchemaParams): Promise<BUN_MARIADB_DatabaseSchemaType>;

57
dist/lib/schema/order-db-schema.js vendored Normal file
View File

@ -0,0 +1,57 @@
import _ from "lodash";
export default async function orderDBSchema({ db_schema, }) {
let new_db_schema = _.cloneDeep(db_schema);
const tables = new_db_schema.tables;
let new_tables_set = new Set();
let new_tables_start_set = new Set();
let new_tables_end_set = new Set();
function setParentTable(table) {
const fields = table.fields;
let does_table_have_foreign_keys = false;
for (let f = 0; f < fields.length; f++) {
const field = fields[f];
if (!field)
continue;
const dst_table_name = field.foreignKey?.destinationTableName;
if (dst_table_name) {
const fk_table = tables.find((tb) => tb.tableName == dst_table_name);
if (fk_table &&
!new_tables_start_set.has(fk_table) &&
!new_tables_end_set.has(fk_table)) {
setParentTable(fk_table);
}
new_tables_end_set.add(table);
if (fk_table) {
new_tables_start_set.add(fk_table);
}
does_table_have_foreign_keys = true;
}
}
return { does_table_have_foreign_keys };
}
for (let i = 0; i < tables.length; i++) {
const table = tables[i];
if (!table) {
continue;
}
let { does_table_have_foreign_keys } = setParentTable(table);
if (does_table_have_foreign_keys) {
new_tables_end_set.add(table);
}
else {
new_tables_start_set.add(table);
}
}
const parsed_tables = [
...Array.from(new_tables_start_set),
...Array.from(new_tables_end_set),
];
for (let nt = 0; nt < parsed_tables.length; nt++) {
const new_table = parsed_tables[nt];
if (new_table) {
new_tables_set.add(new_table);
}
}
new_db_schema.tables = Array.from(new_tables_set);
return new_db_schema;
}

9
dist/lib/schema/recreate-table.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
/**
* Full table rebuild. For `isVector` tables this drops and recreates in place
* (preserving rows when possible). For regular tables it uses a temp-table swap.
*/
export default function recreateTable({ table, config, }: {
table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig;
}): Promise<void>;

111
dist/lib/schema/recreate-table.js vendored Normal file
View File

@ -0,0 +1,111 @@
import createTable from "./create-table";
import getTableColumns from "./get-table-columns";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
async function checkIfTableExists({ tableName, config, }) {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows({
query: `SELECT 1 AS \`table_exists\` FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_NAME = ? LIMIT 1`,
values: [...schemaCond.values, tableName],
config,
});
return Boolean(rows[0]?.table_exists);
}
/**
* Full table rebuild. For `isVector` tables this drops and recreates in place
* (preserving rows when possible). For regular tables it uses a temp-table swap.
*/
export default async function recreateTable({ table, config, }) {
const doesTableExist = await checkIfTableExists({
tableName: table.tableName,
config,
});
if (!doesTableExist) {
await createTable({ table, config });
return;
}
/**
* Vector tables: drop + recreate + reinsert (MariaDB VECTOR INDEX / dim
* changes are not reliably alterable in place).
*/
if (table.isVector) {
console.log(`Recreating vector table: ${table.tableName}`);
const existingRows = await querySchemaRows({
query: `SELECT * FROM ${MariaDBQuoteGen(table.tableName)}`,
config,
});
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
try {
await runSchemaQuery({
query: `DROP TABLE IF EXISTS ${MariaDBQuoteGen(table.tableName)}`,
config,
});
await createTable({ table, config });
}
finally {
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
}
if (existingRows.length > 0) {
const schemaFieldNames = new Set((table.fields || [])
.map((f) => f.fieldName)
.filter((n) => Boolean(n)));
for (const row of existingRows) {
const columns = Object.keys(row).filter((c) => schemaFieldNames.has(c));
if (columns.length === 0)
continue;
const placeholders = columns.map(() => "?").join(", ");
const columnList = columns
.map((c) => MariaDBQuoteGen(c))
.join(", ");
await runSchemaQuery({
query: `INSERT INTO ${MariaDBQuoteGen(table.tableName)} (${columnList}) VALUES (${placeholders})`,
values: columns.map((c) => row[c] ?? null),
config,
});
}
}
return;
}
const tempTableName = `${table.tableName}_temp_${Date.now()}`;
const backupOldTableName = `${table.tableName}_old_${Date.now()}`;
const existingColumns = await getTableColumns({
tableName: table.tableName,
config,
});
const columnsToKeep = (table.fields || [])
.filter((field) => existingColumns.some((column) => column.name === field.fieldName))
.map((field) => field.fieldName)
.filter((fieldName) => Boolean(fieldName));
await createTable({
table: { ...table, tableName: tempTableName },
config,
});
if (columnsToKeep.length > 0) {
const columnList = columnsToKeep
.map((column) => MariaDBQuoteGen(column))
.join(", ");
await runSchemaQuery({
query: `INSERT INTO ${MariaDBQuoteGen(tempTableName)} (${columnList}) SELECT ${columnList} FROM ${MariaDBQuoteGen(table.tableName)}`,
config,
});
}
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
try {
await runSchemaQuery({
query: `RENAME TABLE ${MariaDBQuoteGen(table.tableName)} TO ${MariaDBQuoteGen(backupOldTableName)}`,
config,
});
await runSchemaQuery({
query: `RENAME TABLE ${MariaDBQuoteGen(tempTableName)} TO ${MariaDBQuoteGen(table.tableName)}`,
config,
});
await runSchemaQuery({
query: `DROP TABLE ${MariaDBQuoteGen(backupOldTableName)}`,
config,
});
}
finally {
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 1`, config });
}
}

2
dist/lib/schema/resolve-table.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import type { BUN_MARIADB_DatabaseSchemaType, BUN_MARIADB_TableSchemaType } from "../../types";
export default function resolveTable(table: BUN_MARIADB_TableSchemaType, db_schema: BUN_MARIADB_DatabaseSchemaType): BUN_MARIADB_TableSchemaType;

34
dist/lib/schema/resolve-table.js vendored Normal file
View File

@ -0,0 +1,34 @@
import _ from "lodash";
export default function resolveTable(table, db_schema) {
if (!table.parentTableName) {
return _.cloneDeep(table);
}
const parentTable = db_schema.tables.find((schemaTable) => schemaTable.tableName === table.parentTableName);
if (!parentTable) {
throw new Error(`Parent table \`${table.parentTableName}\` not found for \`${table.tableName}\``);
}
const mergedFieldsMap = new Map();
(parentTable.fields || []).forEach((f) => {
if (f.fieldName)
mergedFieldsMap.set(f.fieldName, _.cloneDeep(f));
});
(table.fields || []).forEach((f) => {
if (f.fieldName) {
const existing = mergedFieldsMap.get(f.fieldName) || {};
mergedFieldsMap.set(f.fieldName, _.merge({}, existing, f));
}
});
return {
..._.cloneDeep(parentTable),
tableName: table.tableName,
tableDescription: table.tableDescription || parentTable.tableDescription,
collation: table.collation || parentTable.collation,
isVector: table.isVector !== undefined ? table.isVector : parentTable.isVector,
fields: Array.from(mergedFieldsMap.values()),
indexes: _.uniqBy([...(parentTable.indexes || []), ...(table.indexes || [])], "indexName"),
uniqueConstraints: [
...(parentTable.uniqueConstraints || []),
...(table.uniqueConstraints || []),
],
};
}

11
dist/lib/schema/run-schema-query.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
import type { BunMariaDBConfig } from "../../types";
export default function runSchemaQuery({ query, values, config, }: {
query: string;
values?: any[];
config?: BunMariaDBConfig;
}): Promise<void>;
export declare function querySchemaRows<T extends Record<string, any>>({ query, values, config, }: {
query: string;
values?: any[];
config?: BunMariaDBConfig;
}): Promise<T[]>;

22
dist/lib/schema/run-schema-query.js vendored Normal file
View File

@ -0,0 +1,22 @@
import dbHandler from "../db-handler";
export default async function runSchemaQuery({ query, values, config, }) {
const res = await dbHandler({
query,
values,
config,
});
if (!res.success) {
throw new Error(`Database query failed: ${query} ... ERROR: ${res.error || res.msg}`);
}
}
export async function querySchemaRows({ query, values, config, }) {
const res = await dbHandler({
query,
values,
config,
});
if (!res.success) {
throw new Error(`Database query failed: ${query} ... ERROR: ${res.error || res.msg}`);
}
return (res.payload || []);
}

5
dist/lib/schema/schema-condition.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import type { BunMariaDBConfig } from "../../types";
export default function schemaCondition(config?: BunMariaDBConfig): {
where: string;
values: string[];
};

13
dist/lib/schema/schema-condition.js vendored Normal file
View File

@ -0,0 +1,13 @@
export default function schemaCondition(config) {
const databaseName = config?.db_name || global.CONFIG?.db_name;
if (databaseName) {
return {
where: "TABLE_SCHEMA = ?",
values: [databaseName],
};
}
return {
where: "TABLE_SCHEMA = DATABASE()",
values: [],
};
}

5
dist/lib/schema/sync-indexes.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
export default function syncIndexes({ table, config, }: {
table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig;
}): Promise<void>;

131
dist/lib/schema/sync-indexes.js vendored Normal file
View File

@ -0,0 +1,131 @@
import isVectorField from "./is-vector-field";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
function isVectorIndexDef(index, table) {
if (index.indexType === "VECTOR")
return true;
if (table.isVector)
return true;
const firstFieldName = index.indexTableFields?.[0];
if (!firstFieldName)
return false;
const field = table.fields?.find((f) => f.fieldName === firstFieldName);
return isVectorField(field);
}
function vectorDistanceMetric(index) {
if (index.vectorDistanceMetric === "cosine")
return "cosine";
if (index.vectorDistanceMetric === "euclidean")
return "euclidean";
return "euclidean";
}
export default async function syncIndexes({ table, config, }) {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows({
query: `SELECT INDEX_NAME, COLUMN_NAME, INDEX_TYPE FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY' ORDER BY INDEX_NAME, SEQ_IN_INDEX`,
values: [...schemaCond.values, table.tableName],
config,
});
/**
* Indexes required by foreign keys / unique constraints cannot be dropped
* freely. Skip those when cleaning up schema indexes.
*/
const protectedConstraintRows = await querySchemaRows({
query: `SELECT CONSTRAINT_NAME, CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND CONSTRAINT_TYPE IN ('FOREIGN KEY', 'UNIQUE')`,
values: [...schemaCond.values, table.tableName],
config,
});
const protectedIndexNames = new Set(protectedConstraintRows.map((r) => r.CONSTRAINT_NAME));
// Column-level UNIQUE creates an index often named after the column
for (const field of table.fields || []) {
if (field.unique && field.fieldName) {
protectedIndexNames.add(field.fieldName);
}
}
for (const constraint of table.uniqueConstraints || []) {
if (constraint.constraintName) {
protectedIndexNames.add(constraint.constraintName);
}
}
const existingIndexesMap = new Map();
for (const row of rows) {
if (!existingIndexesMap.has(row.INDEX_NAME)) {
existingIndexesMap.set(row.INDEX_NAME, {
columns: [],
type: row.INDEX_TYPE,
});
}
existingIndexesMap.get(row.INDEX_NAME).columns.push(row.COLUMN_NAME);
}
for (const [indexName, details] of existingIndexesMap.entries()) {
if (protectedIndexNames.has(indexName)) {
continue;
}
const schemaIndex = table.indexes?.find((i) => i.indexName === indexName);
if (!schemaIndex) {
console.log(`Dropping index: ${indexName}`);
try {
await runSchemaQuery({
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
config,
});
}
catch (err) {
if (String(err?.message || "").includes("needed in a foreign key constraint")) {
console.warn(`Skipping drop of index ${indexName}: required by a foreign key constraint`);
continue;
}
throw err;
}
}
else {
const schemaColumns = schemaIndex.indexTableFields || [];
const columnsMatch = details.columns.length === schemaColumns.length &&
details.columns.every((col, idx) => col === schemaColumns[idx]);
if (!columnsMatch) {
console.log(`Recreating changed index: ${indexName}`);
await runSchemaQuery({
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
config,
});
existingIndexesMap.delete(indexName);
}
}
}
for (const index of table.indexes || []) {
if (!index.indexName ||
!index.indexTableFields ||
index.indexTableFields.length === 0) {
continue;
}
if (!existingIndexesMap.has(index.indexName)) {
if (isVectorIndexDef(index, table)) {
console.log(`Creating Vector index: ${index.indexName}`);
const targetField = MariaDBQuoteGen(index.indexTableFields[0]);
const distanceMetric = vectorDistanceMetric(index);
await runSchemaQuery({
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD VECTOR INDEX ${MariaDBQuoteGen(index.indexName)} (${targetField}) M=8 DISTANCE=${distanceMetric}`,
config,
});
}
else {
console.log(`Creating standard index: ${index.indexName}`);
const fields = index.indexTableFields
.map((field) => MariaDBQuoteGen(field))
.join(", ");
const typeUpper = index.indexType?.toUpperCase();
const isSpecialType = typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
const indexPrefix = isSpecialType ? `${typeUpper} ` : "";
const indexSuffix = !isSpecialType &&
(typeUpper === "BTREE" || typeUpper === "HASH")
? ` USING ${typeUpper}`
: "";
await runSchemaQuery({
query: `CREATE ${indexPrefix}INDEX ${MariaDBQuoteGen(index.indexName)} ON ${MariaDBQuoteGen(table.tableName)} (${fields})${indexSuffix}`,
config,
});
}
}
}
}

5
dist/lib/schema/update-table.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
import type { BUN_MARIADB_TableSchemaType, BunMariaDBConfig } from "../../types";
export default function updateTable({ table, config, }: {
table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig;
}): Promise<void>;

177
dist/lib/schema/update-table.js vendored Normal file
View File

@ -0,0 +1,177 @@
import buildColumnDefinition from "./build-column-definition";
import createTable from "./create-table";
import getTableColumns from "./get-table-columns";
import isVectorField from "./is-vector-field";
import mapDataType from "./map-data-types";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import recreateTable from "./recreate-table";
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
/**
* Compare live COLUMN_TYPE with schema-mapped type.
* Live types often include display widths (e.g. bigint(20) vs BIGINT).
*/
function columnTypesMatch(liveType, expectedType) {
const live = liveType.toLowerCase().replace(/\s+/g, "");
const expected = expectedType.toLowerCase().replace(/\s+/g, "");
if (live === expected)
return true;
// live may include display width: bigint(20) vs bigint
if (live.startsWith(`${expected}(`))
return true;
// expected may include length live omits in some versions
if (expected.startsWith(`${live}(`))
return true;
return false;
}
function vectorTypeDiverged(liveType, liveComment, field) {
const dimensions = field.vectorSize || 1536;
const expectedNative = `vector(${dimensions})`;
const live = liveType.toLowerCase().replace(/\s+/g, "");
if (live === expectedNative)
return false;
// Legacy LONGTEXT storage with vector_size comment
if (live.startsWith("longtext") || live.startsWith("text")) {
const match = liveComment.match(/vector_size\s*=\s*(\d+)/i);
if (match && Number(match[1]) === dimensions) {
// Still legacy storage — treat as diverged so we can migrate to VECTOR
return true;
}
return true;
}
return true;
}
async function addColumn({ tableName, field, config, }) {
console.log(`Adding column: ${tableName}.${field.fieldName}`);
const columnDef = buildColumnDefinition(field).trim();
await runSchemaQuery({
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} ADD COLUMN IF NOT EXISTS ${columnDef}`,
config,
});
}
async function modifyColumn({ tableName, field, config, }) {
console.log(`Modifying column: ${tableName}.${field.fieldName}`);
const columnDef = buildColumnDefinition(field).trim();
await runSchemaQuery({
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} MODIFY COLUMN ${columnDef}`,
config,
});
}
async function dropColumn({ tableName, fieldName, config, }) {
console.log(`Dropping column: ${tableName}.${fieldName}`);
await runSchemaQuery({
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} DROP COLUMN ${MariaDBQuoteGen(fieldName)}`,
config,
});
}
export default async function updateTable({ table, config, }) {
const existingColumns = await getTableColumns({
tableName: table.tableName,
config,
});
if (existingColumns.length === 0) {
await createTable({ table, config });
return;
}
const liveFieldsMap = new Map(existingColumns.map((col) => [
col.name,
{ type: col.type.toLowerCase(), comment: col.comment || "" },
]));
const codeFieldsMap = new Map((table.fields || []).map((f) => [f.fieldName, f]));
const fieldsToAdd = [];
const fieldsToModify = [];
const fieldsToDrop = [];
let needsVectorRecreate = false;
for (const field of table.fields || []) {
if (!field.fieldName)
continue;
const liveField = liveFieldsMap.get(field.fieldName);
if (!liveField) {
// Adding a new vector column can require rebuild if VECTOR INDEX
// constraints conflict; still try surgical add first.
fieldsToAdd.push(field);
}
else {
let typeDiverged = !columnTypesMatch(liveField.type, mapDataType(field));
if (isVectorField(field)) {
typeDiverged = vectorTypeDiverged(liveField.type, liveField.comment, field);
if (typeDiverged) {
needsVectorRecreate = true;
}
}
if (typeDiverged) {
fieldsToModify.push(field);
}
}
}
// Vector dimension / storage type changes → full rebuild automatically
if (needsVectorRecreate) {
console.log(`Vector column change detected on \`${table.tableName}\`; recreating table`);
await recreateTable({ table, config });
return;
}
for (const col of existingColumns) {
if (!codeFieldsMap.has(col.name)) {
fieldsToDrop.push(col.name);
}
}
if (fieldsToAdd.length === 0 &&
fieldsToModify.length === 0 &&
fieldsToDrop.length === 0) {
return;
}
console.log(`Surgically updating table structure from database layout: ${table.tableName}`);
if (fieldsToDrop.length > 0) {
const schemaCond = schemaCondition(config);
const pkRows = await querySchemaRows({
query: `SELECT COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE ${schemaCond.where} AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'`,
values: [...schemaCond.values, table.tableName],
config,
});
const pkColumnNames = pkRows.map((r) => r.COLUMN_NAME);
if (fieldsToDrop.some((f) => pkColumnNames.includes(f))) {
console.log(`Dropping primary key because a PK column is being dropped`);
await runSchemaQuery({
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP PRIMARY KEY`,
config,
});
}
const indexRows = await querySchemaRows({
query: `SELECT INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY'`,
values: [...schemaCond.values, table.tableName],
config,
});
const indexesToDrop = new Set();
for (const row of indexRows) {
if (fieldsToDrop.includes(row.COLUMN_NAME)) {
indexesToDrop.add(row.INDEX_NAME);
}
}
for (const indexName of indexesToDrop) {
console.log(`Dropping index ${indexName} because it contains a dropped column`);
await runSchemaQuery({
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
config,
});
}
}
for (const field of fieldsToAdd) {
await addColumn({ tableName: table.tableName, field, config });
}
for (const field of fieldsToModify) {
try {
await modifyColumn({ tableName: table.tableName, field, config });
}
catch (err) {
if (isVectorField(field)) {
console.warn(`[Vector Resize] Re-aligning dimension spaces natively via safe migration schema rebuild.`);
await recreateTable({ table, config });
return;
}
throw err;
}
}
for (const fieldName of fieldsToDrop) {
await dropColumn({ tableName: table.tableName, fieldName, config });
}
}

View File

@ -0,0 +1,9 @@
import type { BunMariaDBConfig } from "../../types";
export default function upsertDbManagerTable({ tableName, config, }: {
tableName: string;
config?: BunMariaDBConfig;
}): Promise<void>;
export declare function removeDbManagerTable({ tableName, config, }: {
tableName: string;
config?: BunMariaDBConfig;
}): Promise<void>;

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