use sqlx::AnyPool; pub async fn init_schema(pool: &AnyPool) { sqlx::query("PRAGMA foreign_keys = ON") .execute(pool) .await .expect("Failed to enable SQLite foreign keys"); // Rename the legacy "space" tables/columns to the "project" vocabulary on // existing databases. Best-effort: on a fresh database (or one already // migrated) the old names don't exist, so these fail silently and the // CREATE TABLE IF NOT EXISTS statements below take over. let rename_migrations = [ "ALTER TABLE spaces RENAME TO projects", "ALTER TABLE space_files RENAME TO project_files", "ALTER TABLE space_collaborators RENAME TO project_collaborators", "ALTER TABLE project_files RENAME COLUMN space_id TO project_id", "ALTER TABLE project_collaborators RENAME COLUMN space_id TO project_id", ]; for stmt in &rename_migrations { let _ = sqlx::query(stmt).execute(pool).await; } let statements = [ "CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, email TEXT UNIQUE, password_hash TEXT NOT NULL, is_admin INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", "CREATE TABLE IF NOT EXISTS folders ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), parent_id TEXT REFERENCES folders(id), name TEXT NOT NULL, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", "CREATE TABLE IF NOT EXISTS documents ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), folder_id TEXT REFERENCES folders(id), title TEXT NOT NULL, content BLOB, thumbnail_svg TEXT, public_role TEXT DEFAULT NULL, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", "CREATE TABLE IF NOT EXISTS files ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), document_id TEXT REFERENCES documents(id), folder_id TEXT REFERENCES folders(id), name TEXT NOT NULL, mime_type TEXT NOT NULL, data BLOB NOT NULL, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", "CREATE TABLE IF NOT EXISTS collaborators ( id TEXT PRIMARY KEY, document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, role TEXT NOT NULL, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), UNIQUE(document_id, user_id) )", "CREATE TABLE IF NOT EXISTS invitations ( id TEXT PRIMARY KEY, document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE, role TEXT NOT NULL, token TEXT NOT NULL UNIQUE, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), expires_at TEXT )", "CREATE TABLE IF NOT EXISTS comments ( id TEXT PRIMARY KEY, document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, content TEXT NOT NULL, resolved INTEGER DEFAULT 0, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", "CREATE TABLE IF NOT EXISTS document_history ( id TEXT PRIMARY KEY, document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE, content BLOB NOT NULL, created_by TEXT NOT NULL REFERENCES users(id) ON DELETE SET NULL, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", "CREATE TABLE IF NOT EXISTS document_versions ( id TEXT PRIMARY KEY, document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, content TEXT NOT NULL, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", "CREATE TABLE IF NOT EXISTS api_keys ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, key_hash TEXT NOT NULL UNIQUE, key_prefix TEXT NOT NULL, created_at TEXT NOT NULL, last_used_at TEXT, rate_limit INTEGER NOT NULL DEFAULT 60 )", "CREATE TABLE IF NOT EXISTS api_render_cache ( id TEXT PRIMARY KEY, content_hash TEXT NOT NULL UNIQUE, format TEXT NOT NULL, data BLOB NOT NULL, created_at TEXT NOT NULL )", "CREATE TABLE IF NOT EXISTS api_key_usage ( key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE, date TEXT NOT NULL, count INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(key_id, date) )", "CREATE TABLE IF NOT EXISTS api_key_usage_detail ( key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE, minute TEXT NOT NULL, count INTEGER NOT NULL DEFAULT 1, PRIMARY KEY(key_id, minute) )", "CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), folder_id TEXT REFERENCES folders(id), name TEXT NOT NULL, entrypoint TEXT NOT NULL DEFAULT 'main.typ', thumbnail_svg TEXT, public_role TEXT DEFAULT NULL, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')) )", "CREATE TABLE IF NOT EXISTS project_files ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, path TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'text', content BLOB, mime_type TEXT NOT NULL DEFAULT 'text/plain', created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), UNIQUE(project_id, path) )", "CREATE TABLE IF NOT EXISTS project_collaborators ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, role TEXT NOT NULL, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), UNIQUE(project_id, user_id) )", "CREATE TABLE IF NOT EXISTS packages ( id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), namespace TEXT NOT NULL DEFAULT 'typstdrive', name TEXT NOT NULL, description TEXT, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), UNIQUE(namespace, name) )", "CREATE TABLE IF NOT EXISTS package_versions ( id TEXT PRIMARY KEY, package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE, version TEXT NOT NULL, entrypoint TEXT NOT NULL DEFAULT 'lib.typ', manifest BLOB, created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')), UNIQUE(package_id, version) )", "CREATE TABLE IF NOT EXISTS package_files ( id TEXT PRIMARY KEY, version_id TEXT NOT NULL REFERENCES package_versions(id) ON DELETE CASCADE, path TEXT NOT NULL, data BLOB NOT NULL, UNIQUE(version_id, path) )", "CREATE TABLE IF NOT EXISTS device_tokens ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, token_hash TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, last_used_at TEXT )", ]; for stmt in &statements { sqlx::query(stmt) .execute(pool) .await .expect("Failed to execute SQLite schema"); } // Idempotent migrations for existing databases let migrations = [ "ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0", "ALTER TABLE users ADD COLUMN created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))", "ALTER TABLE project_files ADD COLUMN updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))", ]; for stmt in &migrations { let _ = sqlx::query(stmt).execute(pool).await; } }