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