bun-mariadb/dist/lib/schema/update-table.js

178 lines
7.0 KiB
JavaScript

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 });
}
}