From 4f51f1efb5ff809931cd5fac7e37be43ffdf526c Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 14 Apr 2026 03:55:09 +0200 Subject: [PATCH] Pin critical auth indexes to COLLATE "C" (ICU-drift immune) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit idx_users_email and idx_sessions_token_hash now use byte-order collation so a future ICU library bump cannot silently corrupt the indexes the way it did this week. The columns themselves retain their default collation; only the index comparison is C, which is safe for these because: - users.email is lowercased ASCII in practice - user_sessions.token_hash is SHA-256 hex (pure ASCII) Both are used for equality lookups only, never ORDER BY. Migration is idempotent, gated on app_settings.migration.text_indexes_c. Slug indexes on learning_* tables left at default for now — those are also ASCII in practice but under lighter load; the startup drift check + auto-REINDEX covers them. --- src/db/database.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/db/database.js b/src/db/database.js index b17ac18..30849f1 100644 --- a/src/db/database.js +++ b/src/db/database.js @@ -50,6 +50,37 @@ async function initDatabase() { console.warn('⚠️ Collation check skipped:', e.message); } + // Convert lookup-only text indexes to COLLATE "C" so a future ICU/glibc + // version bump cannot silently corrupt them. Idempotent — driven by a + // flag in app_settings so we only do the DROP/CREATE once. + try { + var done = await client.query( + "SELECT value FROM app_settings WHERE key = 'migration.text_indexes_c'" + ).catch(function() { return { rows: [] }; }); + if (!done.rows.length || done.rows[0].value !== 'true') { + console.log('Migrating text lookup indexes to COLLATE "C"...'); + var indexMigrations = [ + // users.email — primary auth lookup + 'DROP INDEX IF EXISTS idx_users_email', + 'CREATE INDEX idx_users_email ON users (email COLLATE "C")', + // user_sessions.token_hash — session auth + 'DROP INDEX IF EXISTS idx_sessions_token_hash', + 'CREATE INDEX idx_sessions_token_hash ON user_sessions (token_hash COLLATE "C")' + ]; + for (var i = 0; i < indexMigrations.length; i++) { + await client.query(indexMigrations[i]); + } + // Mark complete — create the setting row if the table exists. + await client.query( + "INSERT INTO app_settings (key, value) VALUES ('migration.text_indexes_c', 'true') " + + "ON CONFLICT (key) DO UPDATE SET value = 'true'" + ).catch(function() {}); + console.log('✅ Text indexes: migrated to COLLATE "C" (ICU-drift immune)'); + } + } catch (e) { + console.warn('⚠️ Index migration skipped:', e.message); + } + await client.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY,