@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.