pub const DEFAULT_LEDGER_SCHEMA: &str = "public";
pub const DDL_SCHEMA_MIGRATIONS: &str = r#"
CREATE TABLE IF NOT EXISTS public.schema_migrations (
id BIGSERIAL PRIMARY KEY,
filename TEXT NOT NULL UNIQUE,
checksum TEXT NOT NULL,
state TEXT NOT NULL DEFAULT 'applied',
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
applied_by TEXT NOT NULL DEFAULT current_user,
duration_ms INTEGER NULL,
migration_kind TEXT NOT NULL DEFAULT 'baseline',
proto_manifest_checksum TEXT NULL,
proto_table_checksum TEXT NULL,
source_schema TEXT NULL,
source_table TEXT NULL,
operation_kind TEXT NULL
);
CREATE INDEX IF NOT EXISTS idx_schema_migrations_filename
ON public.schema_migrations (filename);
CREATE INDEX IF NOT EXISTS idx_schema_migrations_state
ON public.schema_migrations (state);
CREATE INDEX IF NOT EXISTS idx_schema_migrations_kind
ON public.schema_migrations (migration_kind);
-- Upgrade guard: extend the table when migrating from a baseline schema.
ALTER TABLE public.schema_migrations
ADD COLUMN IF NOT EXISTS migration_kind TEXT NOT NULL DEFAULT 'baseline',
ADD COLUMN IF NOT EXISTS proto_manifest_checksum TEXT NULL,
ADD COLUMN IF NOT EXISTS proto_table_checksum TEXT NULL,
ADD COLUMN IF NOT EXISTS source_schema TEXT NULL,
ADD COLUMN IF NOT EXISTS source_table TEXT NULL,
ADD COLUMN IF NOT EXISTS operation_kind TEXT NULL;
"#;
pub const DDL_PROTO_SCHEMA_VERSIONS: &str = r#"
CREATE TABLE IF NOT EXISTS public.proto_schema_versions (
id BIGSERIAL PRIMARY KEY,
manifest_checksum TEXT NOT NULL UNIQUE,
manifest_json JSONB NOT NULL,
generator_version TEXT NOT NULL DEFAULT '1',
generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
applied_at TIMESTAMPTZ NULL,
applied_by_file TEXT NULL
);
CREATE INDEX IF NOT EXISTS idx_proto_schema_versions_checksum
ON public.proto_schema_versions (manifest_checksum);
CREATE INDEX IF NOT EXISTS idx_proto_schema_versions_applied_at
ON public.proto_schema_versions (applied_at)
WHERE applied_at IS NOT NULL;
-- Composite index to accelerate the ORDER BY applied_at DESC NULLS LAST, id DESC
-- used by load_last_manifest() on every startup. Without this, the query does a
-- full sequential scan of the JSONB column even though only 1 row is needed.
CREATE INDEX IF NOT EXISTS idx_proto_schema_versions_applied_at_id
ON public.proto_schema_versions (applied_at DESC NULLS LAST, id DESC);
"#;
pub const DDL_MIGRATION_ERROR_LOG: &str = r#"
CREATE TABLE IF NOT EXISTS public.migration_error_log (
id BIGSERIAL PRIMARY KEY,
run_id TEXT NOT NULL DEFAULT '',
failed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
fsm_state TEXT NOT NULL,
filename TEXT NULL,
error_message TEXT NOT NULL,
error_detail JSONB NULL,
resolved_at TIMESTAMPTZ NULL,
recovery_note TEXT NULL
);
CREATE INDEX IF NOT EXISTS idx_migration_error_log_run_id
ON public.migration_error_log (run_id);
CREATE INDEX IF NOT EXISTS idx_migration_error_log_failed_at
ON public.migration_error_log (failed_at DESC);
CREATE INDEX IF NOT EXISTS idx_migration_error_log_unresolved
ON public.migration_error_log (id)
WHERE resolved_at IS NULL;
"#;
pub const DDL_MIGRATION_RUNTIME_STATE: &str = r#"
CREATE TABLE IF NOT EXISTS public.migration_runtime_state (
singleton BOOLEAN PRIMARY KEY DEFAULT TRUE,
run_id TEXT NOT NULL DEFAULT '',
current_state TEXT NOT NULL DEFAULT 'IDLE',
active_file TEXT NULL,
retry_count INTEGER NOT NULL DEFAULT 0,
last_note TEXT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT migration_runtime_state_singleton_check CHECK (singleton = TRUE)
);
-- Insert the default IDLE row on first bootstrap.
INSERT INTO public.migration_runtime_state (singleton) VALUES (TRUE)
ON CONFLICT (singleton) DO NOTHING;
"#;
pub const ALL_TRACKER_DDL: &[&str] = &[
DDL_SCHEMA_MIGRATIONS,
DDL_PROTO_SCHEMA_VERSIONS,
DDL_MIGRATION_ERROR_LOG,
DDL_MIGRATION_RUNTIME_STATE,
];
pub fn all_tracker_ddl_sql() -> String {
all_tracker_ddl_sql_for_schema(DEFAULT_LEDGER_SCHEMA)
}
pub fn all_tracker_ddl_sql_for_schema(schema: &str) -> String {
let schema = if schema.trim().is_empty() {
DEFAULT_LEDGER_SCHEMA
} else {
schema.trim()
};
let escaped = schema.replace('"', "\"\"");
let mut ddl = format!("CREATE SCHEMA IF NOT EXISTS \"{escaped}\";\n\n");
ddl.push_str(
&ALL_TRACKER_DDL
.join("\n")
.replace("public.", &format!("\"{escaped}\".")),
);
ddl
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ddl_strings_are_non_empty() {
for ddl in ALL_TRACKER_DDL {
assert!(!ddl.trim().is_empty(), "tracker DDL must not be empty");
assert!(
ddl.to_uppercase().contains("CREATE TABLE IF NOT EXISTS"),
"each tracker DDL must contain CREATE TABLE IF NOT EXISTS"
);
}
}
#[test]
fn all_tracker_ddl_sql_contains_all_tables() {
let combined = all_tracker_ddl_sql();
for table in &[
"schema_migrations",
"proto_schema_versions",
"migration_error_log",
"migration_runtime_state",
] {
assert!(
combined.contains(table),
"combined DDL must reference table {table}"
);
}
}
}