Skip to main content

forge_runtime/pg/migration/
builtin.rs

1//! Built-in FORGE schema migrations.
2//!
3//! These migrations create all internal tables required by the FORGE runtime.
4//! They use a version-based naming scheme (`__forge_vXXX`) to avoid conflicts
5//! with user migrations.
6//!
7//! # Migration Naming
8//!
9//! - System migrations: `__forge_vXXX` (pre-1.0: a single `__forge_v001`)
10//! - User migrations: `0001_xxx`, `0002_xxx`, etc.
11//!
12//! System migrations are always applied before user migrations, regardless of
13//! naming. This allows new forge features to be added without conflicting with
14//! existing user migration numbering.
15
16use super::runner::Migration;
17
18/// System migration prefix. All forge internal migrations use this prefix.
19pub const SYSTEM_MIGRATION_PREFIX: &str = "__forge_v";
20
21const V001_INITIAL: &str = include_str!("../../../migrations/system/v001_initial.sql");
22
23/// A system migration with a version number.
24#[derive(Debug, Clone)]
25pub struct SystemMigration {
26    pub version: u32,
27    pub sql: &'static str,
28    pub description: &'static str,
29}
30
31impl SystemMigration {
32    /// Get the migration version string used in the database (e.g., `__forge_v001`).
33    pub fn name(&self) -> String {
34        format!("{}{:03}", SYSTEM_MIGRATION_PREFIX, self.version)
35    }
36
37    pub fn to_migration(&self) -> Migration {
38        Migration::new(self.name(), self.sql)
39    }
40}
41
42/// All built-in FORGE system migrations in version order.
43pub fn get_system_migrations() -> Vec<SystemMigration> {
44    vec![SystemMigration {
45        version: 1,
46        sql: V001_INITIAL,
47        description: "FORGE internal schema",
48    }]
49}
50
51pub fn get_builtin_migrations() -> Vec<Migration> {
52    get_system_migrations()
53        .into_iter()
54        .map(|m| m.to_migration())
55        .collect()
56}
57
58/// All system SQL concatenated. Use for test setup; production code should use [`get_builtin_migrations`].
59pub fn get_all_system_sql() -> String {
60    get_system_migrations()
61        .into_iter()
62        .map(|m| m.sql)
63        .collect::<Vec<_>>()
64        .join("\n\n")
65}
66
67pub fn is_system_migration(name: &str) -> bool {
68    name.starts_with(SYSTEM_MIGRATION_PREFIX)
69}
70
71/// Returns `None` if `name` is not a valid system migration name.
72pub fn extract_version(name: &str) -> Option<u32> {
73    name.strip_prefix(SYSTEM_MIGRATION_PREFIX)
74        .and_then(|suffix| suffix.parse().ok())
75}
76
77#[cfg(test)]
78#[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::panic)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_get_system_migrations() {
84        let migrations = get_system_migrations();
85        assert_eq!(migrations.len(), 1);
86        assert_eq!(migrations[0].version, 1);
87        assert_eq!(migrations[0].name(), "__forge_v001");
88    }
89
90    #[test]
91    fn test_migration_sql_not_empty() {
92        let migrations = get_system_migrations();
93        for m in migrations {
94            assert!(!m.sql.is_empty(), "Migration v{} has empty SQL", m.version);
95        }
96    }
97
98    #[test]
99    fn test_migration_sql_contains_expected_objects() {
100        let v001 = get_system_migrations()[0].sql;
101
102        // Core tables
103        for table in [
104            "forge_nodes",
105            "forge_leaders",
106            "forge_kv",
107            "forge_kv_counters",
108            "forge_jobs",
109            "forge_jobs_history",
110            "forge_cron_runs",
111            "forge_workflow_definitions",
112            "forge_workflow_runs",
113            "forge_workflow_state",
114            "forge_workflow_events",
115            "forge_workflow_steps",
116            "forge_admin_audit",
117            "forge_paused_queues",
118            "forge_rate_limits",
119            "forge_change_log",
120            "forge_daemons",
121            "forge_webhook_events",
122            "forge_refresh_tokens",
123            "forge_oauth_clients",
124            "forge_oauth_codes",
125            "forge_signals_events",
126            "forge_signals_sessions",
127            "forge_signals_users",
128            "forge_signals_hourly_stats",
129            "forge_signals_daily_rollup",
130        ] {
131            assert!(v001.contains(table), "expected table {table} in v001 SQL");
132        }
133
134        // Key functions
135        for fn_name in [
136            "forge_validate_identifier",
137            "forge_notify_change",
138            "forge_notify_change_statement",
139            "forge_notify_job_available",
140            "forge_enable_reactivity",
141            "forge_disable_reactivity",
142            "forge_workflow_event_notify",
143            "forge_workflow_runs_cancel_notify",
144            "forge_cleanup_webhook_events",
145            "forge_cleanup_expired_jobs",
146            "forge_archive_completed_jobs",
147            "forge_purge_expired_refresh_tokens",
148            "forge_purge_expired_oauth_codes",
149            "forge_trim_change_log",
150            "forge_signals_ensure_partition",
151            "forge_signals_drop_old_partitions",
152            "forge_signals_roll_up_hour",
153            "forge_signals_roll_up_day",
154        ] {
155            assert!(
156                v001.contains(fn_name),
157                "expected function {fn_name} in v001 SQL"
158            );
159        }
160
161        // Notable columns and replay fields
162        for needle in [
163            "owner_subject",
164            "token_family",
165            "raw_body",
166            "raw_headers",
167            "tenant_id",
168        ] {
169            assert!(v001.contains(needle), "expected {needle} in v001 SQL");
170        }
171    }
172
173    #[test]
174    fn test_is_system_migration() {
175        assert!(is_system_migration("__forge_v001"));
176        assert!(is_system_migration("__forge_v002"));
177        assert!(is_system_migration("__forge_v100"));
178        assert!(!is_system_migration("0001_create_users"));
179        assert!(!is_system_migration("user_migration"));
180    }
181
182    #[test]
183    fn test_extract_version() {
184        assert_eq!(extract_version("__forge_v001"), Some(1));
185        assert_eq!(extract_version("__forge_v002"), Some(2));
186        assert_eq!(extract_version("__forge_v100"), Some(100));
187        assert_eq!(extract_version("0001_create_users"), None);
188        assert_eq!(extract_version("invalid"), None);
189    }
190
191    #[test]
192    fn test_system_migration_to_migration() {
193        let sys = SystemMigration {
194            version: 1,
195            sql: "SELECT 1;",
196            description: "Test",
197        };
198        let m = sys.to_migration();
199        assert_eq!(m.version, "__forge_v001");
200        assert_eq!(m.up_sql, "SELECT 1;");
201    }
202}