bug: prod mysql conflict

This commit is contained in:
DeathKaioken 2026-01-18 16:19:08 +01:00
parent 8d4c70b631
commit 56fe694458
4 changed files with 160 additions and 109 deletions

View File

@ -65,7 +65,65 @@ async function ensureIndex(conn, table, indexName, indexDDL) {
}
}
async function createDatabase() {
// --- Schema pre-check helpers (Approach A) ---
async function columnExists(conn, table, column) {
const [rows] = await conn.query(
`SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND COLUMN_NAME = ?
LIMIT 1`,
[table, column]
);
return rows.length > 0;
}
async function addColumnIfMissing(conn, table, column, ddlFragment /* includes type + nullability + AFTER/etc */) {
if (await columnExists(conn, table, column)) return false;
await conn.query(`ALTER TABLE \`${table}\` ADD COLUMN \`${column}\` ${ddlFragment}`);
console.log(`🆕 Added column ${table}.${column}`);
return true;
}
async function constraintExists(conn, table, constraintName) {
const [rows] = await conn.query(
`SELECT 1
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
AND CONSTRAINT_NAME = ?
LIMIT 1`,
[table, constraintName]
);
return rows.length > 0;
}
async function addForeignKeyIfMissing(conn, table, constraintName, ddl /* full ALTER TABLE ... ADD CONSTRAINT ... */) {
if (await constraintExists(conn, table, constraintName)) return false;
await conn.query(ddl);
console.log(`🆕 Added constraint ${constraintName} on ${table}`);
return true;
}
async function getPrimaryKeyColumns(conn, table) {
const [rows] = await conn.query(
`SELECT k.COLUMN_NAME
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS t
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE k
ON t.CONSTRAINT_NAME = k.CONSTRAINT_NAME
AND t.TABLE_SCHEMA = k.TABLE_SCHEMA
AND t.TABLE_NAME = k.TABLE_NAME
WHERE t.TABLE_SCHEMA = DATABASE()
AND t.TABLE_NAME = ?
AND t.CONSTRAINT_TYPE = 'PRIMARY KEY'
ORDER BY k.ORDINAL_POSITION`,
[table]
);
return rows.map(r => r.COLUMN_NAME);
}
const createDatabase = async () => {
console.log('🚀 Starting MySQL database initialization...');
console.log('📍 Database host:', process.env.DB_HOST);
console.log('📍 Database name:', process.env.DB_NAME);
@ -285,22 +343,28 @@ async function createDatabase() {
INDEX idx_object_storage_id (object_storage_id)
);
`);
// Backfill in case table already existed without contract_type
await connection.query(`
ALTER TABLE user_documents
ADD COLUMN IF NOT EXISTS contract_type ENUM('contract','gdpr') NOT NULL DEFAULT 'contract' AFTER document_type;
`);
await connection.query(`
CREATE INDEX IF NOT EXISTS idx_user_contract_type ON user_documents (user_id, contract_type);
`);
// Backfill/migrate for older schemas (NO IF NOT EXISTS; pre-check instead)
await addColumnIfMissing(
connection,
'user_documents',
'contract_type',
`ENUM('contract','gdpr') NOT NULL DEFAULT 'contract' AFTER document_type`
);
await ensureIndex(connection, 'user_documents', 'idx_user_contract_type', '`user_id`, `contract_type`');
await connection.query(`
ALTER TABLE user_documents
MODIFY COLUMN object_storage_id VARCHAR(255) NULL;
`);
await connection.query(`
ALTER TABLE user_documents
ADD COLUMN IF NOT EXISTS signatureBase64 LONGTEXT NULL AFTER object_storage_id;
`);
await addColumnIfMissing(
connection,
'user_documents',
'signatureBase64',
`LONGTEXT NULL AFTER object_storage_id`
);
console.log('✅ User documents table created/verified');
// 8c. document_templates table: Stores template metadata and object storage keys
@ -370,54 +434,36 @@ async function createDatabase() {
console.log('✅ User action logs table created/verified');
// --- Add missing user_id column for existing databases + backfill ---
// (convert from "try/catch duplicate" to pre-check)
if (await addColumnIfMissing(connection, 'user_action_logs', 'user_id', `INT NULL`)) {
await addForeignKeyIfMissing(
connection,
'user_action_logs',
'fk_user_action_logs_user',
`
ALTER TABLE user_action_logs
ADD CONSTRAINT fk_user_action_logs_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
`
);
await ensureIndex(connection, 'user_action_logs', 'idx_user_id', '`user_id`');
}
// Backfill: prefer performed_by_user_id, else affected_user_id
try {
// Add column if missing
await connection.query(`ALTER TABLE user_action_logs ADD COLUMN user_id INT NULL`);
console.log('🆕 Added user_action_logs.user_id column');
// Add FK if just added
try {
await connection.query(`
ALTER TABLE user_action_logs
ADD CONSTRAINT fk_user_action_logs_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
`);
console.log('🆕 Added FK fk_user_action_logs_user');
} catch (e) {
console.log(' FK fk_user_action_logs_user already exists or cannot add:', e.message);
}
// Add index if missing
try {
await connection.query(`CREATE INDEX idx_user_id ON user_action_logs (user_id)`);
console.log('🆕 Added index idx_user_id on user_action_logs.user_id');
} catch (e) {
console.log(' idx_user_id already exists or cannot add:', e.message);
}
// Backfill: prefer performed_by_user_id, else affected_user_id
try {
const [res1] = await connection.query(`
UPDATE user_action_logs
SET user_id = performed_by_user_id
WHERE user_id IS NULL AND performed_by_user_id IS NOT NULL
`);
const [res2] = await connection.query(`
await connection.query(`
UPDATE user_action_logs
SET user_id = affected_user_id
WHERE user_id IS NULL AND affected_user_id IS NOT NULL
`);
console.log('🧹 Backfilled user_action_logs.user_id from performed_by_user_id/affected_user_id');
} catch (e) {
console.warn('⚠️ Could not backfill user_action_logs.user_id:', e.message);
}
} catch (e) {
// Column may already exist; ignore
if (!/Duplicate column name|exists/i.test(e.message)) {
console.log(' user_action_logs.user_id add skipped or not required:', e.message);
} else {
console.log(' user_action_logs.user_id already exists');
}
console.warn('⚠️ Could not backfill user_action_logs.user_id:', e.message);
}
// --- Email & Registration Flow Tables ---
@ -818,26 +864,17 @@ async function createDatabase() {
console.log('✅ Invoice payments table created/verified');
// Extend coffee_abonement_history with optional related_invoice_id
try {
await connection.query(`ALTER TABLE coffee_abonement_history ADD COLUMN related_invoice_id BIGINT NULL`);
console.log('🆕 Added coffee_abonement_history.related_invoice_id');
} catch (e) {
if (!/Duplicate column name|exists/i.test(e.message)) {
console.log(' related_invoice_id add skipped or not required:', e.message);
} else {
console.log(' coffee_abonement_history.related_invoice_id already exists');
}
}
try {
await connection.query(`
await addColumnIfMissing(connection, 'coffee_abonement_history', 'related_invoice_id', `BIGINT NULL`);
await addForeignKeyIfMissing(
connection,
'coffee_abonement_history',
'fk_hist_invoice',
`
ALTER TABLE coffee_abonement_history
ADD CONSTRAINT fk_hist_invoice FOREIGN KEY (related_invoice_id)
REFERENCES invoices(id) ON DELETE SET NULL ON UPDATE CASCADE
`);
console.log('🆕 Added FK fk_hist_invoice on coffee_abonement_history.related_invoice_id');
} catch (e) {
console.log(' fk_hist_invoice already exists or failed:', e.message);
}
`
);
// --- Matrix: Global 5-ary tree config and relations ---
await connection.query(`
@ -851,15 +888,9 @@ async function createDatabase() {
CONSTRAINT chk_matrix_singleton CHECK (id = 1)
);
`);
// Safeguard: if table pre-existed without name column, add it
try {
await connection.query(`ALTER TABLE matrix_config ADD COLUMN name VARCHAR(255) NULL`);
console.log('🆕 Added missing matrix_config.name column');
} catch (e) {
if (!/Duplicate column/i.test(e.message)) {
console.log(' matrix_config.name alter skipped:', e.message);
}
}
// Safeguard: if table pre-existed without name column, add it (pre-check)
await addColumnIfMissing(connection, 'matrix_config', 'name', `VARCHAR(255) NULL`);
console.log('✅ Matrix config table created/verified');
await connection.query(`
@ -1012,30 +1043,34 @@ async function createDatabase() {
}
// --- Ensure multi-instance columns (idempotent) ---
try { await connection.query(`ALTER TABLE user_tree_edges ADD COLUMN matrix_instance_id INT NULL`); } catch (_) {}
try { await connection.query(`ALTER TABLE user_tree_edges ADD COLUMN rogue_user BOOLEAN DEFAULT FALSE`); } catch (_) {}
try {
await connection.query(`
await addColumnIfMissing(connection, 'user_tree_edges', 'matrix_instance_id', `INT NULL`);
await addColumnIfMissing(connection, 'user_tree_edges', 'rogue_user', `BOOLEAN DEFAULT FALSE`);
await addForeignKeyIfMissing(
connection,
'user_tree_edges',
'fk_edges_instance',
`
ALTER TABLE user_tree_edges
ADD CONSTRAINT fk_edges_instance FOREIGN KEY (matrix_instance_id)
REFERENCES matrix_instances(id) ON DELETE CASCADE ON UPDATE CASCADE
`);
console.log('🆕 FK fk_edges_instance added');
} catch (e) {
console.log(' fk_edges_instance already exists or failed:', e.message);
}
`
);
// Backfill matrix_instance_id for existing edges
const [firstInstRow] = await connection.query(`SELECT id FROM matrix_instances ORDER BY id ASC LIMIT 1`);
const firstInstanceId = firstInstRow[0]?.id;
if (firstInstanceId) {
await connection.query(`UPDATE user_tree_edges SET matrix_instance_id = ? WHERE matrix_instance_id IS NULL`, [firstInstanceId]);
await connection.query(
`UPDATE user_tree_edges SET matrix_instance_id = ? WHERE matrix_instance_id IS NULL`,
[firstInstanceId]
);
}
// Indexes (idempotent)
try { await connection.query(`CREATE INDEX idx_edges_instance_parent ON user_tree_edges (matrix_instance_id, parent_user_id)`); } catch (_) {}
try { await connection.query(`CREATE INDEX idx_edges_instance_child ON user_tree_edges (matrix_instance_id, child_user_id)`); } catch (_) {}
try { await connection.query(`CREATE INDEX idx_edges_rogue ON user_tree_edges (matrix_instance_id, rogue_user)`); } catch (_) {}
// Indexes (pre-check)
await ensureIndex(connection, 'user_tree_edges', 'idx_edges_instance_parent', '`matrix_instance_id`, `parent_user_id`');
await ensureIndex(connection, 'user_tree_edges', 'idx_edges_instance_child', '`matrix_instance_id`, `child_user_id`');
await ensureIndex(connection, 'user_tree_edges', 'idx_edges_rogue', '`matrix_instance_id`, `rogue_user`');
// --- Alter user_tree_closure: add matrix_instance_id ---
await connection.query(`
@ -1054,23 +1089,42 @@ async function createDatabase() {
`);
console.log('✅ user_tree_closure (multi) created/verified');
// If legacy closure rows without matrix_instance_id (older separate table definition), attempt add column & backfill
try {
await connection.query(`ALTER TABLE user_tree_closure ADD COLUMN matrix_instance_id INT NOT NULL`);
console.log('🆕 Added matrix_instance_id column to existing user_tree_closure');
} catch (e) {
// already integrated new definition
// If legacy closure table existed without matrix_instance_id, migrate it safely (pre-check)
if (!(await columnExists(connection, 'user_tree_closure', 'matrix_instance_id'))) {
await connection.query(`ALTER TABLE user_tree_closure ADD COLUMN matrix_instance_id INT NULL`);
if (firstInstanceId) {
await connection.query(
`UPDATE user_tree_closure SET matrix_instance_id = ? WHERE matrix_instance_id IS NULL`,
[firstInstanceId]
);
}
// Best-effort: enforce NOT NULL after backfill
try {
await connection.query(`ALTER TABLE user_tree_closure MODIFY COLUMN matrix_instance_id INT NOT NULL`);
} catch (e) {
console.log(' user_tree_closure.matrix_instance_id NOT NULL enforcement skipped:', e.message);
}
// Best-effort: align PK to (matrix_instance_id, ancestor_user_id, descendant_user_id)
try {
const pkCols = await getPrimaryKeyColumns(connection, 'user_tree_closure');
const wants = ['matrix_instance_id', 'ancestor_user_id', 'descendant_user_id'];
const matches =
pkCols.length === wants.length && pkCols.every((c, i) => c === wants[i]);
if (!matches) {
await connection.query(`ALTER TABLE user_tree_closure DROP PRIMARY KEY`);
await connection.query(
`ALTER TABLE user_tree_closure ADD PRIMARY KEY (matrix_instance_id, ancestor_user_id, descendant_user_id)`
);
}
} catch (e) {
console.log(' user_tree_closure PK alignment skipped:', e.message);
}
}
if (firstInstanceId) {
try {
await connection.query(`UPDATE user_tree_closure SET matrix_instance_id = ? WHERE matrix_instance_id IS NULL`, [firstInstanceId]);
console.log('🧩 Backfilled closure matrix_instance_id');
} catch (e) {
console.log(' Closure backfill skipped:', e.message);
}
}
try { await connection.query(`CREATE INDEX idx_closure_instance_depth ON user_tree_closure (matrix_instance_id, depth)`); } catch (_) {}
try { await connection.query(`CREATE INDEX idx_closure_instance_ancestor ON user_tree_closure (matrix_instance_id, ancestor_user_id)`); } catch (_) {}
await ensureIndex(connection, 'user_tree_closure', 'idx_closure_instance_depth', '`matrix_instance_id`, `depth`');
await ensureIndex(connection, 'user_tree_closure', 'idx_closure_instance_ancestor', '`matrix_instance_id`, `ancestor_user_id`');
// Remove singleton constraint if present (best effort)
try {

View File

@ -1 +0,0 @@
safdasdffasdafd

View File

@ -1 +0,0 @@
safdasdffasdafd

View File

@ -1 +0,0 @@
safdasdffasdafd