Update .gitignore, add dist directory
This commit is contained in:
Vendored
+15
@@ -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 {};
|
||||
Vendored
+61
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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(", ")}`;
|
||||
}
|
||||
Vendored
+17
@@ -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 {};
|
||||
Vendored
+68
@@ -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
@@ -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
@@ -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 };
|
||||
}
|
||||
Vendored
+17
@@ -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 {};
|
||||
Vendored
+66
@@ -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
@@ -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
@@ -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];
|
||||
}
|
||||
Vendored
+18
@@ -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 {};
|
||||
Vendored
+94
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Vendored
+11
@@ -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 {};
|
||||
Vendored
+45
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Vendored
+18
@@ -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 {};
|
||||
Vendored
+109
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||
export default function buildColumnDefinition(field: BUN_MARIADB_FieldSchemaType): string;
|
||||
+43
@@ -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(" ");
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||
export default function buildForeignKeyConstraint(field: BUN_MARIADB_FieldSchemaType): string;
|
||||
+15
@@ -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
@@ -0,0 +1,2 @@
|
||||
import type { BUN_MARIADB_TableSchemaType } from "../../types";
|
||||
export default function buildTableOptions(table: BUN_MARIADB_TableSchemaType): string;
|
||||
+7
@@ -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(" ")}`;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import type { CreateDBSchemaParams } from "../../types";
|
||||
export default function createDBManagerTable({ db_schema, config, }: CreateDBSchemaParams): Promise<void>;
|
||||
+15
@@ -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
@@ -0,0 +1,2 @@
|
||||
import type { CreateDBSchemaParams } from "../../types";
|
||||
export default function createDBSchema(params: CreateDBSchemaParams): Promise<void>;
|
||||
Vendored
+17
@@ -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 });
|
||||
}
|
||||
Vendored
+5
@@ -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>;
|
||||
Vendored
+41
@@ -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
@@ -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[]>;
|
||||
Vendored
+15
@@ -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
@@ -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
@@ -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 });
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import type { CreateDBSchemaParams } from "../../types";
|
||||
export default function handleDBSchemaTables(params: CreateDBSchemaParams): Promise<void>;
|
||||
+93
@@ -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 });
|
||||
}
|
||||
}
|
||||
Vendored
+5
@@ -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;
|
||||
Vendored
+8
@@ -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";
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||
export default function mapDataType(field: BUN_MARIADB_FieldSchemaType): string;
|
||||
Vendored
+93
@@ -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
@@ -0,0 +1 @@
|
||||
export default function MariaDBQuoteGen(str: string): string;
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export default function MariaDBQuoteGen(str) {
|
||||
return `\`${str.replace(/`/g, "``")}\``;
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import type { BUN_MARIADB_DatabaseSchemaType, CreateDBSchemaParams } from "../../types";
|
||||
export default function orderDBSchema({ db_schema, }: CreateDBSchemaParams): Promise<BUN_MARIADB_DatabaseSchemaType>;
|
||||
Vendored
+57
@@ -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;
|
||||
}
|
||||
Vendored
+9
@@ -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>;
|
||||
Vendored
+111
@@ -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 });
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -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;
|
||||
Vendored
+34
@@ -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
@@ -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[]>;
|
||||
Vendored
+22
@@ -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
@@ -0,0 +1,5 @@
|
||||
import type { BunMariaDBConfig } from "../../types";
|
||||
export default function schemaCondition(config?: BunMariaDBConfig): {
|
||||
where: string;
|
||||
values: string[];
|
||||
};
|
||||
Vendored
+13
@@ -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: [],
|
||||
};
|
||||
}
|
||||
Vendored
+5
@@ -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>;
|
||||
Vendored
+131
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+5
@@ -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>;
|
||||
Vendored
+177
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user