This commit is contained in:
2026-07-14 09:10:30 +01:00
parent 79c5b896fe
commit 56462e6cc2
38 changed files with 1056 additions and 1720 deletions
-4
View File
@@ -1,5 +1,4 @@
import { Command } from "commander";
import grabDBDir from "../../utils/grab-db-dir";
import chalk from "chalk";
import { select } from "@inquirer/prompts";
import listTables from "./list-tables";
@@ -9,9 +8,6 @@ export default function () {
return new Command("admin")
.description("View Tables and Data, Run SQL Queries, Etc.")
.action(async () => {
const config = global.CONFIG;
const { db_file_path } = grabDBDir({ config });
console.log(chalk.bold(chalk.blue("\nBun MariaDB Admin\n")));
try {
+36 -30
View File
@@ -4,24 +4,16 @@ 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";
type Params = {};
type ColumnInfo = {
cid: number;
name: string;
type: string;
notnull: number;
dflt_value: string | null;
pk: number;
};
export default async function listTables(
params?: Params,
): Promise<"__exit__" | void> {
const tables = (
await dbHandler({
query: `SELECT table_name FROM ${AppData["DbSchemaManagerTableName"]}`,
await dbHandler<{ table_name: string }>({
query: `SELECT table_name FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])}`,
})
).payload;
@@ -30,7 +22,6 @@ export default async function listTables(
return;
}
// Level 1: table selection loop
while (true) {
const tableName = await select({
message: "Select a table:",
@@ -47,7 +38,6 @@ export default async function listTables(
if (tableName === "__back__") break;
if (tableName === "__exit__") return "__exit__";
// Level 2: action loop — stays here until "Go Back"
while (true) {
const action = await select({
message: `"${tableName}" — choose an action:`,
@@ -73,24 +63,40 @@ export default async function listTables(
if (result === "__exit__") return "__exit__";
}
// if (action === "schema") {
// const columns = db
// .query<ColumnInfo, []>(`PRAGMA table_info("${tableName}")`)
// .all();
if (action === "schema") {
const columns = (
await dbHandler<{
ORDINAL_POSITION: number;
COLUMN_NAME: string;
COLUMN_TYPE: string;
IS_NULLABLE: string;
COLUMN_DEFAULT: string | null;
COLUMN_KEY: string;
EXTRA: string;
}>({
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`);
// console.table(
// columns.map((c) => ({
// "#": c.cid,
// Name: c.name,
// Type: c.type,
// "Not Null": c.notnull ? "YES" : "NO",
// Default: c.dflt_value ?? "(none)",
// "Primary Key": c.pk ? "YES" : "NO",
// })),
// );
// console.log();
// }
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();
}
}
}
}
+6 -5
View File
@@ -13,18 +13,19 @@ export default async function runSQL(params?: Params) {
try {
const res = await dbHandler({ query: sql });
if (res.payload) {
if (res.payload?.length) {
console.log(
`\n${chalk.bold(`Result (${res.payload.length} row${res.payload.length !== 1 ? "s" : ""}):`)} \n`,
);
if (res.payload.length) console.table(res.payload);
else console.log(chalk.yellow("No res returned.\n"));
} else if (res.single_res) {
console.table(res.payload);
} else if (res.success) {
console.log(
chalk.green(
`\nSuccess! Affected rows: ${res.single_res.changes}, Last insert ID: ${res.single_res.lastInsertRowid}\n`,
`\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: any) {
console.error(chalk.red(`\nSQL Error: ${error.message}\n`));
+37 -24
View File
@@ -1,11 +1,11 @@
import chalk from "chalk";
import { select, input } from "@inquirer/prompts";
import dbHandler from "../../lib/db-handler";
import MariaDBQuoteGen from "../../lib/schema/mariadb-quote-gen";
type Params = {
tableName: string;
};
type ColumnInfo = { cid: number; name: string };
const LIMIT = 50;
@@ -14,36 +14,38 @@ export default async function showEntries({ tableName }: Params) {
let searchField: string | null = null;
let searchTerm: string | null = null;
const quotedTable = MariaDBQuoteGen(tableName);
while (true) {
const offset = page * LIMIT;
const rows = searchTerm
? (
await dbHandler({
query: `SELECT * FROM "${tableName}" WHERE "${searchField}" LIKE ? LIMIT ${LIMIT} OFFSET ${offset}`,
query: `SELECT * FROM ${quotedTable} WHERE ${MariaDBQuoteGen(searchField!)} LIKE ? LIMIT ${LIMIT} OFFSET ${offset}`,
values: [`%${searchTerm}%`],
})
).payload
: (
await dbHandler({
query: `SELECT * FROM "${tableName}" LIMIT ${LIMIT} OFFSET ${offset}`,
query: `SELECT * FROM ${quotedTable} LIMIT ${LIMIT} OFFSET ${offset}`,
})
).payload;
const countRow = searchTerm
? (
await dbHandler({
query: `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${searchField}" LIKE ?`,
await dbHandler<{ count: number }>({
query: `SELECT COUNT(*) as count FROM ${quotedTable} WHERE ${MariaDBQuoteGen(searchField!)} LIKE ?`,
values: [`%${searchTerm}%`],
})
).payload
: (
await dbHandler({
query: `SELECT COUNT(*) as count FROM "${tableName}"`,
await dbHandler<{ count: number }>({
query: `SELECT COUNT(*) as count FROM ${quotedTable}`,
})
).payload;
const total = countRow?.[0]?.count;
const total = Number(countRow?.[0]?.count || 0);
const searchInfo = searchTerm
? chalk.dim(` · searching "${searchField}" = "${searchTerm}"`)
: "";
@@ -57,8 +59,7 @@ export default async function showEntries({ tableName }: Params) {
);
if (rows.length) {
console.log(rows);
// if (rows.length) console.table(rows);
console.table(rows);
} else {
console.log(chalk.yellow("No rows found."));
console.log();
@@ -85,19 +86,31 @@ export default async function showEntries({ tableName }: Params) {
searchTerm = null;
page = 0;
}
// if (action === "search") {
// const columns = db
// .query<ColumnInfo, []>(`PRAGMA table_info("${tableName}")`)
// .all();
// searchField = await select({
// message: "Search by field:",
// choices: columns.map((c) => ({ name: c.name, value: c.name })),
// });
// searchTerm = await input({
// message: `Search term for "${searchField}":`,
// validate: (v) => v.trim().length > 0 || "Cannot be empty",
// });
// page = 0;
// }
if (action === "search") {
const columns = (
await dbHandler<{ COLUMN_NAME: string }>({
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;
}
}
}
+101 -84
View File
@@ -1,92 +1,109 @@
import chalk from "chalk";
import { select } from "@inquirer/prompts";
import dbHandler from "../../lib/db-handler";
type Params = {
tableName: string;
};
type ColumnInfo = {
cid: number;
name: string;
type: string;
notnull: number;
dflt_value: string | null;
pk: number;
};
type IndexInfo = { name: string; unique: number; origin: string };
type IndexColumn = { name: string };
type ForeignKey = {
id: number;
from: string;
table: string;
to: string;
on_update: string;
on_delete: string;
};
export default async function showFields({
tableName,
}: Params): Promise<"__exit__" | void> {
// const columns = db
// .query<ColumnInfo, []>(`PRAGMA table_info("${tableName}")`)
// .all();
// const indexes = db
// .query<IndexInfo, []>(`PRAGMA index_list("${tableName}")`)
// .all();
// const foreignKeys = db
// .query<ForeignKey, []>(`PRAGMA foreign_key_list("${tableName}")`)
// .all();
// const indexedFields = new Map<string, { unique: boolean }>();
// for (const idx of indexes) {
// const cols = db
// .query<IndexColumn, []>(`PRAGMA index_info("${idx.name}")`)
// .all();
// for (const col of cols) {
// indexedFields.set(col.name, { unique: idx.unique === 1 });
// }
// }
// while (true) {
// const fieldName = await select({
// message: `"${tableName}" — select a field:`,
// choices: [
// ...columns.map((c) => ({ name: c.name, value: c.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.name === fieldName)!;
// const idx = indexedFields.get(fieldName);
// const fk = foreignKeys.find((f) => f.from === fieldName);
// console.log(`\n${chalk.bold(`Field: "${fieldName}"`)}\n`);
// console.log(` ${chalk.dim("Table")} ${tableName}`);
// console.log(` ${chalk.dim("Column #")} ${col.cid}`);
// console.log(
// ` ${chalk.dim("Type")} ${col.type || chalk.italic("(none)")}`,
// );
// console.log(
// ` ${chalk.dim("Primary Key")} ${col.pk ? chalk.green("YES") : "NO"}`,
// );
// console.log(
// ` ${chalk.dim("Not Null")} ${col.notnull ? chalk.yellow("YES") : "NO"}`,
// );
// console.log(
// ` ${chalk.dim("Default")} ${col.dflt_value ?? chalk.italic("(none)")}`,
// );
// console.log(
// ` ${chalk.dim("Indexed")} ${idx ? chalk.cyan("YES") : "NO"}`,
// );
// console.log(
// ` ${chalk.dim("Unique")} ${idx?.unique ? chalk.cyan("YES") : "NO"}`,
// );
// if (fk) {
// console.log(
// ` ${chalk.dim("Foreign Key")} ${chalk.magenta(`${fk.table}(${fk.to})`)}`,
// );
// console.log(` ${chalk.dim("On Update")} ${fk.on_update}`);
// console.log(` ${chalk.dim("On Delete")} ${fk.on_delete}`);
// } else {
// console.log(` ${chalk.dim("Foreign Key")} NO`);
// }
// console.log();
// }
const columns = (
await dbHandler<{
ORDINAL_POSITION: number;
COLUMN_NAME: string;
COLUMN_TYPE: string;
IS_NULLABLE: string;
COLUMN_DEFAULT: string | null;
COLUMN_KEY: string;
EXTRA: string;
COLUMN_COMMENT: string;
}>({
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<{
INDEX_NAME: string;
COLUMN_NAME: string;
NON_UNIQUE: number;
INDEX_TYPE: string;
}>({
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<{
CONSTRAINT_NAME: string;
COLUMN_NAME: string;
REFERENCED_TABLE_NAME: string;
REFERENCED_COLUMN_NAME: string;
}>({
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();
}
}
+79 -9
View File
@@ -1,28 +1,98 @@
import { Command } from "commander";
import path from "path";
import grabDBDir from "../utils/grab-db-dir";
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 mariadbCliEnv, {
mariadbCliConnectionArgs,
} from "../utils/mariadb-cli-env";
/**
* Prefer mariadb-dump, fall back to mysqldump.
*/
function resolveDumpBinary(): string {
const candidates = ["mariadb-dump", "mysqldump"];
for (const bin of candidates) {
try {
const result = Bun.spawnSync(["which", bin], {
stdout: "pipe",
stderr: "pipe",
});
if (result.exitCode === 0) {
return new TextDecoder().decode(result.stdout).trim() || bin;
}
} catch {
// try next
}
}
return "mariadb-dump";
}
export default function () {
return new Command("backup")
.description("Backup Database")
.action(async (opts) => {
.description("Backup MariaDB database (SQL dump)")
.action(async () => {
console.log(`Backing up database ...`);
const config = global.CONFIG;
const { backup_dir } = grabDBDir({ config });
const { backup_dir, db_file_path } = grabDBDir({ config });
if (!fs.existsSync(backup_dir)) {
fs.mkdirSync(backup_dir, { recursive: true });
}
const new_db_file_name = grabDBBackupFileName({ config });
const backup_file_name = grabDBBackupFileName({ config });
const backup_path = path.join(backup_dir, backup_file_name);
fs.cpSync(db_file_path, path.join(backup_dir, new_db_file_name));
const dumpBin = resolveDumpBinary();
const args = [
dumpBin,
...mariadbCliConnectionArgs(),
"--single-transaction",
"--routines",
"--triggers",
"--events",
config.db_name,
];
trimBackups({ config });
try {
const proc = Bun.spawn(args, {
stdout: "pipe",
stderr: "pipe",
env: mariadbCliEnv(),
});
console.log(`${chalk.bold(chalk.green(`DB Backup Success!`))}`);
process.exit();
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
if (exitCode !== 0) {
console.error(
chalk.red(
`Backup failed (exit ${exitCode}): ${stderr || "unknown error"}`,
),
);
process.exit(1);
}
fs.writeFileSync(backup_path, stdout, "utf-8");
trimBackups({ config });
console.log(
`${chalk.bold(chalk.green(`DB Backup Success!`))}${backup_path}`,
);
process.exit(0);
} catch (error: any) {
console.error(
chalk.red(
`Backup ERROR => ${error.message}. Ensure \`mariadb-dump\` or \`mysqldump\` is installed.`,
),
);
process.exit(1);
}
});
}
+70 -12
View File
@@ -6,22 +6,47 @@ import grabSortedBackups from "../utils/grab-sorted-backups";
import { select } from "@inquirer/prompts";
import grabBackupData from "../utils/grab-backup-data";
import path from "path";
import mariadbCliEnv, {
mariadbCliConnectionArgs,
} from "../utils/mariadb-cli-env";
/**
* Prefer mariadb client, fall back to mysql.
*/
function resolveClientBinary(): string {
const candidates = ["mariadb", "mysql"];
for (const bin of candidates) {
try {
const result = Bun.spawnSync(["which", bin], {
stdout: "pipe",
stderr: "pipe",
});
if (result.exitCode === 0) {
return new TextDecoder().decode(result.stdout).trim() || bin;
}
} catch {
// try next
}
}
return "mariadb";
}
export default function () {
return new Command("restore")
.description("Restore Database")
.action(async (opts) => {
console.log(`Restoring up database ...`);
.description("Restore MariaDB database from an SQL dump")
.action(async () => {
console.log(`Restoring database ...`);
const config = global.CONFIG;
const { backup_dir } = grabDBDir({ config });
const { backup_dir, db_file_path } = grabDBDir({ config });
const backups = grabSortedBackups({ config });
const backups = grabSortedBackups({ config }).filter((b) =>
b.endsWith(".sql"),
);
if (!backups?.[0]) {
console.error(
`No Backups to restore. Use the \`backup\` command to create a backup`,
`No SQL backups to restore. Use the \`backup\` command to create a backup.`,
);
process.exit(1);
}
@@ -40,16 +65,49 @@ export default function () {
}),
});
fs.cpSync(path.join(backup_dir, selected_backup), db_file_path);
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 clientBin = resolveClientBinary();
const sql = fs.readFileSync(backup_path, "utf-8");
const args = [
clientBin,
...mariadbCliConnectionArgs(),
config.db_name,
];
const proc = Bun.spawn(args, {
stdin: new Blob([sql]),
stdout: "pipe",
stderr: "pipe",
env: mariadbCliEnv(),
});
const [stderr, exitCode] = await Promise.all([
new Response(proc.stderr).text(),
proc.exited,
]);
if (exitCode !== 0) {
console.error(
chalk.red(
`Restore failed (exit ${exitCode}): ${stderr || "unknown error"}`,
),
);
process.exit(1);
}
console.log(
`${chalk.bold(chalk.green(`DB Restore Success!`))}`,
`${chalk.bold(chalk.green(`DB Restore Success!`))}${selected_backup}`,
);
process.exit();
process.exit(0);
} catch (error: any) {
console.error(`Backup Restore ERROR => ${error.message}`);
process.exit();
process.exit(1);
}
});
}
+6 -16
View File
@@ -1,9 +1,7 @@
import { Command } from "commander";
import { MariaDBSchemaManager } from "../lib/mariadb/db-schema-manager";
import grabDirNames from "../data/grab-dir-names";
import path from "path";
import dbSchemaToTypeDef from "../lib/mariadb/schema-to-typedef";
import _ from "lodash";
import appendDefaultFieldsToDbSchema from "../utils/append-default-fields-to-db-schema";
import chalk from "chalk";
import { writeLiveSchema } from "../functions/live-schema";
@@ -12,10 +10,6 @@ import createDBSchema from "../lib/schema/create-db-schema";
export default function () {
return new Command("schema")
.description("Build DB From Schema")
.option(
"-v, --vector",
"Recreate Vector Tables. This will drop and rebuild all vector tables",
)
.option("-t, --typedef", "Generate typescript type definitions")
.action(async (opts) => {
console.log(`Starting process ...`);
@@ -32,14 +26,10 @@ export default function () {
dbSchema,
});
// const manager = new MariaDBSchemaManager({
// schema: finaldbSchema,
// });
// await manager.syncSchema();
// manager.close();
await createDBSchema({ db_schema: finaldbSchema });
await createDBSchema({
db_schema: finaldbSchema,
config,
});
if (isTypeDef && config.typedef_file_path) {
const out_file = path.resolve(
@@ -59,9 +49,9 @@ export default function () {
console.log(
`${chalk.bold(chalk.green(`DB Schema setup success!`))}`,
);
process.exit();
process.exit(0);
} catch (error) {
console.log(error);
console.error(error);
process.exit(1);
}
});
+13 -15
View File
@@ -7,8 +7,8 @@ import chalk from "chalk";
export default function () {
return new Command("typedef")
.description("Build DB From Schema")
.action(async (opts) => {
.description("Generate TypeScript type definitions from the schema")
.action(async () => {
console.log(`Creating Type Definition From DB Schema ...`);
const config = global.CONFIG;
@@ -18,23 +18,21 @@ export default function () {
const finaldbSchema = appendDefaultFieldsToDbSchema({ dbSchema });
if (config.typedef_file_path) {
const out_file = path.resolve(
ROOT_DIR,
config.typedef_file_path,
if (!config.typedef_file_path) {
console.error(
`\`typedef_file_path\` is required in bun-mariadb.config.ts to generate types.`,
);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
config,
});
} else {
console.error(``);
process.exit(1);
}
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
const out_file = path.resolve(ROOT_DIR, config.typedef_file_path);
dbSchemaToTypeDef({
dbSchema: finaldbSchema,
dst_file: out_file,
config,
});
process.exit();
console.log(`${chalk.bold(chalk.green(`Typedef gen success!`))}`);
process.exit(0);
});
}