package db import ( "database/sql" "fmt" ) const CurrentSchemaVersion = "2026-06-compat-baseline" func (s *Store) migrate(conn *sql.DB, d dialect) error { statements := []string{} if d.name == "sqlite" { statements = append(statements, "PRAGMA busy_timeout = 5000", "PRAGMA journal_mode = WAL", "PRAGMA foreign_keys = ON", ) } statements = append(statements, schemaStatements(d)...) for _, statement := range statements { if _, err := conn.Exec(d.rebind(statement)); err != nil { return err } } return s.recordSchemaVersion(conn, d) } func schemaStatements(d dialect) []string { keyText := d.keyTextType() shortText := d.shortTextType() mediumText := d.mediumTextType() longText := d.longTextType() return []string{ fmt.Sprintf(`CREATE TABLE IF NOT EXISTS schema_migrations ( version VARCHAR(64) NOT NULL PRIMARY KEY, applied_at %s NOT NULL, description VARCHAR(255) NOT NULL DEFAULT '' )`, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS admin_users ( id %s, username %s NOT NULL UNIQUE, password_hash %s NOT NULL, password_changed INTEGER NOT NULL DEFAULT 0, created_at %s NOT NULL, updated_at %s NOT NULL )`, d.idType(), keyText, shortText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS sessions ( id %s, session_id %s NOT NULL UNIQUE, username %s NOT NULL, csrf %s NOT NULL, expires_at %s NOT NULL, created_at %s NOT NULL )`, d.idType(), keyText, keyText, shortText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS release_packages ( id %s, product %s NOT NULL, version %s NOT NULL, platform %s NOT NULL, arch %s NOT NULL, file_name %s NOT NULL UNIQUE, url %s NOT NULL, sha256 %s NOT NULL, size_bytes BIGINT NOT NULL DEFAULT 0, enabled INTEGER NOT NULL DEFAULT 1, created_at %s NOT NULL, updated_at %s NOT NULL )`, d.idType(), keyText, keyText, keyText, keyText, keyText, mediumText, shortText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS release_notices ( id %s, version %s NOT NULL UNIQUE, build %s NOT NULL DEFAULT '', channel %s NOT NULL DEFAULT 'stable', title %s NOT NULL DEFAULT '', message %s NOT NULL, release_notes %s NOT NULL, message_md %s NOT NULL, release_notes_md %s NOT NULL, download_url %s NOT NULL DEFAULT '', notice_file %s NOT NULL DEFAULT '', raw_json %s NOT NULL, published_at %s NOT NULL DEFAULT '', created_at %s NOT NULL, updated_at %s NOT NULL )`, d.idType(), keyText, shortText, shortText, mediumText, longText, longText, longText, longText, mediumText, keyText, longText, shortText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS release_notice_revisions ( id %s, version %s NOT NULL, raw_json %s NOT NULL, note %s NOT NULL DEFAULT '', created_by %s NOT NULL DEFAULT '', created_at %s NOT NULL )`, d.idType(), keyText, longText, mediumText, keyText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_tickets ( code %s PRIMARY KEY, title %s NOT NULL, type %s NOT NULL, severity %s NOT NULL, category %s NOT NULL DEFAULT '', priority %s NOT NULL DEFAULT '', contact %s NOT NULL DEFAULT '', body %s NOT NULL, status %s NOT NULL, status_detail %s NOT NULL DEFAULT '', public_reply %s NOT NULL, note %s NOT NULL, assignee %s NOT NULL DEFAULT '', handled_by %s NOT NULL DEFAULT '', due_at %s NOT NULL DEFAULT '', resolved_at %s NOT NULL DEFAULT '', archived_at %s NOT NULL DEFAULT '', sla_level %s NOT NULL DEFAULT '', source_channel %s NOT NULL DEFAULT '', risk_score INTEGER NOT NULL DEFAULT 0, resolution %s NOT NULL, attachment %s NOT NULL DEFAULT '', package_path %s NOT NULL DEFAULT '', encrypted_package_path %s NOT NULL DEFAULT '', package_sha256 %s NOT NULL DEFAULT '', plain_package_sha256 %s NOT NULL DEFAULT '', summary_text %s NOT NULL, included_files %s NOT NULL, mail_sent INTEGER NOT NULL DEFAULT 0, remote_addr %s NOT NULL DEFAULT '', tags %s NOT NULL, created_at %s NOT NULL, updated_at %s NOT NULL, last_activity_at %s NOT NULL )`, keyText, mediumText, keyText, keyText, keyText, keyText, mediumText, longText, keyText, mediumText, longText, longText, keyText, keyText, shortText, shortText, shortText, keyText, keyText, longText, mediumText, mediumText, mediumText, shortText, shortText, longText, longText, shortText, longText, shortText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_comments ( id %s, feedback_code %s NOT NULL, author %s NOT NULL DEFAULT '', body %s NOT NULL, internal INTEGER NOT NULL DEFAULT 1, created_at %s NOT NULL )`, d.idType(), keyText, keyText, longText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_attachments ( id %s, feedback_code %s NOT NULL, kind %s NOT NULL, path %s NOT NULL, file_name %s NOT NULL, sha256 %s NOT NULL DEFAULT '', size_bytes BIGINT NOT NULL DEFAULT 0, created_at %s NOT NULL )`, d.idType(), keyText, keyText, mediumText, mediumText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_events ( id %s, feedback_code %s NOT NULL, event_type %s NOT NULL, actor %s NOT NULL DEFAULT '', from_value %s NOT NULL DEFAULT '', to_value %s NOT NULL DEFAULT '', message %s NOT NULL DEFAULT '', created_at %s NOT NULL )`, d.idType(), keyText, keyText, keyText, mediumText, mediumText, mediumText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS feedback_tags ( feedback_code %s NOT NULL, tag %s NOT NULL, created_at %s NOT NULL, PRIMARY KEY (feedback_code, tag) )`, keyText, keyText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS mail_records ( id %s, feedback_code %s NOT NULL DEFAULT '', kind %s NOT NULL DEFAULT '', status %s NOT NULL DEFAULT '', to_address %s NOT NULL DEFAULT '', subject %s NOT NULL DEFAULT '', plain_body %s NOT NULL, html_body %s NOT NULL, attachment_path %s NOT NULL DEFAULT '', attachment_name %s NOT NULL DEFAULT '', error_message %s NOT NULL, created_at %s NOT NULL, sent_at %s NOT NULL DEFAULT '' )`, d.idType(), keyText, keyText, keyText, mediumText, mediumText, longText, longText, mediumText, mediumText, longText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS source_categories ( id %s, category_id %s NOT NULL UNIQUE, name %s NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, ui_config %s NOT NULL, created_at %s NOT NULL, updated_at %s NOT NULL )`, d.idType(), keyText, shortText, longText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS source_endpoints ( id %s, category_id %s NOT NULL, category_name %s NOT NULL, source_id %s NOT NULL UNIQUE, name %s NOT NULL, description %s NOT NULL DEFAULT '', method %s NOT NULL DEFAULT 'GET', api_url %s NOT NULL DEFAULT '', url_template %s NOT NULL DEFAULT '', thumbnail_url %s NOT NULL DEFAULT '', proxy_mode %s NOT NULL DEFAULT 'client_direct', timeout_ms INTEGER NOT NULL DEFAULT 8000, retry_count INTEGER NOT NULL DEFAULT 1, cache_seconds INTEGER NOT NULL DEFAULT 300, check_interval_sec INTEGER NOT NULL DEFAULT 300, enabled INTEGER NOT NULL DEFAULT 1, client_visible INTEGER NOT NULL DEFAULT 1, supported_formats %s NOT NULL, last_status %s NOT NULL DEFAULT 'unknown', last_latency_ms INTEGER NOT NULL DEFAULT 0, last_checked_at %s NOT NULL DEFAULT '', last_error %s NOT NULL, consecutive_failure INTEGER NOT NULL DEFAULT 0, created_at %s NOT NULL, updated_at %s NOT NULL )`, d.idType(), keyText, shortText, keyText, shortText, mediumText, keyText, mediumText, mediumText, mediumText, keyText, longText, keyText, shortText, longText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS endpoint_health_checks ( id %s, source_db_id BIGINT NOT NULL, status %s NOT NULL, latency_ms INTEGER NOT NULL DEFAULT 0, error %s NOT NULL, checked_at %s NOT NULL )`, d.idType(), keyText, longText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS endpoint_call_logs ( id %s, source_id %s NOT NULL, status %s NOT NULL, latency_ms INTEGER NOT NULL DEFAULT 0, error %s NOT NULL, client %s NOT NULL DEFAULT '', created_at %s NOT NULL )`, d.idType(), keyText, keyText, longText, mediumText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS database_sync_jobs ( id %s, direction %s NOT NULL, status %s NOT NULL, message %s NOT NULL, tables_json %s NOT NULL, started_at %s NOT NULL, finished_at %s NOT NULL DEFAULT '' )`, d.idType(), keyText, keyText, longText, longText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS system_settings ( %s %s NOT NULL PRIMARY KEY, value %s NOT NULL, updated_at %s NOT NULL )`, d.quoteIdent("key"), keyText, longText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS legacy_sync_jobs ( id %s, status %s NOT NULL, summary %s NOT NULL, stats_json %s NOT NULL, started_at %s NOT NULL, finished_at %s NOT NULL DEFAULT '' )`, d.idType(), keyText, longText, longText, shortText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS audit_logs ( id %s, actor %s NOT NULL DEFAULT '', type %s NOT NULL, target %s NOT NULL DEFAULT '', message %s NOT NULL, ip %s NOT NULL DEFAULT '', user_agent %s NOT NULL DEFAULT '', created_at %s NOT NULL )`, d.idType(), keyText, keyText, keyText, longText, keyText, mediumText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS legacy_json_revisions ( id %s, name %s NOT NULL, raw %s NOT NULL, note %s NOT NULL DEFAULT '', created_by %s NOT NULL DEFAULT '', created_at %s NOT NULL )`, d.idType(), keyText, longText, mediumText, keyText, shortText), fmt.Sprintf(`CREATE TABLE IF NOT EXISTS webhook_deliveries ( id %s, webhook_name %s NOT NULL DEFAULT '', event %s NOT NULL DEFAULT '', status %s NOT NULL DEFAULT '', attempts INTEGER NOT NULL DEFAULT 0, response_code INTEGER NOT NULL DEFAULT 0, error_message %s NOT NULL, payload_sha256 %s NOT NULL DEFAULT '', created_at %s NOT NULL, finished_at %s NOT NULL DEFAULT '' )`, d.idType(), keyText, keyText, keyText, longText, shortText, shortText, shortText), `CREATE INDEX IF NOT EXISTS idx_feedback_tickets_activity ON feedback_tickets(last_activity_at)`, `CREATE INDEX IF NOT EXISTS idx_feedback_comments_code ON feedback_comments(feedback_code)`, `CREATE INDEX IF NOT EXISTS idx_feedback_attachments_code ON feedback_attachments(feedback_code)`, `CREATE INDEX IF NOT EXISTS idx_feedback_events_code ON feedback_events(feedback_code)`, `CREATE INDEX IF NOT EXISTS idx_mail_records_code ON mail_records(feedback_code)`, `CREATE INDEX IF NOT EXISTS idx_endpoint_call_logs_source ON endpoint_call_logs(source_id)`, `CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at)`, `CREATE INDEX IF NOT EXISTS idx_audit_logs_type ON audit_logs(type)`, `CREATE INDEX IF NOT EXISTS idx_audit_logs_target ON audit_logs(target)`, `CREATE INDEX IF NOT EXISTS idx_legacy_json_revisions_name ON legacy_json_revisions(name, id)`, `CREATE INDEX IF NOT EXISTS idx_release_notices_version ON release_notices(version)`, `CREATE INDEX IF NOT EXISTS idx_release_notice_revisions_version ON release_notice_revisions(version, id)`, } } func (s *Store) recordSchemaVersion(conn *sql.DB, d dialect) error { columns := []string{"version", "applied_at", "description"} _, err := conn.Exec(d.rebind(d.upsert("schema_migrations", columns, []string{"version"})), CurrentSchemaVersion, Now(), "unified-management layered monolith baseline", ) return err }