@FreezeSchema
The @FreezeSchema macro marks a SwiftData VersionedSchema for freezing and generates helper functions for freezing and drift detection.
Overview
@FreezeSchema is an attached macro that generates two static functions:
__freezeray_freeze_X_Y_Z() - Exports the schema to SQLite fixtures
__freezeray_check_X_Y_Z() - Validates the schema hasn’t drifted from the frozen version
These functions are called by the FreezeRay CLI and scaffolded tests.
Declaration
@attached (member, names : arbitrary)
public macro FreezeSchema ( version : String )
Parameters
version
Type: String
Required: Yes
The version identifier for this schema. Used to organize fixtures in FreezeRay/Fixtures/<VERSION>/.
Format: Any string, but semantic versioning is recommended:
"1.0.0" - Full semantic version
"1", "2" - Simple incremental
"2024-01-15" - Date-based
The version string is separate from SwiftData’s versionIdentifier. Use it to organize your frozen fixtures.
Usage
Basic Example
import SwiftData
import FreezeRay
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema {
static let versionIdentifier = Schema. Version ( 1 , 0 , 0 )
static var models: [ any PersistentModel. Type ] {
[User. self ]
}
@Model
final class User {
var name: String
var email: String
init ( name : String , email : String ) {
self . name = name
self . email = email
}
}
}
Multiple Schema Versions
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema {
static let versionIdentifier = Schema. Version ( 1 , 0 , 0 )
static var models: [ any PersistentModel. Type ] {
[User. self ]
}
// ... models ...
}
@FreezeSchema (version : "2.0.0" )
enum AppSchemaV2 : VersionedSchema {
static let versionIdentifier = Schema. Version ( 2 , 0 , 0 )
static var models: [ any PersistentModel. Type ] {
[User. self , Post. self ]
}
// ... models ...
}
@FreezeSchema (version : "3.0.0" )
enum AppSchemaV3 : VersionedSchema {
static let versionIdentifier = Schema. Version ( 3 , 0 , 0 )
static var models: [ any PersistentModel. Type ] {
[User. self , Post. self , Comment. self ]
}
// ... models ...
}
Generated Code
When you add @FreezeSchema(version: "1.0.0") to a schema, the macro generates:
Freeze Function
@available ( macOS 14 , iOS 17 , * )
static func __freezeray_freeze_1_0_0 () throws {
try FreezeRayRuntime. freeze (
schema : AppSchemaV1. self ,
version : "1.0.0"
)
}
Purpose: Exports the schema to SQLite fixtures.
Called by:
FreezeRay CLI during freezeray freeze 1.0.0
Temporary test generated by CLI
What it does:
Creates a ModelContainer with the schema
Generates a SQLite database in iOS Simulator
Exports fixtures to /tmp/FreezeRay/Fixtures/1.0.0/
CLI copies fixtures to your project
Drift Check Function
@available ( macOS 14 , iOS 17 , * )
static func __freezeray_check_1_0_0 () throws {
try FreezeRayRuntime. checkDrift (
schema : AppSchemaV1. self ,
version : "1.0.0"
)
}
Purpose: Validates the schema matches the frozen fixtures.
Called by:
Scaffolded drift tests
Your test suite (on every run)
What it does:
Loads frozen fixtures from FreezeRay/Fixtures/1.0.0/
Generates current schema and computes checksum
Compares current checksum with frozen checksum
Throws FreezeRayError.schemaDrift if they don’t match
Function Naming
The generated function names use underscores instead of dots:
Version Freeze Function Drift Check Function
"1.0.0"__freezeray_freeze_1_0_0()__freezeray_check_1_0_0()"2.1.3"__freezeray_freeze_2_1_3()__freezeray_check_2_1_3()"1"__freezeray_freeze_1()__freezeray_check_1()
The double underscore prefix (__) indicates these are internal functions not meant for direct use.
Availability
Both generated functions are marked with:
@available ( macOS 14 , iOS 17 , * )
This matches SwiftData’s minimum requirements.
Requirements
Must Be Applied to Enum
✅ Correct :
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema { /* ... */ }
❌ Error :
@FreezeSchema (version : "1.0.0" )
struct AppSchemaV1 : VersionedSchema { /* ... */ }
// Error: @Freeze can only be applied to enum declarations
Must Have Version Parameter
✅ Correct :
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema { /* ... */ }
❌ Error :
@FreezeSchema
enum AppSchemaV1 : VersionedSchema { /* ... */ }
// Error: @Freeze requires version argument: @Freeze(version: "1.4.0")
✅ Correct :
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema { /* ... */ }
❌ Error (no compile error, but won't work) :
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 { /* ... */ }
// Schema must conform to VersionedSchema
How It Works
Macro Expansion
The macro is a MemberMacro that adds members (functions) to the schema enum.
Before macro expansion:
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema {
static let versionIdentifier = Schema. Version ( 1 , 0 , 0 )
static var models: [ any PersistentModel. Type ] {
[User. self ]
}
}
After macro expansion:
enum AppSchemaV1 : VersionedSchema {
static let versionIdentifier = Schema. Version ( 1 , 0 , 0 )
static var models: [ any PersistentModel. Type ] {
[User. self ]
}
// ✅ Generated by macro:
@available ( macOS 14 , iOS 17 , * )
static func __freezeray_freeze_1_0_0 () throws {
try FreezeRayRuntime. freeze (
schema : AppSchemaV1. self ,
version : "1.0.0"
)
}
@available ( macOS 14 , iOS 17 , * )
static func __freezeray_check_1_0_0 () throws {
try FreezeRayRuntime. checkDrift (
schema : AppSchemaV1. self ,
version : "1.0.0"
)
}
}
Runtime Integration
The generated functions call FreezeRayRuntime methods:
public enum FreezeRayRuntime {
/// Freeze a schema version by generating fixture artifacts
public static func freeze < S : VersionedSchema >(
schema : S. Type ,
version : String ,
outputDirectory : URL ? = nil
) throws
/// Check if sealed schema has drifted from current definition
public static func checkDrift < S : VersionedSchema >(
schema : S. Type ,
version : String
) throws
}
See FreezeRayRuntime Reference for details on runtime behavior.
Common Use Cases
Freezing Multiple Schemas
Annotate each schema version:
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema { /* ... */ }
@FreezeSchema (version : "2.0.0" )
enum AppSchemaV2 : VersionedSchema { /* ... */ }
@FreezeSchema (version : "3.0.0" )
enum AppSchemaV3 : VersionedSchema { /* ... */ }
Then freeze each:
freezeray freeze 1.0.0
freezeray freeze 2.0.0
freezeray freeze 3.0.0
Version Naming Strategies
Semantic Versioning (Recommended):
@FreezeSchema (version : "1.0.0" ) // Initial
@FreezeSchema (version : "2.0.0" ) // Breaking changes
@FreezeSchema (version : "2.1.0" ) // New features
@FreezeSchema (version : "2.1.1" ) // Bug fixes
Simple Incremental:
@FreezeSchema (version : "1" )
@FreezeSchema (version : "2" )
@FreezeSchema (version : "3" )
Date-Based:
@FreezeSchema (version : "2024-01-15" )
@FreezeSchema (version : "2024-03-20" )
Schema Organization
Option 1: One file per version
Models/
├── SchemaV1.swift
├── SchemaV2.swift
└── SchemaV3.swift
// SchemaV1.swift
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema { /* ... */ }
// SchemaV2.swift
@FreezeSchema (version : "2.0.0" )
enum AppSchemaV2 : VersionedSchema { /* ... */ }
Option 2: All schemas in one file
// Schemas.swift
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema { /* ... */ }
@FreezeSchema (version : "2.0.0" )
enum AppSchemaV2 : VersionedSchema { /* ... */ }
@FreezeSchema (version : "3.0.0" )
enum AppSchemaV3 : VersionedSchema { /* ... */ }
enum AppMigrations : SchemaMigrationPlan {
static var schemas: [ any VersionedSchema. Type ] {
[AppSchemaV1. self , AppSchemaV2. self , AppSchemaV3. self ]
}
// ...
}
Debugging Macro Expansion
To see the expanded code, use Xcode’s macro expansion tool:
Right-click on @FreezeSchema
Select Expand Macro
View the generated code inline
Or use Swift’s -print-expanded-macro flag:
swift build -Xswiftc -print-expanded-macro
Error Messages
Invalid Version Argument
@FreezeSchema // ❌ Missing version
enum AppSchemaV1 : VersionedSchema { /* ... */ }
Error:
@Freeze requires version argument: @Freeze(version: "1.4.0")
Not Applied to Enum
@FreezeSchema (version : "1.0.0" )
struct AppSchemaV1 : VersionedSchema { /* ... */ } // ❌ struct instead of enum
Error:
@Freeze can only be applied to enum declarations
Best Practices
Never remove @FreezeSchema from a shipped schema. The generated drift check functions are called by tests.
1. Annotate All Schema Versions
// ✅ Good: All versions annotated
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema { /* ... */ }
@FreezeSchema (version : "2.0.0" )
enum AppSchemaV2 : VersionedSchema { /* ... */ }
// ❌ Bad: V2 not annotated
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema { /* ... */ }
enum AppSchemaV2 : VersionedSchema { /* ... */ } // Missing @FreezeSchema!
// ✅ Good: Consistent format
@FreezeSchema (version : "1.0.0" )
@FreezeSchema (version : "2.0.0" )
@FreezeSchema (version : "3.0.0" )
// ❌ Bad: Inconsistent format
@FreezeSchema (version : "1.0.0" )
@FreezeSchema (version : "2" )
@FreezeSchema (version : "2024-03-20" )
3. Freeze Before Shipping
Add @FreezeSchema before releasing to production:
// During development (not frozen yet)
enum AppSchemaV2 : VersionedSchema { /* ... */ }
// Before App Store submission
@FreezeSchema (version : "2.0.0" ) // ← Add this
enum AppSchemaV2 : VersionedSchema { /* ... */ }
Then run:
freezeray freeze 2.0.0
git add FreezeRay/
git commit -m "Freeze schema v2.0.0 for release"
4. Keep Annotations After Freezing
// ✅ Good: Keep annotation
@FreezeSchema (version : "1.0.0" )
enum AppSchemaV1 : VersionedSchema { /* ... */ }
@FreezeSchema (version : "2.0.0" )
enum AppSchemaV2 : VersionedSchema { /* ... */ }
Don’t remove annotations from old schemas - drift tests depend on the generated functions.
freeze command CLI command that calls the freeze function
Schema Freezing Learn how schema freezing works
Drift Detection How drift check functions detect changes
First Freeze Guide Tutorial for using @FreezeSchema