Updates
This commit is contained in:
@@ -3,8 +3,14 @@ import isVectorField from "./is-vector-field";
|
||||
import mapDataType from "./map-data-types";
|
||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||
|
||||
export type BuildColumnDefinitionOptions = {
|
||||
/** UNIQUE is managed by syncUniqueConstraints on existing tables */
|
||||
omitUnique?: boolean;
|
||||
};
|
||||
|
||||
export default function buildColumnDefinition(
|
||||
field: BUN_MARIADB_FieldSchemaType,
|
||||
options: BuildColumnDefinitionOptions = {},
|
||||
): string {
|
||||
if (!field.fieldName) {
|
||||
throw new Error("Field name is required");
|
||||
@@ -29,7 +35,12 @@ export default function buildColumnDefinition(
|
||||
}
|
||||
|
||||
// VECTOR columns cannot be UNIQUE in the usual sense
|
||||
if (field.unique && !field.primaryKey && !isVectorField(field)) {
|
||||
if (
|
||||
!options.omitUnique &&
|
||||
field.unique &&
|
||||
!field.primaryKey &&
|
||||
!isVectorField(field)
|
||||
) {
|
||||
parts.push("UNIQUE");
|
||||
}
|
||||
|
||||
@@ -51,3 +62,12 @@ export default function buildColumnDefinition(
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
/** Whether schema field requires NOT NULL */
|
||||
export function fieldRequiresNotNull(
|
||||
field: BUN_MARIADB_FieldSchemaType,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
field.notNullValue || field.primaryKey || isVectorField(field),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import type { BUN_MARIADB_FieldSchemaType } from "../../types";
|
||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||
|
||||
export function defaultForeignKeyName(
|
||||
tableName: string,
|
||||
fieldName: string,
|
||||
): string {
|
||||
return `fk_${tableName}_${fieldName}`;
|
||||
}
|
||||
|
||||
export function resolveForeignKeyName(
|
||||
field: BUN_MARIADB_FieldSchemaType,
|
||||
tableName: string,
|
||||
): string {
|
||||
const fieldName = field.fieldName || "column";
|
||||
return field.foreignKey?.foreignKeyName || defaultForeignKeyName(tableName, fieldName);
|
||||
}
|
||||
|
||||
export default function buildForeignKeyConstraint(
|
||||
field: BUN_MARIADB_FieldSchemaType,
|
||||
tableName: string,
|
||||
): string {
|
||||
const fk = field.foreignKey!;
|
||||
const constraintName = fk.foreignKeyName
|
||||
? `CONSTRAINT ${MariaDBQuoteGen(fk.foreignKeyName)} `
|
||||
: "";
|
||||
const constraintName = resolveForeignKeyName(field, tableName);
|
||||
|
||||
let constraint = `${constraintName}FOREIGN KEY (${MariaDBQuoteGen(field.fieldName!)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName!)}(${MariaDBQuoteGen(fk.destinationTableColumnName!)})`;
|
||||
let constraint = `CONSTRAINT ${MariaDBQuoteGen(constraintName)} FOREIGN KEY (${MariaDBQuoteGen(field.fieldName!)}) REFERENCES ${MariaDBQuoteGen(fk.destinationTableName!)}(${MariaDBQuoteGen(fk.destinationTableColumnName!)})`;
|
||||
|
||||
if (fk.cascadeDelete) {
|
||||
constraint += " ON DELETE CASCADE";
|
||||
|
||||
@@ -31,7 +31,9 @@ export default async function createTable({
|
||||
}
|
||||
|
||||
if (field.foreignKey && !table.isVector) {
|
||||
foreignKeys.push(buildForeignKeyConstraint(field));
|
||||
foreignKeys.push(
|
||||
buildForeignKeyConstraint(field, table.tableName),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,21 +44,21 @@ export default async function createTable({
|
||||
|
||||
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, "")}`;
|
||||
const columns = (constraint.constraintTableFields || [])
|
||||
.map((field) => field.value)
|
||||
.filter((value): value is string => Boolean(value));
|
||||
|
||||
columnDefinitions.push(
|
||||
`CONSTRAINT ${MariaDBQuoteGen(constraintName)} UNIQUE (${fields})`,
|
||||
);
|
||||
if (columns.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fields = columns.map((col) => MariaDBQuoteGen(col)).join(", ");
|
||||
const constraintName =
|
||||
constraint.constraintName || `unique_${columns.join("_")}`;
|
||||
|
||||
columnDefinitions.push(
|
||||
`CONSTRAINT ${MariaDBQuoteGen(constraintName)} UNIQUE (${fields})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ export type ColumnInfoRow = {
|
||||
name: string;
|
||||
type: string;
|
||||
comment?: string;
|
||||
isNullable: boolean;
|
||||
columnDefault: string | null;
|
||||
extra: string;
|
||||
};
|
||||
|
||||
export default async function getTableColumns({
|
||||
@@ -20,8 +23,11 @@ export default async function getTableColumns({
|
||||
COLUMN_NAME: string;
|
||||
COLUMN_TYPE: string;
|
||||
COLUMN_COMMENT: string;
|
||||
IS_NULLABLE: string;
|
||||
COLUMN_DEFAULT: string | null;
|
||||
EXTRA: string;
|
||||
}>({
|
||||
query: `SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE ${schemaCond.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
||||
query: `SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT, IS_NULLABLE, COLUMN_DEFAULT, EXTRA FROM information_schema.COLUMNS WHERE ${schemaCond.where} AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`,
|
||||
values: [...schemaCond.values, tableName],
|
||||
config,
|
||||
});
|
||||
@@ -30,5 +36,8 @@ export default async function getTableColumns({
|
||||
name: row.COLUMN_NAME,
|
||||
type: row.COLUMN_TYPE,
|
||||
comment: row.COLUMN_COMMENT,
|
||||
isNullable: String(row.IS_NULLABLE).toUpperCase() === "YES",
|
||||
columnDefault: row.COLUMN_DEFAULT,
|
||||
extra: row.EXTRA || "",
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||
import resolveTable from "./resolve-table";
|
||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||
import schemaCondition from "./schema-condition";
|
||||
import {
|
||||
dropObsoleteForeignKeys,
|
||||
ensureForeignKeys,
|
||||
} from "./sync-foreign-keys";
|
||||
import syncIndexes from "./sync-indexes";
|
||||
import syncPrimaryKey from "./sync-primary-key";
|
||||
import syncTableOptions from "./sync-table-options";
|
||||
import syncUniqueConstraints from "./sync-unique-constraints";
|
||||
import updateTable from "./update-table";
|
||||
import upsertDbManagerTable, {
|
||||
removeDbManagerTable,
|
||||
@@ -21,7 +28,6 @@ export default async function handleDBSchemaTable({
|
||||
|
||||
let tableExistsTracked = Boolean(db_manager_table_name);
|
||||
let tableExistsLive = Boolean(existing_live_table?.TABLE_NAME);
|
||||
let wasRenamed = false;
|
||||
|
||||
if (
|
||||
resolvedTable.tableNameOld &&
|
||||
@@ -53,7 +59,6 @@ export default async function handleDBSchemaTable({
|
||||
});
|
||||
tableExistsTracked = true;
|
||||
tableExistsLive = true;
|
||||
wasRenamed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,17 +69,27 @@ export default async function handleDBSchemaTable({
|
||||
config,
|
||||
});
|
||||
} else {
|
||||
if (!wasRenamed) {
|
||||
await updateTable({
|
||||
table: resolvedTable,
|
||||
config,
|
||||
});
|
||||
}
|
||||
// Columns first (also drops FKs on removed/modified columns)
|
||||
await updateTable({
|
||||
table: resolvedTable,
|
||||
config,
|
||||
});
|
||||
await upsertDbManagerTable({
|
||||
tableName: resolvedTable.tableName,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
// Order matters:
|
||||
// 1. Table options (engine/collation)
|
||||
// 2. Drop obsolete/changed FKs (unlocks index cleanup)
|
||||
// 3. Primary key
|
||||
// 4. Indexes / uniques (FK columns need supporting indexes)
|
||||
// 5. Ensure foreign keys exist
|
||||
await syncTableOptions({ table: resolvedTable, config });
|
||||
await dropObsoleteForeignKeys({ table: resolvedTable, config });
|
||||
await syncPrimaryKey({ table: resolvedTable, config });
|
||||
await syncIndexes({ table: resolvedTable, config });
|
||||
await syncUniqueConstraints({ table: resolvedTable, config });
|
||||
await ensureForeignKeys({ table: resolvedTable, config });
|
||||
}
|
||||
|
||||
@@ -91,7 +91,8 @@ export default async function handleDBSchemaTables(
|
||||
schemaTableNames.includes(row.TABLE_NAME) ||
|
||||
!tablesToDrop.includes(row.TABLE_NAME)
|
||||
) {
|
||||
const list = referencedByRemaining.get(row.REFERENCED_TABLE_NAME) || [];
|
||||
const list =
|
||||
referencedByRemaining.get(row.REFERENCED_TABLE_NAME) || [];
|
||||
if (!list.includes(row.TABLE_NAME)) {
|
||||
list.push(row.TABLE_NAME);
|
||||
}
|
||||
@@ -118,6 +119,7 @@ export default async function handleDBSchemaTables(
|
||||
}
|
||||
|
||||
await runSchemaQuery({ query: `SET FOREIGN_KEY_CHECKS = 0`, config });
|
||||
|
||||
try {
|
||||
for (const tableName of safeToDrop) {
|
||||
console.log(`Dropping table: ${tableName}`);
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import type {
|
||||
BUN_MARIADB_FieldSchemaType,
|
||||
BUN_MARIADB_TableSchemaType,
|
||||
BunMariaDBConfig,
|
||||
} from "../../types";
|
||||
import buildForeignKeyConstraint, {
|
||||
resolveForeignKeyName,
|
||||
} from "./build-foreign-key-constraint";
|
||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||
|
||||
export type DesiredForeignKey = {
|
||||
name: string;
|
||||
column: string;
|
||||
refTable: string;
|
||||
refColumn: string;
|
||||
cascadeDelete: boolean;
|
||||
cascadeUpdate: boolean;
|
||||
field: BUN_MARIADB_FieldSchemaType;
|
||||
};
|
||||
|
||||
export type LiveForeignKey = {
|
||||
name: string;
|
||||
column: string;
|
||||
refTable: string;
|
||||
refColumn: string;
|
||||
deleteRule: string;
|
||||
updateRule: string;
|
||||
};
|
||||
|
||||
function normalizeRule(rule: string | undefined | null): string {
|
||||
return String(rule || "RESTRICT").toUpperCase().replace(/_/g, " ");
|
||||
}
|
||||
|
||||
function ruleIsCascade(rule: string): boolean {
|
||||
return normalizeRule(rule) === "CASCADE";
|
||||
}
|
||||
|
||||
function fkIdentityKey(fk: {
|
||||
column: string;
|
||||
refTable: string;
|
||||
refColumn: string;
|
||||
}): string {
|
||||
return `${fk.column}\0${fk.refTable}\0${fk.refColumn}`;
|
||||
}
|
||||
|
||||
function rulesMatch(
|
||||
live: LiveForeignKey,
|
||||
desired: DesiredForeignKey,
|
||||
): boolean {
|
||||
return (
|
||||
ruleIsCascade(live.deleteRule) === desired.cascadeDelete &&
|
||||
ruleIsCascade(live.updateRule) === desired.cascadeUpdate
|
||||
);
|
||||
}
|
||||
|
||||
export function grabDesiredForeignKeys(
|
||||
table: BUN_MARIADB_TableSchemaType,
|
||||
): DesiredForeignKey[] {
|
||||
if (table.isVector) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const desired: DesiredForeignKey[] = [];
|
||||
|
||||
for (const field of table.fields || []) {
|
||||
const fk = field.foreignKey;
|
||||
if (
|
||||
!field.fieldName ||
|
||||
!fk?.destinationTableName ||
|
||||
!fk.destinationTableColumnName
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
desired.push({
|
||||
name: resolveForeignKeyName(field, table.tableName),
|
||||
column: field.fieldName,
|
||||
refTable: fk.destinationTableName,
|
||||
refColumn: fk.destinationTableColumnName,
|
||||
cascadeDelete: Boolean(fk.cascadeDelete),
|
||||
cascadeUpdate: Boolean(fk.cascadeUpdate),
|
||||
field,
|
||||
});
|
||||
}
|
||||
|
||||
return desired;
|
||||
}
|
||||
|
||||
export async function grabLiveForeignKeys({
|
||||
tableName,
|
||||
config,
|
||||
}: {
|
||||
tableName: string;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<LiveForeignKey[]> {
|
||||
const databaseName = config?.db_name || global.CONFIG?.db_name;
|
||||
const schemaWhere = databaseName
|
||||
? "rc.CONSTRAINT_SCHEMA = ?"
|
||||
: "rc.CONSTRAINT_SCHEMA = DATABASE()";
|
||||
const schemaValues = databaseName ? [databaseName] : [];
|
||||
|
||||
const rows = await querySchemaRows<{
|
||||
CONSTRAINT_NAME: string;
|
||||
COLUMN_NAME: string;
|
||||
REFERENCED_TABLE_NAME: string;
|
||||
REFERENCED_COLUMN_NAME: string;
|
||||
DELETE_RULE: string;
|
||||
UPDATE_RULE: string;
|
||||
}>({
|
||||
query: `
|
||||
SELECT
|
||||
rc.CONSTRAINT_NAME,
|
||||
kcu.COLUMN_NAME,
|
||||
kcu.REFERENCED_TABLE_NAME,
|
||||
kcu.REFERENCED_COLUMN_NAME,
|
||||
rc.DELETE_RULE,
|
||||
rc.UPDATE_RULE
|
||||
FROM information_schema.REFERENTIAL_CONSTRAINTS rc
|
||||
INNER JOIN information_schema.KEY_COLUMN_USAGE kcu
|
||||
ON rc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
|
||||
AND rc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
||||
AND rc.TABLE_NAME = kcu.TABLE_NAME
|
||||
WHERE ${schemaWhere}
|
||||
AND rc.TABLE_NAME = ?
|
||||
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
||||
ORDER BY rc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
|
||||
`,
|
||||
values: [...schemaValues, tableName],
|
||||
config,
|
||||
});
|
||||
|
||||
// Single-column FKs only (schema model is one FK per field)
|
||||
const byName = new Map<string, LiveForeignKey>();
|
||||
for (const row of rows) {
|
||||
if (byName.has(row.CONSTRAINT_NAME)) {
|
||||
continue;
|
||||
}
|
||||
byName.set(row.CONSTRAINT_NAME, {
|
||||
name: row.CONSTRAINT_NAME,
|
||||
column: row.COLUMN_NAME,
|
||||
refTable: row.REFERENCED_TABLE_NAME,
|
||||
refColumn: row.REFERENCED_COLUMN_NAME,
|
||||
deleteRule: row.DELETE_RULE,
|
||||
updateRule: row.UPDATE_RULE,
|
||||
});
|
||||
}
|
||||
|
||||
return [...byName.values()];
|
||||
}
|
||||
|
||||
export async function dropForeignKey({
|
||||
tableName,
|
||||
constraintName,
|
||||
config,
|
||||
}: {
|
||||
tableName: string;
|
||||
constraintName: string;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
console.log(
|
||||
`Dropping foreign key: ${constraintName} on ${tableName}`,
|
||||
);
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} DROP FOREIGN KEY ${MariaDBQuoteGen(constraintName)}`,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
export async function dropForeignKeysOnColumns({
|
||||
tableName,
|
||||
columns,
|
||||
config,
|
||||
}: {
|
||||
tableName: string;
|
||||
columns: string[];
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
if (columns.length === 0) return;
|
||||
|
||||
const live = await grabLiveForeignKeys({ tableName, config });
|
||||
const colSet = new Set(columns);
|
||||
|
||||
for (const fk of live) {
|
||||
if (colSet.has(fk.column)) {
|
||||
await dropForeignKey({
|
||||
tableName,
|
||||
constraintName: fk.name,
|
||||
config,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop foreign keys that are removed from schema or need recreation
|
||||
* (name/cascade/target changed). Run before index cleanup.
|
||||
*/
|
||||
export async function dropObsoleteForeignKeys({
|
||||
table,
|
||||
config,
|
||||
}: {
|
||||
table: BUN_MARIADB_TableSchemaType;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
const desired = grabDesiredForeignKeys(table);
|
||||
const live = await grabLiveForeignKeys({
|
||||
tableName: table.tableName,
|
||||
config,
|
||||
});
|
||||
|
||||
const keepNames = new Set<string>();
|
||||
const droppedNames = new Set<string>();
|
||||
|
||||
const dropOnce = async (name: string) => {
|
||||
if (droppedNames.has(name)) return;
|
||||
droppedNames.add(name);
|
||||
await dropForeignKey({
|
||||
tableName: table.tableName,
|
||||
constraintName: name,
|
||||
config,
|
||||
});
|
||||
};
|
||||
|
||||
for (const want of desired) {
|
||||
const byIdentity = live.find(
|
||||
(existing) =>
|
||||
!keepNames.has(existing.name) &&
|
||||
!droppedNames.has(existing.name) &&
|
||||
fkIdentityKey(existing) === fkIdentityKey(want),
|
||||
);
|
||||
|
||||
if (
|
||||
byIdentity &&
|
||||
rulesMatch(byIdentity, want) &&
|
||||
byIdentity.name === want.name
|
||||
) {
|
||||
keepNames.add(byIdentity.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Changed relationship → drop so ensureForeignKeys can recreate
|
||||
if (byIdentity) {
|
||||
await dropOnce(byIdentity.name);
|
||||
}
|
||||
|
||||
const byName = live.find((existing) => existing.name === want.name);
|
||||
if (byName && byName !== byIdentity) {
|
||||
await dropOnce(byName.name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const existing of live) {
|
||||
if (keepNames.has(existing.name) || droppedNames.has(existing.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not in schema anymore
|
||||
await dropOnce(existing.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure all desired foreign keys exist. Run after indexes/uniques.
|
||||
*/
|
||||
export async function ensureForeignKeys({
|
||||
table,
|
||||
config,
|
||||
}: {
|
||||
table: BUN_MARIADB_TableSchemaType;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
const desired = grabDesiredForeignKeys(table);
|
||||
const live = await grabLiveForeignKeys({
|
||||
tableName: table.tableName,
|
||||
config,
|
||||
});
|
||||
|
||||
for (const want of desired) {
|
||||
const exists = live.some(
|
||||
(existing) =>
|
||||
fkIdentityKey(existing) === fkIdentityKey(want) &&
|
||||
rulesMatch(existing, want) &&
|
||||
existing.name === want.name,
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Creating foreign key: ${want.name} (${want.column} → ${want.refTable}.${want.refColumn}) on ${table.tableName}`,
|
||||
);
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD ${buildForeignKeyConstraint(want.field, table.tableName)}`,
|
||||
config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default async function syncForeignKeys({
|
||||
table,
|
||||
config,
|
||||
}: {
|
||||
table: BUN_MARIADB_TableSchemaType;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
await dropObsoleteForeignKeys({ table, config });
|
||||
await ensureForeignKeys({ table, config });
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import isVectorField from "./is-vector-field";
|
||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||
import schemaCondition from "./schema-condition";
|
||||
import { grabDesiredUniqueConstraints } from "./sync-unique-constraints";
|
||||
|
||||
function isVectorIndexDef(
|
||||
index: BUN_MARIADB_IndexSchemaType,
|
||||
@@ -28,6 +29,88 @@ function vectorDistanceMetric(index: BUN_MARIADB_IndexSchemaType): string {
|
||||
return "euclidean";
|
||||
}
|
||||
|
||||
/** Normalize schema index type vs information_schema.INDEX_TYPE */
|
||||
function indexTypesMatch(
|
||||
liveType: string,
|
||||
schemaIndex: BUN_MARIADB_IndexSchemaType,
|
||||
table: BUN_MARIADB_TableSchemaType,
|
||||
): boolean {
|
||||
const live = (liveType || "").toUpperCase();
|
||||
const schemaType = schemaIndex.indexType?.toUpperCase();
|
||||
|
||||
if (isVectorIndexDef(schemaIndex, table)) {
|
||||
// MariaDB may report VECTOR indexes as BTREE or VECTOR depending on version
|
||||
return live === "VECTOR" || live === "BTREE" || live === "";
|
||||
}
|
||||
|
||||
if (!schemaType || schemaType === "BTREE") {
|
||||
// Default / unspecified → BTREE
|
||||
return live === "BTREE" || live === "";
|
||||
}
|
||||
|
||||
if (schemaType === "FULLTEXT") {
|
||||
return live === "FULLTEXT";
|
||||
}
|
||||
|
||||
if (schemaType === "SPATIAL") {
|
||||
return live === "SPATIAL";
|
||||
}
|
||||
|
||||
if (schemaType === "HASH") {
|
||||
return live === "HASH";
|
||||
}
|
||||
|
||||
return live === schemaType;
|
||||
}
|
||||
|
||||
async function createIndex({
|
||||
table,
|
||||
index,
|
||||
config,
|
||||
}: {
|
||||
table: BUN_MARIADB_TableSchemaType;
|
||||
index: BUN_MARIADB_IndexSchemaType;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
if (
|
||||
!index.indexName ||
|
||||
!index.indexTableFields ||
|
||||
index.indexTableFields.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function syncIndexes({
|
||||
table,
|
||||
config,
|
||||
@@ -62,17 +145,8 @@ export default async function syncIndexes({
|
||||
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);
|
||||
}
|
||||
for (const constraint of grabDesiredUniqueConstraints(table)) {
|
||||
protectedIndexNames.add(constraint.name);
|
||||
}
|
||||
|
||||
const existingIndexesMap = new Map<
|
||||
@@ -106,6 +180,7 @@ export default async function syncIndexes({
|
||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||
config,
|
||||
});
|
||||
existingIndexesMap.delete(indexName);
|
||||
} catch (err: any) {
|
||||
if (
|
||||
String(err?.message || "").includes(
|
||||
@@ -124,8 +199,13 @@ export default async function syncIndexes({
|
||||
const columnsMatch =
|
||||
details.columns.length === schemaColumns.length &&
|
||||
details.columns.every((col, idx) => col === schemaColumns[idx]);
|
||||
const typeMatch = indexTypesMatch(
|
||||
details.type,
|
||||
schemaIndex,
|
||||
table,
|
||||
);
|
||||
|
||||
if (!columnsMatch) {
|
||||
if (!columnsMatch || !typeMatch) {
|
||||
console.log(`Recreating changed index: ${indexName}`);
|
||||
await runSchemaQuery({
|
||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||
@@ -146,35 +226,7 @@ export default async function syncIndexes({
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
await createIndex({ table, index, config });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import type {
|
||||
BUN_MARIADB_TableSchemaType,
|
||||
BunMariaDBConfig,
|
||||
} from "../../types";
|
||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||
import schemaCondition from "./schema-condition";
|
||||
|
||||
function columnsEqual(a: string[], b: string[]): boolean {
|
||||
return a.length === b.length && a.every((col, i) => col === b[i]);
|
||||
}
|
||||
|
||||
export function grabDesiredPrimaryKeyColumns(
|
||||
table: BUN_MARIADB_TableSchemaType,
|
||||
): string[] {
|
||||
return (table.fields || [])
|
||||
.filter((field) => field.primaryKey && field.fieldName)
|
||||
.map((field) => field.fieldName!);
|
||||
}
|
||||
|
||||
async function grabLivePrimaryKeyColumns({
|
||||
tableName,
|
||||
config,
|
||||
}: {
|
||||
tableName: string;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<string[]> {
|
||||
const schemaCond = schemaCondition(config);
|
||||
const rows = await querySchemaRows<{
|
||||
COLUMN_NAME: string;
|
||||
ORDINAL_POSITION: number;
|
||||
}>({
|
||||
query: `
|
||||
SELECT COLUMN_NAME, ORDINAL_POSITION
|
||||
FROM information_schema.KEY_COLUMN_USAGE
|
||||
WHERE ${schemaCond.where}
|
||||
AND TABLE_NAME = ?
|
||||
AND CONSTRAINT_NAME = 'PRIMARY'
|
||||
ORDER BY ORDINAL_POSITION
|
||||
`,
|
||||
values: [...schemaCond.values, tableName],
|
||||
config,
|
||||
});
|
||||
|
||||
return rows.map((row) => row.COLUMN_NAME);
|
||||
}
|
||||
|
||||
export default async function syncPrimaryKey({
|
||||
table,
|
||||
config,
|
||||
}: {
|
||||
table: BUN_MARIADB_TableSchemaType;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
const desired = grabDesiredPrimaryKeyColumns(table);
|
||||
const live = await grabLivePrimaryKeyColumns({
|
||||
tableName: table.tableName,
|
||||
config,
|
||||
});
|
||||
|
||||
if (columnsEqual(desired, live)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (live.length > 0) {
|
||||
console.log(`Dropping primary key on ${table.tableName}`);
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP PRIMARY KEY`,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
if (desired.length > 0) {
|
||||
const cols = desired.map((col) => MariaDBQuoteGen(col)).join(", ");
|
||||
console.log(
|
||||
`Creating primary key (${desired.join(", ")}) on ${table.tableName}`,
|
||||
);
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD PRIMARY KEY (${cols})`,
|
||||
config,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type {
|
||||
BUN_MARIADB_TableSchemaType,
|
||||
BunMariaDBConfig,
|
||||
} from "../../types";
|
||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||
import schemaCondition from "./schema-condition";
|
||||
|
||||
export default async function syncTableOptions({
|
||||
table,
|
||||
config,
|
||||
}: {
|
||||
table: BUN_MARIADB_TableSchemaType;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
const schemaCond = schemaCondition(config);
|
||||
const rows = await querySchemaRows<{
|
||||
ENGINE: string;
|
||||
TABLE_COLLATION: string;
|
||||
}>({
|
||||
query: `SELECT ENGINE, TABLE_COLLATION FROM information_schema.TABLES WHERE ${schemaCond.where} AND TABLE_NAME = ? AND TABLE_TYPE = 'BASE TABLE'`,
|
||||
values: [...schemaCond.values, table.tableName],
|
||||
config,
|
||||
});
|
||||
|
||||
const live = rows[0];
|
||||
if (!live) {
|
||||
return;
|
||||
}
|
||||
|
||||
const alters: string[] = [];
|
||||
|
||||
if ((live.ENGINE || "").toUpperCase() !== "INNODB") {
|
||||
alters.push("ENGINE=InnoDB");
|
||||
}
|
||||
|
||||
if (table.collation) {
|
||||
const liveCollation = (live.TABLE_COLLATION || "").toLowerCase();
|
||||
if (liveCollation !== table.collation.toLowerCase()) {
|
||||
alters.push(
|
||||
`CONVERT TO CHARACTER SET utf8mb4 COLLATE ${table.collation}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (alters.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Updating table options on ${table.tableName}: ${alters.join(", ")}`,
|
||||
);
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ${alters.join(", ")}`,
|
||||
config,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import type {
|
||||
BUN_MARIADB_TableSchemaType,
|
||||
BunMariaDBConfig,
|
||||
} from "../../types";
|
||||
import isVectorField from "./is-vector-field";
|
||||
import MariaDBQuoteGen from "./mariadb-quote-gen";
|
||||
import runSchemaQuery, { querySchemaRows } from "./run-schema-query";
|
||||
|
||||
type UniqueConstraintLive = {
|
||||
name: string;
|
||||
columns: string[];
|
||||
};
|
||||
|
||||
type UniqueConstraintDesired = {
|
||||
name: string;
|
||||
columns: string[];
|
||||
};
|
||||
|
||||
function columnsKey(columns: string[]): string {
|
||||
return columns.join("\0");
|
||||
}
|
||||
|
||||
function columnsEqual(a: string[], b: string[]): boolean {
|
||||
return a.length === b.length && a.every((col, i) => col === b[i]);
|
||||
}
|
||||
|
||||
function defaultConstraintName(columns: string[]): string {
|
||||
return `unique_${columns.join("_")}`;
|
||||
}
|
||||
|
||||
export function grabDesiredUniqueConstraints(
|
||||
table: BUN_MARIADB_TableSchemaType,
|
||||
): UniqueConstraintDesired[] {
|
||||
const desired: UniqueConstraintDesired[] = [];
|
||||
const seenColumnSets = new Set<string>();
|
||||
|
||||
for (const constraint of table.uniqueConstraints || []) {
|
||||
const columns = (constraint.constraintTableFields || [])
|
||||
.map((field) => field.value)
|
||||
.filter((value): value is string => Boolean(value));
|
||||
|
||||
if (columns.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = columnsKey(columns);
|
||||
if (seenColumnSets.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seenColumnSets.add(key);
|
||||
|
||||
desired.push({
|
||||
name: constraint.constraintName || defaultConstraintName(columns),
|
||||
columns,
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of table.fields || []) {
|
||||
if (
|
||||
!field.fieldName ||
|
||||
!field.unique ||
|
||||
field.primaryKey ||
|
||||
isVectorField(field)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const columns = [field.fieldName];
|
||||
const key = columnsKey(columns);
|
||||
if (seenColumnSets.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seenColumnSets.add(key);
|
||||
|
||||
// Matches MariaDB's default name for column-level UNIQUE
|
||||
desired.push({
|
||||
name: field.fieldName,
|
||||
columns,
|
||||
});
|
||||
}
|
||||
|
||||
return desired;
|
||||
}
|
||||
|
||||
async function grabLiveUniqueConstraints({
|
||||
tableName,
|
||||
config,
|
||||
}: {
|
||||
tableName: string;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<UniqueConstraintLive[]> {
|
||||
const databaseName = config?.db_name || global.CONFIG?.db_name;
|
||||
const schemaWhere = databaseName
|
||||
? "tc.TABLE_SCHEMA = ?"
|
||||
: "tc.TABLE_SCHEMA = DATABASE()";
|
||||
const schemaValues = databaseName ? [databaseName] : [];
|
||||
|
||||
const rows = await querySchemaRows<{
|
||||
CONSTRAINT_NAME: string;
|
||||
COLUMN_NAME: string;
|
||||
ORDINAL_POSITION: number;
|
||||
}>({
|
||||
query: `
|
||||
SELECT tc.CONSTRAINT_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION
|
||||
FROM information_schema.TABLE_CONSTRAINTS tc
|
||||
INNER JOIN information_schema.KEY_COLUMN_USAGE kcu
|
||||
ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
|
||||
AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
||||
AND tc.TABLE_NAME = kcu.TABLE_NAME
|
||||
WHERE ${schemaWhere}
|
||||
AND tc.TABLE_NAME = ?
|
||||
AND tc.CONSTRAINT_TYPE = 'UNIQUE'
|
||||
ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION
|
||||
`,
|
||||
values: [...schemaValues, tableName],
|
||||
config,
|
||||
});
|
||||
|
||||
const map = new Map<string, string[]>();
|
||||
for (const row of rows) {
|
||||
if (!map.has(row.CONSTRAINT_NAME)) {
|
||||
map.set(row.CONSTRAINT_NAME, []);
|
||||
}
|
||||
map.get(row.CONSTRAINT_NAME)!.push(row.COLUMN_NAME);
|
||||
}
|
||||
|
||||
return [...map.entries()].map(([name, columns]) => ({ name, columns }));
|
||||
}
|
||||
|
||||
export default async function syncUniqueConstraints({
|
||||
table,
|
||||
config,
|
||||
}: {
|
||||
table: BUN_MARIADB_TableSchemaType;
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
const desired = grabDesiredUniqueConstraints(table);
|
||||
const live = await grabLiveUniqueConstraints({
|
||||
tableName: table.tableName,
|
||||
config,
|
||||
});
|
||||
|
||||
const matchedLiveNames = new Set<string>();
|
||||
|
||||
for (const want of desired) {
|
||||
const byColumns = live.find(
|
||||
(existing) =>
|
||||
!matchedLiveNames.has(existing.name) &&
|
||||
columnsEqual(existing.columns, want.columns),
|
||||
);
|
||||
|
||||
if (byColumns) {
|
||||
matchedLiveNames.add(byColumns.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
const byName = live.find((existing) => existing.name === want.name);
|
||||
if (byName && !matchedLiveNames.has(byName.name)) {
|
||||
console.log(
|
||||
`Recreating changed unique constraint: ${want.name} on ${table.tableName}`,
|
||||
);
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP INDEX ${MariaDBQuoteGen(byName.name)}`,
|
||||
config,
|
||||
});
|
||||
matchedLiveNames.add(byName.name);
|
||||
}
|
||||
|
||||
const fields = want.columns.map((col) => MariaDBQuoteGen(col)).join(", ");
|
||||
console.log(
|
||||
`Creating unique constraint: ${want.name} (${want.columns.join(", ")}) on ${table.tableName}`,
|
||||
);
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} ADD CONSTRAINT ${MariaDBQuoteGen(want.name)} UNIQUE (${fields})`,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
for (const existing of live) {
|
||||
if (matchedLiveNames.has(existing.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stillDesired = desired.some((want) =>
|
||||
columnsEqual(want.columns, existing.columns),
|
||||
);
|
||||
if (stillDesired) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Dropping unique constraint: ${existing.name} on ${table.tableName}`,
|
||||
);
|
||||
try {
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(table.tableName)} DROP INDEX ${MariaDBQuoteGen(existing.name)}`,
|
||||
config,
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (
|
||||
String(err?.message || "").includes(
|
||||
"needed in a foreign key constraint",
|
||||
)
|
||||
) {
|
||||
console.warn(
|
||||
`Skipping drop of unique constraint ${existing.name}: required by a foreign key constraint`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
-16
@@ -3,15 +3,18 @@ import type {
|
||||
BUN_MARIADB_TableSchemaType,
|
||||
BunMariaDBConfig,
|
||||
} from "../../types";
|
||||
import buildColumnDefinition from "./build-column-definition";
|
||||
import buildColumnDefinition, {
|
||||
fieldRequiresNotNull,
|
||||
} from "./build-column-definition";
|
||||
import createTable from "./create-table";
|
||||
import getTableColumns from "./get-table-columns";
|
||||
import getTableColumns, { type ColumnInfoRow } 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";
|
||||
import { dropForeignKeysOnColumns } from "./sync-foreign-keys";
|
||||
|
||||
/**
|
||||
* Compare live COLUMN_TYPE with schema-mapped type.
|
||||
@@ -54,6 +57,95 @@ function vectorTypeDiverged(
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeDefault(value: string | null | undefined): string {
|
||||
if (value == null) return "";
|
||||
let v = String(value).trim();
|
||||
// Strip surrounding quotes MariaDB may include
|
||||
if (
|
||||
(v.startsWith("'") && v.endsWith("'")) ||
|
||||
(v.startsWith('"') && v.endsWith('"'))
|
||||
) {
|
||||
v = v.slice(1, -1);
|
||||
}
|
||||
return v.toLowerCase().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function expectedDefault(field: BUN_MARIADB_FieldSchemaType): string | null {
|
||||
if (field.defaultValue !== undefined) {
|
||||
return String(field.defaultValue);
|
||||
}
|
||||
if (field.defaultValueLiteral) {
|
||||
return field.defaultValueLiteral;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function expectedOnUpdate(field: BUN_MARIADB_FieldSchemaType): string | null {
|
||||
if (field.onUpdate) return field.onUpdate;
|
||||
if (field.onUpdateLiteral) return field.onUpdateLiteral;
|
||||
return null;
|
||||
}
|
||||
|
||||
function defaultsMatch(liveDefault: string, expected: string): boolean {
|
||||
if (liveDefault === expected) return true;
|
||||
if (
|
||||
expected.includes("current_timestamp") &&
|
||||
liveDefault.includes("current_timestamp")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function columnAttributesDiverged(
|
||||
live: ColumnInfoRow,
|
||||
field: BUN_MARIADB_FieldSchemaType,
|
||||
): boolean {
|
||||
const wantsNotNull = fieldRequiresNotNull(field);
|
||||
if (wantsNotNull && live.isNullable) {
|
||||
return true;
|
||||
}
|
||||
if (!wantsNotNull && !live.isNullable && !field.primaryKey) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const liveExtra = (live.extra || "").toLowerCase();
|
||||
const wantsAi = Boolean(field.autoIncrement);
|
||||
const hasAi = liveExtra.includes("auto_increment");
|
||||
if (wantsAi !== hasAi) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const wantsDefault = expectedDefault(field);
|
||||
if (wantsDefault != null) {
|
||||
const liveDefault = normalizeDefault(live.columnDefault);
|
||||
const expected = normalizeDefault(wantsDefault);
|
||||
if (!defaultsMatch(liveDefault, expected)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const wantsOnUpdate = expectedOnUpdate(field);
|
||||
const hasOnUpdate = /on update/i.test(live.extra || "");
|
||||
if (wantsOnUpdate && !hasOnUpdate) {
|
||||
return true;
|
||||
}
|
||||
if (!wantsOnUpdate && hasOnUpdate) {
|
||||
return true;
|
||||
}
|
||||
if (wantsOnUpdate && hasOnUpdate) {
|
||||
const liveOnUpdate = normalizeDefault(
|
||||
(live.extra.match(/on update\s+(.+)/i) || [])[1] || "",
|
||||
);
|
||||
const expectedOu = normalizeDefault(wantsOnUpdate);
|
||||
if (liveOnUpdate && expectedOu && !defaultsMatch(liveOnUpdate, expectedOu)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function addColumn({
|
||||
tableName,
|
||||
field,
|
||||
@@ -64,7 +156,8 @@ async function addColumn({
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
console.log(`Adding column: ${tableName}.${field.fieldName}`);
|
||||
const columnDef = buildColumnDefinition(field).trim();
|
||||
// omitUnique — unique constraints are applied by syncUniqueConstraints
|
||||
const columnDef = buildColumnDefinition(field, { omitUnique: true }).trim();
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} ADD COLUMN IF NOT EXISTS ${columnDef}`,
|
||||
config,
|
||||
@@ -81,7 +174,7 @@ async function modifyColumn({
|
||||
config?: BunMariaDBConfig;
|
||||
}): Promise<void> {
|
||||
console.log(`Modifying column: ${tableName}.${field.fieldName}`);
|
||||
const columnDef = buildColumnDefinition(field).trim();
|
||||
const columnDef = buildColumnDefinition(field, { omitUnique: true }).trim();
|
||||
await runSchemaQuery({
|
||||
query: `ALTER TABLE ${MariaDBQuoteGen(tableName)} MODIFY COLUMN ${columnDef}`,
|
||||
config,
|
||||
@@ -122,10 +215,7 @@ export default async function updateTable({
|
||||
}
|
||||
|
||||
const liveFieldsMap = new Map(
|
||||
existingColumns.map((col) => [
|
||||
col.name,
|
||||
{ type: col.type.toLowerCase(), comment: col.comment || "" },
|
||||
]),
|
||||
existingColumns.map((col) => [col.name, col]),
|
||||
);
|
||||
const codeFieldsMap = new Map(
|
||||
(table.fields || []).map((f) => [f.fieldName, f]),
|
||||
@@ -142,8 +232,6 @@ export default async function updateTable({
|
||||
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(
|
||||
@@ -154,7 +242,7 @@ export default async function updateTable({
|
||||
if (isVectorField(field)) {
|
||||
typeDiverged = vectorTypeDiverged(
|
||||
liveField.type,
|
||||
liveField.comment,
|
||||
liveField.comment || "",
|
||||
field,
|
||||
);
|
||||
if (typeDiverged) {
|
||||
@@ -162,7 +250,10 @@ export default async function updateTable({
|
||||
}
|
||||
}
|
||||
|
||||
if (typeDiverged) {
|
||||
const attrsDiverged =
|
||||
!typeDiverged && columnAttributesDiverged(liveField, field);
|
||||
|
||||
if (typeDiverged || attrsDiverged) {
|
||||
fieldsToModify.push(field);
|
||||
}
|
||||
}
|
||||
@@ -196,6 +287,13 @@ export default async function updateTable({
|
||||
);
|
||||
|
||||
if (fieldsToDrop.length > 0) {
|
||||
// Drop FKs that reference columns being removed
|
||||
await dropForeignKeysOnColumns({
|
||||
tableName: table.tableName,
|
||||
columns: fieldsToDrop,
|
||||
config,
|
||||
});
|
||||
|
||||
const schemaCond = schemaCondition(config);
|
||||
|
||||
const pkRows = await querySchemaRows<{ COLUMN_NAME: string }>({
|
||||
@@ -235,13 +333,38 @@ export default async function updateTable({
|
||||
console.log(
|
||||
`Dropping index ${indexName} because it contains a dropped column`,
|
||||
);
|
||||
await runSchemaQuery({
|
||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.tableName)}`,
|
||||
config,
|
||||
});
|
||||
try {
|
||||
await runSchemaQuery({
|
||||
query: `DROP INDEX ${MariaDBQuoteGen(indexName)} ON ${MariaDBQuoteGen(table.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drop FKs on columns being modified (MODIFY can fail with FK present)
|
||||
if (fieldsToModify.length > 0) {
|
||||
await dropForeignKeysOnColumns({
|
||||
tableName: table.tableName,
|
||||
columns: fieldsToModify
|
||||
.map((f) => f.fieldName)
|
||||
.filter((n): n is string => Boolean(n)),
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
for (const field of fieldsToAdd) {
|
||||
await addColumn({ tableName: table.tableName, field, config });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user