Skip to main content

forge_runtime/migrations/
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_v001` (single migration, pre-1.0)
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
21/// Legacy migration name from older forge versions.
22/// If found, it will be considered equivalent to v001.
23pub const LEGACY_MIGRATION_NAME: &str = "0000_forge_internal";
24
25/// System migration v001: Initial FORGE schema.
26/// Creates all core tables for jobs, workflows, crons, observability, daemons, webhooks, etc.
27const V001_INITIAL: &str = include_str!("../../migrations/system/v001_initial.sql");
28
29/// A system migration with a version number.
30#[derive(Debug, Clone)]
31pub struct SystemMigration {
32    /// Version number (1, 2, 3, ...)
33    pub version: u32,
34    /// The SQL to execute
35    pub sql: &'static str,
36    /// Description of what this migration does
37    pub description: &'static str,
38}
39
40impl SystemMigration {
41    /// Get the migration name used in the database (e.g., `__forge_v001`).
42    pub fn name(&self) -> String {
43        format!("{}{:03}", SYSTEM_MIGRATION_PREFIX, self.version)
44    }
45
46    /// Convert to a Migration struct.
47    pub fn to_migration(&self) -> Migration {
48        Migration::new(self.name(), self.sql)
49    }
50}
51
52/// Get all built-in FORGE system migrations in version order.
53///
54/// These are applied in order before any user migrations.
55pub fn get_system_migrations() -> Vec<SystemMigration> {
56    vec![SystemMigration {
57        version: 1,
58        sql: V001_INITIAL,
59        description: "Initial FORGE schema with jobs, workflows, crons, daemons, webhooks, and auth",
60    }]
61}
62
63/// Get system migrations as Migration structs.
64pub fn get_builtin_migrations() -> Vec<Migration> {
65    get_system_migrations()
66        .into_iter()
67        .map(|m| m.to_migration())
68        .collect()
69}
70
71/// Get all system migrations SQL concatenated.
72///
73/// Use for test setup before running user migrations.
74/// In production, use [`get_builtin_migrations`] for versioned application.
75pub fn get_all_system_sql() -> String {
76    get_system_migrations()
77        .into_iter()
78        .map(|m| m.sql)
79        .collect::<Vec<_>>()
80        .join("\n\n")
81}
82
83/// Check if a migration name is a system migration.
84pub fn is_system_migration(name: &str) -> bool {
85    name.starts_with(SYSTEM_MIGRATION_PREFIX) || name == LEGACY_MIGRATION_NAME
86}
87
88/// Extract version number from a system migration name.
89/// Returns None if not a valid system migration name.
90pub fn extract_version(name: &str) -> Option<u32> {
91    if name == LEGACY_MIGRATION_NAME {
92        return Some(1);
93    }
94    if let Some(suffix) = name.strip_prefix(SYSTEM_MIGRATION_PREFIX) {
95        return suffix.parse().ok();
96    }
97    None
98}
99
100#[cfg(test)]
101#[allow(clippy::unwrap_used, clippy::indexing_slicing, clippy::panic)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn test_get_system_migrations() {
107        let migrations = get_system_migrations();
108        assert!(!migrations.is_empty());
109        assert_eq!(migrations[0].version, 1);
110        assert_eq!(migrations[0].name(), "__forge_v001");
111    }
112
113    #[test]
114    fn test_migration_sql_not_empty() {
115        let migrations = get_system_migrations();
116        for m in migrations {
117            assert!(!m.sql.is_empty(), "Migration v{} has empty SQL", m.version);
118        }
119    }
120
121    #[test]
122    fn test_migration_sql_contains_tables() {
123        let migrations = get_system_migrations();
124        let sql = migrations[0].sql;
125
126        // Verify all core tables are defined
127        assert!(sql.contains("forge_nodes"));
128        assert!(sql.contains("forge_leaders"));
129        assert!(sql.contains("forge_jobs"));
130        assert!(sql.contains("forge_cron_runs"));
131        assert!(sql.contains("forge_workflow_runs"));
132        assert!(sql.contains("forge_workflow_steps"));
133        assert!(sql.contains("forge_sessions"));
134        assert!(sql.contains("forge_subscriptions"));
135        assert!(sql.contains("forge_daemons"));
136        assert!(sql.contains("forge_webhook_events"));
137        assert!(sql.contains("forge_refresh_tokens"));
138        assert!(sql.contains("forge_oauth_clients"));
139        assert!(sql.contains("forge_oauth_codes"));
140    }
141
142    #[test]
143    fn test_is_system_migration() {
144        assert!(is_system_migration("__forge_v001"));
145        assert!(is_system_migration("__forge_v002"));
146        assert!(is_system_migration("__forge_v100"));
147        assert!(is_system_migration("0000_forge_internal")); // Legacy
148        assert!(!is_system_migration("0001_create_users"));
149        assert!(!is_system_migration("user_migration"));
150    }
151
152    #[test]
153    fn test_extract_version() {
154        assert_eq!(extract_version("__forge_v001"), Some(1));
155        assert_eq!(extract_version("__forge_v002"), Some(2));
156        assert_eq!(extract_version("__forge_v100"), Some(100));
157        assert_eq!(extract_version("0000_forge_internal"), Some(1)); // Legacy
158        assert_eq!(extract_version("0001_create_users"), None);
159        assert_eq!(extract_version("invalid"), None);
160    }
161
162    #[test]
163    fn test_system_migrations_version_ordering() {
164        let migrations = get_system_migrations();
165        for window in migrations.windows(2) {
166            assert!(
167                window[0].version < window[1].version,
168                "Migrations must be in ascending version order: v{} >= v{}",
169                window[0].version,
170                window[1].version,
171            );
172        }
173    }
174
175    #[test]
176    fn test_system_migration_to_migration() {
177        let sys = SystemMigration {
178            version: 1,
179            sql: "SELECT 1;",
180            description: "Test",
181        };
182        let m = sys.to_migration();
183        assert_eq!(m.name, "__forge_v001");
184        assert_eq!(m.up_sql, "SELECT 1;");
185    }
186}