Skip to main content

@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:
  1. Creates a ModelContainer with the schema
  2. Generates a SQLite database in iOS Simulator
  3. Exports fixtures to /tmp/FreezeRay/Fixtures/1.0.0/
  4. 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:
  1. Loads frozen fixtures from FreezeRay/Fixtures/1.0.0/
  2. Generates current schema and computes checksum
  3. Compares current checksum with frozen checksum
  4. Throws FreezeRayError.schemaDrift if they don’t match

Function Naming

The generated function names use underscores instead of dots:
VersionFreeze FunctionDrift 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")

Must Conform to VersionedSchema

 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:
  1. Right-click on @FreezeSchema
  2. Select Expand Macro
  3. 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!

2. Use Consistent Version Format

// ✅ 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.