Update .gitignore, add dist directory

This commit is contained in:
2026-07-20 21:43:05 +01:00
parent 9f2db66760
commit 3bbf00cdb0
153 changed files with 6280 additions and 1 deletions
+15
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
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,
};
}
}
+7
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 {};
+25
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
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
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,
},
};
}
}
+15
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 {};
+63
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
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
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,
},
};
}
}
+7
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 {};
+44
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
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
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
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
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
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
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,
},
};
}
}
+8
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
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);
}
}
+2
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
export default function buildColumnDefinition(field: BUN_MARIADB_FieldSchemaType): string;
+43
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(" ");
}
+2
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
export default function buildForeignKeyConstraint(field: BUN_MARIADB_FieldSchemaType): string;
+15
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;
}
+2
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_TableSchemaType } from "../../types";
export default function buildTableOptions(table: BUN_MARIADB_TableSchemaType): string;
+7
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(" ")}`;
}
+2
View File
@@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function createDBManagerTable({ db_schema, config, }: CreateDBSchemaParams): Promise<void>;
+15
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
View File
@@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function createDBSchema(params: CreateDBSchemaParams): Promise<void>;
+17
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
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
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 });
}
@@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function getExistingTablesFromTablesManagerTable({ config, }: CreateDBSchemaParams): Promise<(string | undefined)[]>;
@@ -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
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
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,
}));
}
+2
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>;
+62
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 });
}
+2
View File
@@ -0,0 +1,2 @@
import type { CreateDBSchemaParams } from "../../types";
export default function handleDBSchemaTables(params: CreateDBSchemaParams): Promise<void>;
+93
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
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
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
View File
@@ -0,0 +1,2 @@
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
export default function mapDataType(field: BUN_MARIADB_FieldSchemaType): string;
+93
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";
}
}
+1
View File
@@ -0,0 +1 @@
export default function MariaDBQuoteGen(str: string): string;
+3
View File
@@ -0,0 +1,3 @@
export default function MariaDBQuoteGen(str) {
return `\`${str.replace(/`/g, "``")}\``;
}
+2
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
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
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
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
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
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
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
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
View File
@@ -0,0 +1,5 @@
import type { BunMariaDBConfig } from "../../types";
export default function schemaCondition(config?: BunMariaDBConfig): {
where: string;
values: string[];
};
+13
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
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
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
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
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 });
}
}
+9
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>;
+18
View File
@@ -0,0 +1,18 @@
import { AppData } from "../../data/app-data";
import MariaDBQuoteGen from "./mariadb-quote-gen";
import runSchemaQuery from "./run-schema-query";
export default async function upsertDbManagerTable({ tableName, config, }) {
const now = Date.now();
await runSchemaQuery({
query: `INSERT INTO ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} (table_name, created_at, updated_at) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = VALUES(updated_at)`,
values: [tableName, now, now],
config,
});
}
export async function removeDbManagerTable({ tableName, config, }) {
await runSchemaQuery({
query: `DELETE FROM ${MariaDBQuoteGen(AppData["DbSchemaManagerTableName"])} WHERE table_name = ?`,
values: [tableName],
config,
});
}