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