Compare commits

..
16 Commits
42 changed files with 528 additions and 536 deletions
+2
View File
@@ -34,3 +34,5 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
/test /test
.vscode .vscode
.dump .dump
/.bun-mariadb
+56 -20
View File
@@ -86,7 +86,7 @@ bun add github:moduletrace/bun-mariadb
Connection settings are read from the environment (not the config file): Connection settings are read from the environment (not the config file):
| Variable | Required | Description | | Variable | Required | Description |
| --------------------------------- | -------- | ------------------------------------ | | --------------------------------- | -------- | ---------------------------------- |
| `BUN_MARIADB_SERVER_HOST` | Yes | MariaDB host | | `BUN_MARIADB_SERVER_HOST` | Yes | MariaDB host |
| `BUN_MARIADB_SERVER_USERNAME` | Yes | Database user | | `BUN_MARIADB_SERVER_USERNAME` | Yes | Database user |
| `BUN_MARIADB_SERVER_PASSWORD` | Yes | Database password | | `BUN_MARIADB_SERVER_PASSWORD` | Yes | Database password |
@@ -135,9 +135,22 @@ const schema: BUN_MARIADB_DatabaseSchemaType = {
{ {
tableName: "users", tableName: "users",
fields: [ fields: [
{ fieldName: "first_name", dataType: "VARCHAR", integerLength: 255 }, {
{ fieldName: "last_name", dataType: "VARCHAR", integerLength: 255 }, fieldName: "first_name",
{ fieldName: "email", dataType: "VARCHAR", integerLength: 255, unique: true }, dataType: "VARCHAR",
dataLength: 255,
},
{
fieldName: "last_name",
dataType: "VARCHAR",
dataLength: 255,
},
{
fieldName: "email",
dataType: "VARCHAR",
dataLength: 255,
unique: true,
},
{ fieldName: "bio", dataType: "LONGTEXT", html: true }, { fieldName: "bio", dataType: "LONGTEXT", html: true },
], ],
}, },
@@ -188,7 +201,7 @@ await BunMariaDB.delete({ table: "users", targetId: 1 });
The config file must be named `bun-mariadb.config.ts` and placed at the project root. The config file must be named `bun-mariadb.config.ts` and placed at the project root.
| Field | Type | Required | Description | | Field | Type | Required | Description |
| ------------------- | -------- | -------- | --------------------------------------------------------------------------- | | -------------------- | -------- | -------- | ----------------------------------------------------------------------------- |
| `db_name` | `string` | Yes | MariaDB database name | | `db_name` | `string` | Yes | MariaDB database name |
| `db_dir` | `string` | Yes | Directory for schema, types, and local artifacts (relative to project root) | | `db_dir` | `string` | Yes | Directory for schema, types, and local artifacts (relative to project root) |
| `db_backup_dir` | `string` | No | Backup directory name, relative to `db_dir` (default: `.backups`) | | `db_backup_dir` | `string` | No | Backup directory name, relative to `db_dir` (default: `.backups`) |
@@ -228,7 +241,6 @@ interface BUN_MARIADB_TableSchemaType {
parentTableName?: string; // inherit / merge fields from another table parentTableName?: string; // inherit / merge fields from another table
tableNameOld?: string; // rename: old name triggers ALTER TABLE RENAME tableNameOld?: string; // rename: old name triggers ALTER TABLE RENAME
collation?: "utf8mb4_bin" | "utf8mb4_unicode_520_ci"; collation?: "utf8mb4_bin" | "utf8mb4_unicode_520_ci";
isVector?: boolean; // mark as vector-oriented table
} }
``` ```
@@ -238,12 +250,38 @@ interface BUN_MARIADB_TableSchemaType {
type BUN_MARIADB_FieldSchemaType = { type BUN_MARIADB_FieldSchemaType = {
fieldName?: string; fieldName?: string;
dataType: dataType:
| "CHAR" | "VARCHAR" | "TEXT" | "TINYTEXT" | "MEDIUMTEXT" | "LONGTEXT" | "CHAR"
| "TINYINT" | "SMALLINT" | "MEDIUMINT" | "INT" | "BIGINT" | "VARCHAR"
| "FLOAT" | "DOUBLE" | "DECIMAL" | "TEXT"
| "BINARY" | "VARBINARY" | "BLOB" | "TINYBLOB" | "MEDIUMBLOB" | "LONGBLOB" | "TINYTEXT"
| "DATE" | "TIME" | "DATETIME" | "TIMESTAMP" | "YEAR" | "MEDIUMTEXT"
| "BOOLEAN" | "UUID" | "JSON" | "INET6" | "ENUM" | "SET" | "VECTOR"; | "LONGTEXT"
| "TINYINT"
| "SMALLINT"
| "MEDIUMINT"
| "INT"
| "BIGINT"
| "FLOAT"
| "DOUBLE"
| "DECIMAL"
| "BINARY"
| "VARBINARY"
| "BLOB"
| "TINYBLOB"
| "MEDIUMBLOB"
| "LONGBLOB"
| "DATE"
| "TIME"
| "DATETIME"
| "TIMESTAMP"
| "YEAR"
| "BOOLEAN"
| "UUID"
| "JSON"
| "INET6"
| "ENUM"
| "SET"
| "VECTOR";
primaryKey?: boolean; primaryKey?: boolean;
autoIncrement?: boolean; autoIncrement?: boolean;
notNullValue?: boolean; notNullValue?: boolean;
@@ -253,7 +291,7 @@ type BUN_MARIADB_FieldSchemaType = {
onUpdate?: string; onUpdate?: string;
onUpdateLiteral?: string; onUpdateLiteral?: string;
foreignKey?: BUN_MARIADB_ForeignKeyType; foreignKey?: BUN_MARIADB_ForeignKeyType;
integerLength?: string | number; // e.g. VARCHAR length dataLength?: string | number; // e.g. VARCHAR length
decimals?: string | number; // DECIMAL scale decimals?: string | number; // DECIMAL scale
options?: (string | number)[]; // ENUM / SET values options?: (string | number)[]; // ENUM / SET values
isVector?: boolean; // native VECTOR column isVector?: boolean; // native VECTOR column
@@ -369,7 +407,7 @@ bunx bun-mariadb export [options]
``` ```
| Option | Description | | Option | Description |
| ------------------- | --------------------------------------------------------------------------- | | ---------------- | -------------------------------------------------------------------------- |
| `-o`, `--output` | Output archive path (`.tar.gz` or `.zip`). Defaults to `{db_dir}/.exports` | | `-o`, `--output` | Output archive path (`.tar.gz` or `.zip`). Defaults to `{db_dir}/.exports` |
| `-f`, `--format` | When `--output` is omitted: `tar.gz` (default) or `zip` | | `-f`, `--format` | When `--output` is omitted: `tar.gz` (default) or `zip` |
@@ -401,7 +439,7 @@ bunx bun-mariadb import [file] [options]
``` ```
| Option | Description | | Option | Description |
| ---------------- | --------------------------------------------------------------------------- | | --------------- | --------------------------------------------------------------------- |
| `[file]` | Path to a `.sql` dump or export archive (`.tar.gz` / `.tar` / `.zip`) | | `[file]` | Path to a `.sql` dump or export archive (`.tar.gz` / `.tar` / `.zip`) |
| `--sql-only` | Archive only: restore SQL, do not overwrite `schema.ts` | | `--sql-only` | Archive only: restore SQL, do not overwrite `schema.ts` |
| `--schema-only` | Archive only: write `schema.ts`, do not restore SQL | | `--schema-only` | Archive only: write `schema.ts`, do not restore SQL |
@@ -642,7 +680,7 @@ type ServerQueryParam<T> = {
### Equality Operators ### Equality Operators
| Equality | SQL Equivalent | | Equality | SQL Equivalent |
| ----------------------- | ------------------------------------------------------ | | ----------------------- | ----------------------------- |
| `EQUAL` (default) | `=` | | `EQUAL` (default) | `=` |
| `NOT EQUAL` | `!=` | | `NOT EQUAL` | `!=` |
| `LIKE` | `LIKE '%value%'` | | `LIKE` | `LIKE '%value%'` |
@@ -704,7 +742,6 @@ MariaDB native `VECTOR(n)` columns and `VECTOR INDEX` are supported (MariaDB 11.
```ts ```ts
{ {
tableName: "documents", tableName: "documents",
isVector: true,
fields: [ fields: [
{ {
fieldName: "embedding", fieldName: "embedding",
@@ -715,7 +752,7 @@ MariaDB native `VECTOR(n)` columns and `VECTOR INDEX` are supported (MariaDB 11.
{ {
fieldName: "title", fieldName: "title",
dataType: "VARCHAR", dataType: "VARCHAR",
integerLength: 255, dataLength: 255,
}, },
], ],
indexes: [ indexes: [
@@ -826,7 +863,7 @@ const res = await BunMariaDB.select<BUN_MARIADB_MY_APP_USERS>({
Every table automatically receives: Every table automatically receives:
| Field | Type | Description | | Field | Type | Description |
| ------------ | ------------------------------- | -------------------------------------- | | ------------ | -------------------------------------------- | -------------------------------------- |
| `id` | `BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL` | Unique row identifier | | `id` | `BIGINT PRIMARY KEY AUTO_INCREMENT NOT NULL` | Unique row identifier |
| `created_at` | `BIGINT` | Unix timestamp set on insert | | `created_at` | `BIGINT` | Unix timestamp set on insert |
| `updated_at` | `BIGINT` | Unix timestamp updated on every update | | `updated_at` | `BIGINT` | Unix timestamp updated on every update |
@@ -865,7 +902,6 @@ bun-mariadb/
│ │ ├── create-db-schema.ts │ │ ├── create-db-schema.ts
│ │ ├── create-table.ts │ │ ├── create-table.ts
│ │ ├── update-table.ts │ │ ├── update-table.ts
│ │ ├── recreate-table.ts
│ │ ├── sync-indexes.ts │ │ ├── sync-indexes.ts
│ │ └── ... │ │ └── ...
│ ├── types/ │ ├── types/
+2 -1
View File
@@ -17,4 +17,5 @@ declare const BunMariaDB: {
}; };
}; };
export default BunMariaDB; export default BunMariaDB;
export type { BunMariaDBConfig, BUN_MARIADB_DatabaseSchemaType, BUN_MARIADB_TableSchemaType, BUN_MARIADB_FieldSchemaType, BUN_MARIADB_IndexSchemaType, BUN_MARIADB_UniqueConstraintSchemaType, BUN_MARIADB_ForeignKeyType, DBResponseObject, ServerQueryParam, } from "./types"; export type * from "./types";
export { UsersOmitedFields, MariaDBCollations, MariaDBCharsets, TextFieldTypesArray, BUN_MARIADB_DATATYPES, MariaDBIndexTypes, ServerQueryOperators, ServerQueryEqualities, SQlComparisons, DataCrudRequestMethods, DataCrudRequestMethodsLowerCase, DsqlCrudActions, QueryFields, DockerComposeServices, IndexTypes, DefaultFields, RequiredENVs, } from "./types";
+1
View File
@@ -19,3 +19,4 @@ const BunMariaDB = {
}, },
}; };
export default BunMariaDB; export default BunMariaDB;
export { UsersOmitedFields, MariaDBCollations, MariaDBCharsets, TextFieldTypesArray, BUN_MARIADB_DATATYPES, MariaDBIndexTypes, ServerQueryOperators, ServerQueryEqualities, SQlComparisons, DataCrudRequestMethods, DataCrudRequestMethodsLowerCase, DsqlCrudActions, QueryFields, DockerComposeServices, IndexTypes, DefaultFields, RequiredENVs, } from "./types";
+1
View File
@@ -49,6 +49,7 @@ export default async function dbHandler({ query, values, config }) {
single_res: res_array?.[0], single_res: res_array?.[0],
insert_return, insert_return,
count, count,
db_res: res,
}; };
} }
catch (error) { catch (error) {
+3 -2
View File
@@ -12,7 +12,7 @@ export default async function DbDelete({ table, query, targetId, config, }) {
finalQuery = _.merge(finalQuery, { finalQuery = _.merge(finalQuery, {
query: { query: {
id: { id: {
value: String(targetId), value: Number(targetId),
}, },
}, },
}); });
@@ -41,8 +41,9 @@ export default async function DbDelete({ table, query, targetId, config, }) {
}); });
if (!res.success) { if (!res.success) {
return { return {
success: false,
msg: "Database delete failed", msg: "Database delete failed",
...res,
success: false,
debug: { debug: {
sqlObj, sqlObj,
}, },
+2 -1
View File
@@ -38,8 +38,9 @@ export default async function DbInsert({ table, data, update_on_duplicate, confi
}); });
if (!res.success) { if (!res.success) {
return { return {
success: false,
msg: "Database insert failed", msg: "Database insert failed",
...res,
success: false,
debug: { debug: {
sqlObj, sqlObj,
}, },
+3 -2
View File
@@ -12,7 +12,7 @@ export default async function DbSelect({ table, query, count, targetId, config,
finalQuery = _.merge(finalQuery, { finalQuery = _.merge(finalQuery, {
query: { query: {
id: { id: {
value: String(targetId), value: Number(targetId),
}, },
}, },
}); });
@@ -28,8 +28,9 @@ export default async function DbSelect({ table, query, count, targetId, config,
}); });
if (!res.success) { if (!res.success) {
return { return {
success: false,
msg: "Database select failed", msg: "Database select failed",
...res,
success: false,
debug: { debug: {
sqlObj, sqlObj,
sql: sqlObj.string, sql: sqlObj.string,
+6 -2
View File
@@ -9,8 +9,9 @@ export default async function DbSQL({ sql, values }) {
}); });
if (!res.success) { if (!res.success) {
return { return {
success: false,
msg: "Database query failed", msg: "Database query failed",
...res,
success: false,
debug: { debug: {
sqlObj: { sqlObj: {
sql: trimmedSql, sql: trimmedSql,
@@ -24,7 +25,10 @@ export default async function DbSQL({ sql, values }) {
const single_res = isSelect ? payload?.[0] : res.single_res; const single_res = isSelect ? payload?.[0] : res.single_res;
const singleRaw = res.single_res; const singleRaw = res.single_res;
return { return {
success: true, ...res,
success: isSelect
? Boolean(single_res) || Boolean(payload?.[0])
: true,
payload, payload,
single_res, single_res,
debug: { debug: {
+7 -22
View File
@@ -13,7 +13,7 @@ export default async function DbUpdate({ table, data, query, targetId, config, }
finalQuery = _.merge(finalQuery, { finalQuery = _.merge(finalQuery, {
query: { query: {
id: { id: {
value: String(targetId), value: Number(targetId),
}, },
}, },
}); });
@@ -60,41 +60,26 @@ export default async function DbUpdate({ table, data, query, targetId, config, }
values: values, values: values,
config, config,
}); });
if (res.error) {
return res;
}
sqlObj.string = sql; sqlObj.string = sql;
sqlObj.values = values; sqlObj.values = values;
let updated_sql = ``; let updated_sql = ``;
let updated_sql_values = []; let updated_sql_values = [];
updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`; updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`;
updated_sql_values = [...updated_sql_values, ...sqlQueryObj.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({ const updated_res = await dbHandler({
query: updated_sql, query: updated_sql,
values: updated_sql_values, values: updated_sql_values,
config, config,
}); });
const affected_rows = updated_res.payload?.length;
return { return {
...res, ...updated_res,
success: Boolean(affected_rows),
insert_return: {
affected_rows,
},
debug: { debug: {
sqlObj, sqlObj,
}, },
db_res: res.db_res,
}; };
} }
catch (error) { catch (error) {
+1 -1
View File
@@ -15,7 +15,7 @@ export default async function createTable({ table, config, }) {
if (field.primaryKey && field.fieldName) { if (field.primaryKey && field.fieldName) {
primaryKeys.push(field.fieldName); primaryKeys.push(field.fieldName);
} }
if (field.foreignKey && !table.isVector) { if (field.foreignKey) {
foreignKeys.push(buildForeignKeyConstraint(field, table.tableName)); foreignKeys.push(buildForeignKeyConstraint(field, table.tableName));
} }
} }
+13
View File
@@ -0,0 +1,13 @@
import type { BunMariaDBConfig } from "../../types";
export type ColumnInfoRow = {
name: string;
type: string;
comment?: string;
isNullable: boolean;
columnDefault: string | null;
extra: string;
};
export default function getTableColumnsGemini({ tableName, config, }: {
tableName: string;
config?: BunMariaDBConfig;
}): Promise<ColumnInfoRow[]>;
+45
View File
@@ -0,0 +1,45 @@
import { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
export default async function getTableColumnsGemini({ tableName, config, }) {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows({
query: `
SELECT
c.COLUMN_NAME,
c.COLUMN_TYPE,
c.COLUMN_COMMENT,
c.IS_NULLABLE,
c.COLUMN_DEFAULT,
c.EXTRA,
EXISTS (
SELECT 1
FROM information_schema.CHECK_CONSTRAINTS cc
WHERE cc.CONSTRAINT_SCHEMA = c.TABLE_SCHEMA
AND cc.TABLE_NAME = c.TABLE_NAME
AND cc.CHECK_CLAUSE LIKE CONCAT('%json_valid(\`', c.COLUMN_NAME, '\`)%')
) AS IS_JSON
FROM information_schema.COLUMNS c
WHERE ${schemaCond.where.replace(/\bTABLE_SCHEMA\b/g, "c.TABLE_SCHEMA")}
AND c.TABLE_NAME = ?
ORDER BY c.ORDINAL_POSITION
`,
values: [...schemaCond.values, tableName],
config,
});
return rows.map((row) => {
const isJson = Number(row.IS_JSON) === 1;
// Normalize longtext with a json_valid constraint to "json"
let resolvedType = row.COLUMN_TYPE;
if (isJson && row.COLUMN_TYPE.toLowerCase() === "longtext") {
resolvedType = "json";
}
return {
name: row.COLUMN_NAME,
type: resolvedType,
comment: row.COLUMN_COMMENT,
isNullable: String(row.IS_NULLABLE).toUpperCase() === "YES",
columnDefault: row.COLUMN_DEFAULT,
extra: row.EXTRA || "",
};
});
}
+16 -17
View File
@@ -7,9 +7,9 @@ export default function mapDataType(field) {
} }
switch (dataType) { switch (dataType) {
case "CHAR": case "CHAR":
return `CHAR(${field.integerLength || 255})`; return `CHAR(${field.dataLength || 255})`;
case "VARCHAR": case "VARCHAR":
return `VARCHAR(${field.integerLength || 255})`; return `VARCHAR(${field.dataLength || 255})`;
case "TEXT": case "TEXT":
return "TEXT"; return "TEXT";
case "TINYTEXT": case "TINYTEXT":
@@ -19,36 +19,34 @@ export default function mapDataType(field) {
case "LONGTEXT": case "LONGTEXT":
return "LONGTEXT"; return "LONGTEXT";
case "TINYINT": case "TINYINT":
return field.integerLength return field.dataLength
? `TINYINT(${field.integerLength})` ? `TINYINT(${field.dataLength})`
: "TINYINT"; : "TINYINT";
case "SMALLINT": case "SMALLINT":
return field.integerLength return field.dataLength
? `SMALLINT(${field.integerLength})` ? `SMALLINT(${field.dataLength})`
: "SMALLINT"; : "SMALLINT";
case "MEDIUMINT": case "MEDIUMINT":
return field.integerLength return field.dataLength
? `MEDIUMINT(${field.integerLength})` ? `MEDIUMINT(${field.dataLength})`
: "MEDIUMINT"; : "MEDIUMINT";
case "INT": case "INT":
return field.integerLength ? `INT(${field.integerLength})` : "INT"; return field.dataLength ? `INT(${field.dataLength})` : "INT";
case "BIGINT": case "BIGINT":
return field.integerLength return field.dataLength ? `BIGINT(${field.dataLength})` : "BIGINT";
? `BIGINT(${field.integerLength})`
: "BIGINT";
case "FLOAT": case "FLOAT":
return "FLOAT"; return "FLOAT";
case "DOUBLE": case "DOUBLE":
return "DOUBLE"; return "DOUBLE";
case "DECIMAL": case "DECIMAL":
if (field.integerLength && field.decimals) { if (field.dataLength && field.decimals) {
return `DECIMAL(${field.integerLength}, ${field.decimals})`; return `DECIMAL(${field.dataLength}, ${field.decimals})`;
} }
return "DECIMAL(10,2)"; return "DECIMAL(10,2)";
case "BINARY": case "BINARY":
return `BINARY(${field.integerLength || 1})`; return `BINARY(${field.dataLength || 1})`;
case "VARBINARY": case "VARBINARY":
return `VARBINARY(${field.integerLength || 255})`; return `VARBINARY(${field.dataLength || 255})`;
case "BLOB": case "BLOB":
return "BLOB"; return "BLOB";
case "TINYBLOB": case "TINYBLOB":
@@ -68,11 +66,12 @@ export default function mapDataType(field) {
case "YEAR": case "YEAR":
return "YEAR"; return "YEAR";
case "UUID": case "UUID":
return "CHAR(36)"; // MariaDB does not have a native UUID type return "UUID";
case "JSON": case "JSON":
return "JSON"; return "JSON";
case "INET6": case "INET6":
return "INET6"; return "INET6";
case "BOOL":
case "BOOLEAN": case "BOOLEAN":
return "TINYINT(1)"; return "TINYINT(1)";
case "ENUM": { case "ENUM": {
-9
View File
@@ -1,9 +0,0 @@
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
@@ -1,111 +0,0 @@
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 });
}
}
-1
View File
@@ -23,7 +23,6 @@ export default function resolveTable(table, db_schema) {
tableName: table.tableName, tableName: table.tableName,
tableDescription: table.tableDescription || parentTable.tableDescription, tableDescription: table.tableDescription || parentTable.tableDescription,
collation: table.collation || parentTable.collation, collation: table.collation || parentTable.collation,
isVector: table.isVector !== undefined ? table.isVector : parentTable.isVector,
fields: Array.from(mergedFieldsMap.values()), fields: Array.from(mergedFieldsMap.values()),
indexes: _.uniqBy([...(parentTable.indexes || []), ...(table.indexes || [])], "indexName"), indexes: _.uniqBy([...(parentTable.indexes || []), ...(table.indexes || [])], "indexName"),
uniqueConstraints: [ uniqueConstraints: [
-3
View File
@@ -15,9 +15,6 @@ function rulesMatch(live, desired) {
ruleIsCascade(live.updateRule) === desired.cascadeUpdate); ruleIsCascade(live.updateRule) === desired.cascadeUpdate);
} }
export function grabDesiredForeignKeys(table) { export function grabDesiredForeignKeys(table) {
if (table.isVector) {
return [];
}
const desired = []; const desired = [];
for (const field of table.fields || []) { for (const field of table.fields || []) {
const fk = field.foreignKey; const fk = field.foreignKey;
-2
View File
@@ -6,8 +6,6 @@ import { grabDesiredUniqueConstraints } from "./sync-unique-constraints";
function isVectorIndexDef(index, table) { function isVectorIndexDef(index, table) {
if (index.indexType === "VECTOR") if (index.indexType === "VECTOR")
return true; return true;
if (table.isVector)
return true;
const firstFieldName = index.indexTableFields?.[0]; const firstFieldName = index.indexTableFields?.[0];
if (!firstFieldName) if (!firstFieldName)
return false; return false;
+62 -21
View File
@@ -1,10 +1,10 @@
import buildColumnDefinition, { fieldRequiresNotNull, } from "./build-column-definition"; import buildColumnDefinition, { fieldRequiresNotNull, } from "./build-column-definition";
import createTable from "./create-table"; import createTable from "./create-table";
import getTableColumns, {} from "./get-table-columns"; import getTableColumns, {} from "./get-table-columns";
import getTableColumnsGemini from "./get-table-columns-gemnini";
import isVectorField from "./is-vector-field"; import isVectorField from "./is-vector-field";
import mapDataType from "./map-data-types"; import mapDataType from "./map-data-types";
import MariaDBQuoteGen from "./mariadb-quote-gen"; import MariaDBQuoteGen from "./mariadb-quote-gen";
import recreateTable from "./recreate-table";
import runSchemaQuery, { querySchemaRows } from "./run-schema-query"; import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition"; import schemaCondition from "./schema-condition";
import { dropForeignKeysOnColumns } from "./sync-foreign-keys"; import { dropForeignKeysOnColumns } from "./sync-foreign-keys";
@@ -111,7 +111,9 @@ function columnAttributesDiverged(live, field) {
if (wantsOnUpdate && hasOnUpdate) { if (wantsOnUpdate && hasOnUpdate) {
const liveOnUpdate = normalizeDefault((live.extra.match(/on update\s+(.+)/i) || [])[1] || ""); const liveOnUpdate = normalizeDefault((live.extra.match(/on update\s+(.+)/i) || [])[1] || "");
const expectedOu = normalizeDefault(wantsOnUpdate); const expectedOu = normalizeDefault(wantsOnUpdate);
if (liveOnUpdate && expectedOu && !defaultsMatch(liveOnUpdate, expectedOu)) { if (liveOnUpdate &&
expectedOu &&
!defaultsMatch(liveOnUpdate, expectedOu)) {
return true; return true;
} }
} }
@@ -141,8 +143,52 @@ async function dropColumn({ tableName, fieldName, config, }) {
config, config,
}); });
} }
/**
* Drop + re-add a column (values discarded). Used when VECTOR dimensions change —
* MODIFY cannot resize VECTOR, and a full table rebuild is unnecessary.
*/
async function recreateColumn({ tableName, field, config, }) {
if (!field.fieldName)
return;
console.log(`Recreating column: ${tableName}.${field.fieldName} (values will be cleared)`);
await dropForeignKeysOnColumns({
tableName,
columns: [field.fieldName],
config,
});
const schemaCond = schemaCondition(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, tableName],
config,
});
const indexesToDrop = new Set();
for (const row of indexRows) {
if (row.COLUMN_NAME === field.fieldName) {
indexesToDrop.add(row.INDEX_NAME);
}
}
for (const indexName of indexesToDrop) {
console.log(`Dropping index ${indexName} because column ${field.fieldName} is being recreated`);
try {
await runSchemaQuery({
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(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;
}
}
await dropColumn({ tableName, fieldName: field.fieldName, config });
await addColumn({ tableName, field, config });
}
export default async function updateTable({ table, config, }) { export default async function updateTable({ table, config, }) {
const existingColumns = await getTableColumns({ const existingColumns = await getTableColumnsGemini({
tableName: table.tableName, tableName: table.tableName,
config, config,
}); });
@@ -154,8 +200,8 @@ export default async function updateTable({ table, config, }) {
const codeFieldsMap = new Map((table.fields || []).map((f) => [f.fieldName, f])); const codeFieldsMap = new Map((table.fields || []).map((f) => [f.fieldName, f]));
const fieldsToAdd = []; const fieldsToAdd = [];
const fieldsToModify = []; const fieldsToModify = [];
const fieldsToRecreate = [];
const fieldsToDrop = []; const fieldsToDrop = [];
let needsVectorRecreate = false;
for (const field of table.fields || []) { for (const field of table.fields || []) {
if (!field.fieldName) if (!field.fieldName)
continue; continue;
@@ -164,11 +210,14 @@ export default async function updateTable({ table, config, }) {
fieldsToAdd.push(field); fieldsToAdd.push(field);
} }
else { else {
let typeDiverged = !columnTypesMatch(liveField.type, mapDataType(field)); let mapped_data_type = mapDataType(field);
let typeDiverged = !columnTypesMatch(liveField.type, mapped_data_type);
if (isVectorField(field)) { if (isVectorField(field)) {
typeDiverged = vectorTypeDiverged(liveField.type, liveField.comment || "", field); typeDiverged = vectorTypeDiverged(liveField.type, liveField.comment || "", field);
if (typeDiverged) { if (typeDiverged) {
needsVectorRecreate = true; // VECTOR dimensions / storage cannot be MODIFYed — drop + re-add column
fieldsToRecreate.push(field);
continue;
} }
} }
const attrsDiverged = !typeDiverged && columnAttributesDiverged(liveField, field); const attrsDiverged = !typeDiverged && columnAttributesDiverged(liveField, field);
@@ -177,12 +226,6 @@ export default async function updateTable({ table, config, }) {
} }
} }
} }
// 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) { for (const col of existingColumns) {
if (!codeFieldsMap.has(col.name)) { if (!codeFieldsMap.has(col.name)) {
fieldsToDrop.push(col.name); fieldsToDrop.push(col.name);
@@ -190,6 +233,7 @@ export default async function updateTable({ table, config, }) {
} }
if (fieldsToAdd.length === 0 && if (fieldsToAdd.length === 0 &&
fieldsToModify.length === 0 && fieldsToModify.length === 0 &&
fieldsToRecreate.length === 0 &&
fieldsToDrop.length === 0) { fieldsToDrop.length === 0) {
return; return;
} }
@@ -257,17 +301,14 @@ export default async function updateTable({ table, config, }) {
await addColumn({ tableName: table.tableName, field, config }); await addColumn({ tableName: table.tableName, field, config });
} }
for (const field of fieldsToModify) { for (const field of fieldsToModify) {
try {
await modifyColumn({ tableName: table.tableName, field, config }); await modifyColumn({ tableName: table.tableName, field, config });
} }
catch (err) { for (const field of fieldsToRecreate) {
if (isVectorField(field)) { await recreateColumn({
console.warn(`[Vector Resize] Re-aligning dimension spaces natively via safe migration schema rebuild.`); tableName: table.tableName,
await recreateTable({ table, config }); field,
return; config,
} });
throw err;
}
} }
for (const fieldName of fieldsToDrop) { for (const fieldName of fieldsToDrop) {
await dropColumn({ tableName: table.tableName, fieldName, config }); await dropColumn({ tableName: table.tableName, fieldName, config });
+6 -6
View File
@@ -74,10 +74,6 @@ export interface BUN_MARIADB_TableSchemaType {
*/ */
childTableDbId?: string | number; childTableDbId?: string | number;
collation?: (typeof MariaDBCollations)[number]; collation?: (typeof MariaDBCollations)[number];
/**
* If this is a vector-oriented table (native MariaDB VECTOR columns/indexes)
*/
isVector?: boolean;
} }
/** /**
* Reference object used to link a table to one of its child tables. * Reference object used to link a table to one of its child tables.
@@ -213,7 +209,10 @@ export type BUN_MARIADB_FieldSchemaType = {
onDelete?: string; onDelete?: string;
onDeleteLiteral?: string; onDeleteLiteral?: string;
cssFiles?: string[]; cssFiles?: string[];
integerLength?: string | number; /**
* Datatype length. Eg 255 for VARCHAR
*/
dataLength?: string | number;
decimals?: string | number; decimals?: string | number;
code?: boolean; code?: boolean;
options?: (string | number)[]; options?: (string | number)[];
@@ -830,7 +829,7 @@ export type ServerQueryParamsJoin<Table extends string = string, Field extends o
joinType: "INNER JOIN" | "JOIN" | "LEFT JOIN" | "RIGHT JOIN"; joinType: "INNER JOIN" | "JOIN" | "LEFT JOIN" | "RIGHT JOIN";
alias?: string; alias?: string;
tableName: Table; tableName: Table;
match?: ServerQueryParamsJoinMatchObject<Field> | ServerQueryParamsJoinMatchObject<Field>[]; match?: ServerQueryParamsJoinMatchObject<Field> | (ServerQueryParamsJoinMatchObject<Field> | undefined)[];
selectFields?: (keyof Field | SelectFieldObject<Field>)[]; selectFields?: (keyof Field | SelectFieldObject<Field>)[];
omitFields?: (keyof Field | { omitFields?: (keyof Field | {
field: keyof Field; field: keyof Field;
@@ -1411,6 +1410,7 @@ export type DBResponseObject<T extends {
msg?: string; msg?: string;
debug?: any; debug?: any;
count?: number; count?: number;
db_res?: any;
}; };
export type DBInsertReturn = { export type DBInsertReturn = {
count?: number; count?: number;
+2 -1
View File
@@ -1,4 +1,4 @@
import { isUndefined } from "lodash"; import _, { isUndefined } from "lodash";
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str"; import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
import sqlGenGenJoinStr from "./sql-generator-gen-join-str"; import sqlGenGenJoinStr from "./sql-generator-gen-join-str";
import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql"; import sqlGenGrabSelectFieldSQL from "./sql-generator-grab-select-field-sql";
@@ -162,6 +162,7 @@ export default function sqlGenGenQueryStr(params) {
if (Array.isArray(join.match)) { if (Array.isArray(join.match)) {
return ("(" + return ("(" +
join.match join.match
.filter((mtch) => !_.isUndefined(mtch))
.map((mtch) => { .map((mtch) => {
const { str, values } = sqlGenGenJoinStr({ const { str, values } = sqlGenGenJoinStr({
mtch, mtch,
+1 -1
View File
@@ -31,7 +31,7 @@ export default function sqlInsertGenerator({ tableName, data, dbFullName, }) {
: value : value
? value ? value
: null; : null;
if (!finalValue) { if (!finalValue && typeof value !== "number") {
queryValues.push(null); queryValues.push(null);
return "?"; return "?";
} }
+6 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@moduletrace/bun-mariadb", "name": "@moduletrace/bun-mariadb",
"version": "1.0.3", "version": "1.0.19",
"description": "Schema-driven MariaDB manager for Bun", "description": "Schema-driven MariaDB manager for Bun",
"author": "Benjamin Toby", "author": "Benjamin Toby",
"license": "MIT", "license": "MIT",
@@ -12,6 +12,11 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js", "import": "./dist/index.js",
"default": "./dist/index.js" "default": "./dist/index.js"
},
"./types": {
"types": "./dist/types/index.d.ts",
"import": "./dist/types/index.js",
"default": "./dist/types/index.js"
} }
}, },
"bin": { "bin": {
+19 -10
View File
@@ -23,14 +23,23 @@ const BunMariaDB = {
export default BunMariaDB; export default BunMariaDB;
export type { export type * from "./types";
BunMariaDBConfig, export {
BUN_MARIADB_DatabaseSchemaType, UsersOmitedFields,
BUN_MARIADB_TableSchemaType, MariaDBCollations,
BUN_MARIADB_FieldSchemaType, MariaDBCharsets,
BUN_MARIADB_IndexSchemaType, TextFieldTypesArray,
BUN_MARIADB_UniqueConstraintSchemaType, BUN_MARIADB_DATATYPES,
BUN_MARIADB_ForeignKeyType, MariaDBIndexTypes,
DBResponseObject, ServerQueryOperators,
ServerQueryParam, ServerQueryEqualities,
SQlComparisons,
DataCrudRequestMethods,
DataCrudRequestMethodsLowerCase,
DsqlCrudActions,
QueryFields,
DockerComposeServices,
IndexTypes,
DefaultFields,
RequiredENVs,
} from "./types"; } from "./types";
+1
View File
@@ -75,6 +75,7 @@ export default async function dbHandler<
single_res: res_array?.[0], single_res: res_array?.[0],
insert_return, insert_return,
count, count,
db_res: res,
}; };
} catch (error: any) { } catch (error: any) {
return { return {
+3 -2
View File
@@ -41,7 +41,7 @@ export default async function DbDelete<
{ {
query: { query: {
id: { id: {
value: String(targetId), value: Number(targetId),
}, },
}, },
}, },
@@ -77,8 +77,9 @@ export default async function DbDelete<
if (!res.success) { if (!res.success) {
return { return {
success: false,
msg: "Database delete failed", msg: "Database delete failed",
...res,
success: false,
debug: { debug: {
sqlObj, sqlObj,
}, },
+2 -1
View File
@@ -71,8 +71,9 @@ export default async function DbInsert<
if (!res.success) { if (!res.success) {
return { return {
success: false,
msg: "Database insert failed", msg: "Database insert failed",
...res,
success: false,
debug: { debug: {
sqlObj, sqlObj,
}, },
+3 -2
View File
@@ -43,7 +43,7 @@ export default async function DbSelect<
{ {
query: { query: {
id: { id: {
value: String(targetId), value: Number(targetId),
}, },
}, },
}, },
@@ -63,8 +63,9 @@ export default async function DbSelect<
if (!res.success) { if (!res.success) {
return { return {
success: false,
msg: "Database select failed", msg: "Database select failed",
...res,
success: false,
debug: { debug: {
sqlObj, sqlObj,
sql: sqlObj.string, sql: sqlObj.string,
+6 -2
View File
@@ -20,8 +20,9 @@ export default async function DbSQL<
if (!res.success) { if (!res.success) {
return { return {
success: false,
msg: "Database query failed", msg: "Database query failed",
...res,
success: false,
debug: { debug: {
sqlObj: { sqlObj: {
sql: trimmedSql, sql: trimmedSql,
@@ -37,7 +38,10 @@ export default async function DbSQL<
const singleRaw = res.single_res as any; const singleRaw = res.single_res as any;
return { return {
success: true, ...res,
success: isSelect
? Boolean(single_res) || Boolean(payload?.[0])
: true,
payload, payload,
single_res, single_res,
debug: { debug: {
+8 -24
View File
@@ -45,7 +45,7 @@ export default async function DbUpdate<
{ {
query: { query: {
id: { id: {
value: String(targetId), value: Number(targetId),
}, },
}, },
}, },
@@ -106,6 +106,10 @@ export default async function DbUpdate<
config, config,
}); });
if (res.error) {
return res;
}
sqlObj.string = sql; sqlObj.string = sql;
sqlObj.values = values as any[]; sqlObj.values = values as any[];
@@ -113,22 +117,7 @@ export default async function DbUpdate<
let updated_sql_values: any[] = []; let updated_sql_values: any[] = [];
updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`; updated_sql += `SELECT * FROM ${quoteIdentifier(table)} ${whereClause}`;
updated_sql_values = [...updated_sql_values, ...sqlQueryObj.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({ const updated_res = await dbHandler({
query: updated_sql, query: updated_sql,
@@ -136,17 +125,12 @@ export default async function DbUpdate<
config, config,
}); });
const affected_rows = updated_res.payload?.length;
return { return {
...res, ...updated_res,
success: Boolean(affected_rows),
insert_return: {
affected_rows,
},
debug: { debug: {
sqlObj, sqlObj,
}, },
db_res: res.db_res,
}; };
} catch (error: any) { } catch (error: any) {
return { return {
+1 -1
View File
@@ -30,7 +30,7 @@ export default async function createTable({
primaryKeys.push(field.fieldName); primaryKeys.push(field.fieldName);
} }
if (field.foreignKey && !table.isVector) { if (field.foreignKey) {
foreignKeys.push( foreignKeys.push(
buildForeignKeyConstraint(field, table.tableName), buildForeignKeyConstraint(field, table.tableName),
); );
@@ -0,0 +1,73 @@
import type { BunMariaDBConfig } from "../../types";
import { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition";
export type ColumnInfoRow = {
name: string;
type: string;
comment?: string;
isNullable: boolean;
columnDefault: string | null;
extra: string;
};
export default async function getTableColumnsGemini({
tableName,
config,
}: {
tableName: string;
config?: BunMariaDBConfig;
}): Promise<ColumnInfoRow[]> {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows<{
COLUMN_NAME: string;
COLUMN_TYPE: string;
COLUMN_COMMENT: string;
IS_NULLABLE: string;
COLUMN_DEFAULT: string | null;
EXTRA: string;
IS_JSON: number | boolean;
}>({
query: `
SELECT
c.COLUMN_NAME,
c.COLUMN_TYPE,
c.COLUMN_COMMENT,
c.IS_NULLABLE,
c.COLUMN_DEFAULT,
c.EXTRA,
EXISTS (
SELECT 1
FROM information_schema.CHECK_CONSTRAINTS cc
WHERE cc.CONSTRAINT_SCHEMA = c.TABLE_SCHEMA
AND cc.TABLE_NAME = c.TABLE_NAME
AND cc.CHECK_CLAUSE LIKE CONCAT('%json_valid(\`', c.COLUMN_NAME, '\`)%')
) AS IS_JSON
FROM information_schema.COLUMNS c
WHERE ${schemaCond.where.replace(/\bTABLE_SCHEMA\b/g, "c.TABLE_SCHEMA")}
AND c.TABLE_NAME = ?
ORDER BY c.ORDINAL_POSITION
`,
values: [...schemaCond.values, tableName],
config,
});
return rows.map((row) => {
const isJson = Number(row.IS_JSON) === 1;
// Normalize longtext with a json_valid constraint to "json"
let resolvedType = row.COLUMN_TYPE;
if (isJson && row.COLUMN_TYPE.toLowerCase() === "longtext") {
resolvedType = "json";
}
return {
name: row.COLUMN_NAME,
type: resolvedType,
comment: row.COLUMN_COMMENT,
isNullable: String(row.IS_NULLABLE).toUpperCase() === "YES",
columnDefault: row.COLUMN_DEFAULT,
extra: row.EXTRA || "",
};
});
}
+16 -17
View File
@@ -13,9 +13,9 @@ export default function mapDataType(
switch (dataType) { switch (dataType) {
case "CHAR": case "CHAR":
return `CHAR(${field.integerLength || 255})`; return `CHAR(${field.dataLength || 255})`;
case "VARCHAR": case "VARCHAR":
return `VARCHAR(${field.integerLength || 255})`; return `VARCHAR(${field.dataLength || 255})`;
case "TEXT": case "TEXT":
return "TEXT"; return "TEXT";
case "TINYTEXT": case "TINYTEXT":
@@ -25,36 +25,34 @@ export default function mapDataType(
case "LONGTEXT": case "LONGTEXT":
return "LONGTEXT"; return "LONGTEXT";
case "TINYINT": case "TINYINT":
return field.integerLength return field.dataLength
? `TINYINT(${field.integerLength})` ? `TINYINT(${field.dataLength})`
: "TINYINT"; : "TINYINT";
case "SMALLINT": case "SMALLINT":
return field.integerLength return field.dataLength
? `SMALLINT(${field.integerLength})` ? `SMALLINT(${field.dataLength})`
: "SMALLINT"; : "SMALLINT";
case "MEDIUMINT": case "MEDIUMINT":
return field.integerLength return field.dataLength
? `MEDIUMINT(${field.integerLength})` ? `MEDIUMINT(${field.dataLength})`
: "MEDIUMINT"; : "MEDIUMINT";
case "INT": case "INT":
return field.integerLength ? `INT(${field.integerLength})` : "INT"; return field.dataLength ? `INT(${field.dataLength})` : "INT";
case "BIGINT": case "BIGINT":
return field.integerLength return field.dataLength ? `BIGINT(${field.dataLength})` : "BIGINT";
? `BIGINT(${field.integerLength})`
: "BIGINT";
case "FLOAT": case "FLOAT":
return "FLOAT"; return "FLOAT";
case "DOUBLE": case "DOUBLE":
return "DOUBLE"; return "DOUBLE";
case "DECIMAL": case "DECIMAL":
if (field.integerLength && field.decimals) { if (field.dataLength && field.decimals) {
return `DECIMAL(${field.integerLength}, ${field.decimals})`; return `DECIMAL(${field.dataLength}, ${field.decimals})`;
} }
return "DECIMAL(10,2)"; return "DECIMAL(10,2)";
case "BINARY": case "BINARY":
return `BINARY(${field.integerLength || 1})`; return `BINARY(${field.dataLength || 1})`;
case "VARBINARY": case "VARBINARY":
return `VARBINARY(${field.integerLength || 255})`; return `VARBINARY(${field.dataLength || 255})`;
case "BLOB": case "BLOB":
return "BLOB"; return "BLOB";
case "TINYBLOB": case "TINYBLOB":
@@ -74,11 +72,12 @@ export default function mapDataType(
case "YEAR": case "YEAR":
return "YEAR"; return "YEAR";
case "UUID": case "UUID":
return "CHAR(36)"; // MariaDB does not have a native UUID type return "UUID";
case "JSON": case "JSON":
return "JSON"; return "JSON";
case "INET6": case "INET6":
return "INET6"; return "INET6";
case "BOOL":
case "BOOLEAN": case "BOOLEAN":
return "TINYINT(1)"; return "TINYINT(1)";
case "ENUM": { case "ENUM": {
-148
View File
@@ -1,148 +0,0 @@
import type {
BUN_MARIADB_TableSchemaType,
BunMariaDBConfig,
} from "../../types";
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,
}: {
tableName: string;
config?: BunMariaDBConfig;
}): Promise<boolean> {
const schemaCond = schemaCondition(config);
const rows = await querySchemaRows<{ table_exists: number }>({
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,
}: {
table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig;
}): Promise<void> {
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<Record<string, any>>({
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): n is string => 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): fieldName is string => 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
@@ -41,8 +41,6 @@ export default function resolveTable(
tableName: table.tableName, tableName: table.tableName,
tableDescription: table.tableDescription || parentTable.tableDescription, tableDescription: table.tableDescription || parentTable.tableDescription,
collation: table.collation || parentTable.collation, collation: table.collation || parentTable.collation,
isVector:
table.isVector !== undefined ? table.isVector : parentTable.isVector,
fields: Array.from(mergedFieldsMap.values()), fields: Array.from(mergedFieldsMap.values()),
indexes: _.uniqBy( indexes: _.uniqBy(
[...(parentTable.indexes || []), ...(table.indexes || [])], [...(parentTable.indexes || []), ...(table.indexes || [])],
-4
View File
@@ -57,10 +57,6 @@ function rulesMatch(
export function grabDesiredForeignKeys( export function grabDesiredForeignKeys(
table: BUN_MARIADB_TableSchemaType, table: BUN_MARIADB_TableSchemaType,
): DesiredForeignKey[] { ): DesiredForeignKey[] {
if (table.isVector) {
return [];
}
const desired: DesiredForeignKey[] = []; const desired: DesiredForeignKey[] = [];
for (const field of table.fields || []) { for (const field of table.fields || []) {
+2 -8
View File
@@ -14,7 +14,6 @@ function isVectorIndexDef(
table: BUN_MARIADB_TableSchemaType, table: BUN_MARIADB_TableSchemaType,
): boolean { ): boolean {
if (index.indexType === "VECTOR") return true; if (index.indexType === "VECTOR") return true;
if (table.isVector) return true;
const firstFieldName = index.indexTableFields?.[0]; const firstFieldName = index.indexTableFields?.[0];
if (!firstFieldName) return false; if (!firstFieldName) return false;
@@ -97,8 +96,7 @@ async function createIndex({
.map((field) => MariaDBQuoteGen(field)) .map((field) => MariaDBQuoteGen(field))
.join(", "); .join(", ");
const typeUpper = index.indexType?.toUpperCase(); const typeUpper = index.indexType?.toUpperCase();
const isSpecialType = const isSpecialType = typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
typeUpper === "FULLTEXT" || typeUpper === "SPATIAL";
const indexPrefix = isSpecialType ? `${typeUpper} ` : ""; const indexPrefix = isSpecialType ? `${typeUpper} ` : "";
const indexSuffix = const indexSuffix =
!isSpecialType && (typeUpper === "BTREE" || typeUpper === "HASH") !isSpecialType && (typeUpper === "BTREE" || typeUpper === "HASH")
@@ -199,11 +197,7 @@ export default async function syncIndexes({
const columnsMatch = const columnsMatch =
details.columns.length === schemaColumns.length && details.columns.length === schemaColumns.length &&
details.columns.every((col, idx) => col === schemaColumns[idx]); details.columns.every((col, idx) => col === schemaColumns[idx]);
const typeMatch = indexTypesMatch( const typeMatch = indexTypesMatch(details.type, schemaIndex, table);
details.type,
schemaIndex,
table,
);
if (!columnsMatch || !typeMatch) { if (!columnsMatch || !typeMatch) {
console.log(`Recreating changed index: ${indexName}`); console.log(`Recreating changed index: ${indexName}`);
+92 -25
View File
@@ -8,10 +8,10 @@ import buildColumnDefinition, {
} from "./build-column-definition"; } from "./build-column-definition";
import createTable from "./create-table"; import createTable from "./create-table";
import getTableColumns, { type ColumnInfoRow } from "./get-table-columns"; import getTableColumns, { type ColumnInfoRow } from "./get-table-columns";
import getTableColumnsGemini from "./get-table-columns-gemnini";
import isVectorField from "./is-vector-field"; import isVectorField from "./is-vector-field";
import mapDataType from "./map-data-types"; import mapDataType from "./map-data-types";
import MariaDBQuoteGen from "./mariadb-quote-gen"; import MariaDBQuoteGen from "./mariadb-quote-gen";
import recreateTable from "./recreate-table";
import runSchemaQuery, { querySchemaRows } from "./run-schema-query"; import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
import schemaCondition from "./schema-condition"; import schemaCondition from "./schema-condition";
import { dropForeignKeysOnColumns } from "./sync-foreign-keys"; import { dropForeignKeysOnColumns } from "./sync-foreign-keys";
@@ -138,7 +138,11 @@ function columnAttributesDiverged(
(live.extra.match(/on update\s+(.+)/i) || [])[1] || "", (live.extra.match(/on update\s+(.+)/i) || [])[1] || "",
); );
const expectedOu = normalizeDefault(wantsOnUpdate); const expectedOu = normalizeDefault(wantsOnUpdate);
if (liveOnUpdate && expectedOu && !defaultsMatch(liveOnUpdate, expectedOu)) { if (
liveOnUpdate &&
expectedOu &&
!defaultsMatch(liveOnUpdate, expectedOu)
) {
return true; return true;
} }
} }
@@ -197,6 +201,76 @@ async function dropColumn({
}); });
} }
/**
* Drop + re-add a column (values discarded). Used when VECTOR dimensions change
* MODIFY cannot resize VECTOR, and a full table rebuild is unnecessary.
*/
async function recreateColumn({
tableName,
field,
config,
}: {
tableName: string;
field: BUN_MARIADB_FieldSchemaType;
config?: BunMariaDBConfig;
}): Promise<void> {
if (!field.fieldName) return;
console.log(
`Recreating column: ${tableName}.${field.fieldName} (values will be cleared)`,
);
await dropForeignKeysOnColumns({
tableName,
columns: [field.fieldName],
config,
});
const schemaCond = schemaCondition(config);
const indexRows = await querySchemaRows<{
INDEX_NAME: string;
COLUMN_NAME: string;
}>({
query: `SELECT INDEX_NAME, COLUMN_NAME FROM information_schema.STATISTICS WHERE ${schemaCond.where} AND TABLE_NAME = ? AND INDEX_NAME <> 'PRIMARY'`,
values: [...schemaCond.values, tableName],
config,
});
const indexesToDrop = new Set<string>();
for (const row of indexRows) {
if (row.COLUMN_NAME === field.fieldName) {
indexesToDrop.add(row.INDEX_NAME);
}
}
for (const indexName of indexesToDrop) {
console.log(
`Dropping index ${indexName} because column ${field.fieldName} is being recreated`,
);
try {
await runSchemaQuery({
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(tableName)}`,
config,
});
} catch (err: any) {
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;
}
}
await dropColumn({ tableName, fieldName: field.fieldName, config });
await addColumn({ tableName, field, config });
}
export default async function updateTable({ export default async function updateTable({
table, table,
config, config,
@@ -204,7 +278,7 @@ export default async function updateTable({
table: BUN_MARIADB_TableSchemaType; table: BUN_MARIADB_TableSchemaType;
config?: BunMariaDBConfig; config?: BunMariaDBConfig;
}): Promise<void> { }): Promise<void> {
const existingColumns = await getTableColumns({ const existingColumns = await getTableColumnsGemini({
tableName: table.tableName, tableName: table.tableName,
config, config,
}); });
@@ -223,8 +297,8 @@ export default async function updateTable({
const fieldsToAdd: BUN_MARIADB_FieldSchemaType[] = []; const fieldsToAdd: BUN_MARIADB_FieldSchemaType[] = [];
const fieldsToModify: BUN_MARIADB_FieldSchemaType[] = []; const fieldsToModify: BUN_MARIADB_FieldSchemaType[] = [];
const fieldsToRecreate: BUN_MARIADB_FieldSchemaType[] = [];
const fieldsToDrop: string[] = []; const fieldsToDrop: string[] = [];
let needsVectorRecreate = false;
for (const field of table.fields || []) { for (const field of table.fields || []) {
if (!field.fieldName) continue; if (!field.fieldName) continue;
@@ -234,9 +308,11 @@ export default async function updateTable({
if (!liveField) { if (!liveField) {
fieldsToAdd.push(field); fieldsToAdd.push(field);
} else { } else {
let mapped_data_type = mapDataType(field);
let typeDiverged = !columnTypesMatch( let typeDiverged = !columnTypesMatch(
liveField.type, liveField.type,
mapDataType(field), mapped_data_type,
); );
if (isVectorField(field)) { if (isVectorField(field)) {
@@ -246,7 +322,9 @@ export default async function updateTable({
field, field,
); );
if (typeDiverged) { if (typeDiverged) {
needsVectorRecreate = true; // VECTOR dimensions / storage cannot be MODIFYed — drop + re-add column
fieldsToRecreate.push(field);
continue;
} }
} }
@@ -259,15 +337,6 @@ export default async function updateTable({
} }
} }
// 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) { for (const col of existingColumns) {
if (!codeFieldsMap.has(col.name)) { if (!codeFieldsMap.has(col.name)) {
fieldsToDrop.push(col.name); fieldsToDrop.push(col.name);
@@ -277,6 +346,7 @@ export default async function updateTable({
if ( if (
fieldsToAdd.length === 0 && fieldsToAdd.length === 0 &&
fieldsToModify.length === 0 && fieldsToModify.length === 0 &&
fieldsToRecreate.length === 0 &&
fieldsToDrop.length === 0 fieldsToDrop.length === 0
) { ) {
return; return;
@@ -370,18 +440,15 @@ export default async function updateTable({
} }
for (const field of fieldsToModify) { for (const field of fieldsToModify) {
try {
await modifyColumn({ tableName: table.tableName, field, config }); await modifyColumn({ tableName: table.tableName, field, config });
} catch (err: any) {
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 field of fieldsToRecreate) {
await recreateColumn({
tableName: table.tableName,
field,
config,
});
} }
for (const fieldName of fieldsToDrop) { for (const fieldName of fieldsToDrop) {
+6 -6
View File
@@ -96,10 +96,6 @@ export interface BUN_MARIADB_TableSchemaType {
*/ */
childTableDbId?: string | number; childTableDbId?: string | number;
collation?: (typeof MariaDBCollations)[number]; collation?: (typeof MariaDBCollations)[number];
/**
* If this is a vector-oriented table (native MariaDB VECTOR columns/indexes)
*/
isVector?: boolean;
} }
/** /**
@@ -202,7 +198,10 @@ export type BUN_MARIADB_FieldSchemaType = {
onDelete?: string; onDelete?: string;
onDeleteLiteral?: string; onDeleteLiteral?: string;
cssFiles?: string[]; cssFiles?: string[];
integerLength?: string | number; /**
* Datatype length. Eg 255 for VARCHAR
*/
dataLength?: string | number;
decimals?: string | number; decimals?: string | number;
code?: boolean; code?: boolean;
options?: (string | number)[]; options?: (string | number)[];
@@ -916,7 +915,7 @@ export type ServerQueryParamsJoin<
tableName: Table; tableName: Table;
match?: match?:
| ServerQueryParamsJoinMatchObject<Field> | ServerQueryParamsJoinMatchObject<Field>
| ServerQueryParamsJoinMatchObject<Field>[]; | (ServerQueryParamsJoinMatchObject<Field> | undefined)[];
selectFields?: (keyof Field | SelectFieldObject<Field>)[]; selectFields?: (keyof Field | SelectFieldObject<Field>)[];
omitFields?: ( omitFields?: (
| keyof Field | keyof Field
@@ -1619,6 +1618,7 @@ export type DBResponseObject<
msg?: string; msg?: string;
debug?: any; debug?: any;
count?: number; count?: number;
db_res?: any;
}; };
export type DBInsertReturn = { export type DBInsertReturn = {
+2 -1
View File
@@ -1,4 +1,4 @@
import { isUndefined } from "lodash"; import _, { isUndefined } from "lodash";
import type { ServerQueryParam, TableSelectFieldsObject } from "../types"; import type { ServerQueryParam, TableSelectFieldsObject } from "../types";
import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str"; import sqlGenGrabConcatStr from "./sql-generator-grab-concat-str";
import sqlGenGenJoinStr from "./sql-generator-gen-join-str"; import sqlGenGenJoinStr from "./sql-generator-gen-join-str";
@@ -195,6 +195,7 @@ export default function sqlGenGenQueryStr<
return ( return (
"(" + "(" +
join.match join.match
.filter((mtch) => !_.isUndefined(mtch))
.map((mtch) => { .map((mtch) => {
const { str, values } = const { str, values } =
sqlGenGenJoinStr({ sqlGenGenJoinStr({
+1 -1
View File
@@ -50,7 +50,7 @@ export default function sqlInsertGenerator({
? value ? value
: null; : null;
if (!finalValue) { if (!finalValue && typeof value !== "number") {
queryValues.push(null); queryValues.push(null);
return "?"; return "?";
} }