fix(db): reorder migration to run before index creation

Move migration logic (ALTER TABLE) before CREATE INDEX statements so
that user_id columns exist on old schemas before indexes reference them.
Disable foreign keys during schema setup to avoid constraint issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-03-09 01:10:37 +01:00
committed by Fred Cabassut
co-authored by Claude Opus 4.6
parent 0adf6051e9
commit b3e55ff95c
+59 -54
View File
@@ -16,9 +16,11 @@ const sqlite = new Database(DATABASE_URL);
// Enable WAL mode for better concurrent read performance
sqlite.pragma('journal_mode = WAL');
sqlite.pragma('foreign_keys = ON');
// Auto-create tables on first run
// Disable foreign keys during schema setup to avoid constraint issues during migration
sqlite.pragma('foreign_keys = OFF');
// Auto-create tables on first run (new installs get full schema)
sqlite.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -39,7 +41,7 @@ sqlite.exec(`
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id),
user_id INTEGER NOT NULL DEFAULT 0,
title TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT 'default',
@@ -56,7 +58,7 @@ sqlite.exec(`
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id),
user_id INTEGER NOT NULL DEFAULT 0,
name TEXT NOT NULL
);
@@ -67,7 +69,7 @@ sqlite.exec(`
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id),
user_id INTEGER NOT NULL DEFAULT 0,
note_id TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
mime_type TEXT NOT NULL,
@@ -80,8 +82,8 @@ sqlite.exec(`
CREATE TABLE IF NOT EXISTS sync_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id),
note_id TEXT NOT NULL REFERENCES notes(id),
user_id INTEGER NOT NULL DEFAULT 0,
note_id TEXT NOT NULL,
operation TEXT NOT NULL,
timestamp INTEGER NOT NULL,
client_id TEXT NOT NULL
@@ -94,7 +96,53 @@ sqlite.exec(`
success INTEGER NOT NULL,
timestamp INTEGER NOT NULL
);
`);
// Migration: detect old schema (no email column on users) and add columns
const userColumns = sqlite
.prepare("PRAGMA table_info('users')")
.all() as { name: string }[];
const hasEmail = userColumns.some((col) => col.name === 'email');
if (!hasEmail) {
sqlite.exec(`
ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user';
ALTER TABLE users ADD COLUMN auth_provider TEXT NOT NULL DEFAULT 'password';
ALTER TABLE users ADD COLUMN provider_id TEXT;
`);
}
// Ensure user_id column exists on data tables (migration from old schema)
const notesColumns = sqlite
.prepare("PRAGMA table_info('notes')")
.all() as { name: string }[];
if (!notesColumns.some((col) => col.name === 'user_id')) {
sqlite.exec(`
ALTER TABLE notes ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0;
ALTER TABLE tags ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0;
ALTER TABLE attachments ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0;
ALTER TABLE sync_log ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0;
`);
}
// Backfill: promote existing user to admin and assign all data to them
if (!hasEmail) {
const existingUser = sqlite.prepare('SELECT id FROM users LIMIT 1').get() as
| { id: number }
| undefined;
if (existingUser) {
const uid = existingUser.id;
sqlite.prepare('UPDATE users SET role = ? WHERE id = ?').run('admin', uid);
for (const table of ['notes', 'tags', 'attachments', 'sync_log']) {
sqlite.prepare(`UPDATE ${table} SET user_id = ? WHERE user_id = 0`).run(uid);
}
}
}
// Create indexes AFTER migration ensures columns exist
sqlite.exec(`
CREATE INDEX IF NOT EXISTS notes_user_id_idx ON notes(user_id);
CREATE INDEX IF NOT EXISTS notes_trashed_archived_idx ON notes(trashed, archived);
CREATE INDEX IF NOT EXISTS notes_updated_at_idx ON notes(updated_at);
@@ -104,11 +152,11 @@ sqlite.exec(`
CREATE INDEX IF NOT EXISTS login_attempts_ip_timestamp_idx ON login_attempts(ip, timestamp);
`);
// Drop the old unique index on tags(name) if it exists, replace with compound index
// Replace old tags unique index with compound index including user_id
try {
sqlite.exec(`DROP INDEX IF EXISTS tags_name_unique`);
} catch {
// Index may not exist
// ignore
}
sqlite.exec(
`CREATE UNIQUE INDEX IF NOT EXISTS tags_name_user_unique ON tags(name, user_id)`
@@ -126,51 +174,8 @@ try {
// Column already exists
}
// Migration: detect old schema (no email column on users) and migrate
const userColumns = sqlite
.prepare("PRAGMA table_info('users')")
.all() as { name: string }[];
const hasEmail = userColumns.some((col) => col.name === 'email');
if (!hasEmail) {
// Old single-user schema — add new columns
sqlite.exec(`
ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN display_name TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user';
ALTER TABLE users ADD COLUMN auth_provider TEXT NOT NULL DEFAULT 'password';
ALTER TABLE users ADD COLUMN provider_id TEXT;
`);
// Check if notes table has user_id column
const notesColumns = sqlite
.prepare("PRAGMA table_info('notes')")
.all() as { name: string }[];
const notesHasUserId = notesColumns.some((col) => col.name === 'user_id');
if (!notesHasUserId) {
sqlite.exec(`
ALTER TABLE notes ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id);
ALTER TABLE tags ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id);
ALTER TABLE attachments ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id);
ALTER TABLE sync_log ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0 REFERENCES users(id);
`);
}
// Backfill: promote existing user to admin and assign all data to them
const existingUser = sqlite.prepare('SELECT id FROM users LIMIT 1').get() as
| { id: number }
| undefined;
if (existingUser) {
const uid = existingUser.id;
const backfill = sqlite.prepare('UPDATE users SET role = ? WHERE id = ?');
backfill.run('admin', uid);
for (const table of ['notes', 'tags', 'attachments', 'sync_log']) {
sqlite.prepare(`UPDATE ${table} SET user_id = ? WHERE user_id = 0`).run(uid);
}
}
}
// Re-enable foreign keys
sqlite.pragma('foreign_keys = ON');
export const db = drizzle(sqlite, { schema });
export { sqlite };