178 lines
7.0 KiB
JavaScript
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 });
|
|
}
|
|
}
|