Skip to content

SurrealKit

embed_schema! macro

The embed_schema! macro reads your database/schema/ directory at compile time and generates a Rust module containing the SQL for every .surql file it finds. Because the schema is compiled into the binary, there are no external files to deploy and the schema version is always tied to the application version.

Add SurrealKit to your dependencies. The embed_schema! macro is re-exported from the main crate, so a single dependency is enough:

[dependencies]
surrealkit = "1.0.0-beta.1"

Call the macro at the crate root (typically main.rs or lib.rs):

surrealkit::embed_schema!();

This generates an embedded_schema module. Call sync on it after connecting to apply any outstanding schema changes:

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let db = surrealkit::connect(
        &surrealkit::DbCfg::from_env(None, &Default::default())?
    ).await?;

    embedded_schema::sync(&db).await?;

    // application startup continues here
    Ok(())
}

sync behaves identically to surrealkit sync from the CLI: it applies new or changed definitions and prunes any that have been removed, using the __entity metadata table to track state.

Given a database/schema/ directory with these files:

database/schema/
├── users.surql
└── orders.surql

The macro generates roughly:

pub mod embedded_schema {
    pub static SCHEMA: &[surrealkit::EmbeddedSchemaFile] = &[
        surrealkit::EmbeddedSchemaFile {
            path: "database/schema/users.surql",
            sql: "DEFINE TABLE user SCHEMAFULL; ...",
        },
        surrealkit::EmbeddedSchemaFile {
            path: "database/schema/orders.surql",
            sql: "DEFINE TABLE order SCHEMAFULL; ...",
        },
    ];

    pub async fn sync(
        db: &surrealkit::Surreal<surrealkit::engine::any::Any>,
    ) -> surrealkit::anyhow::Result<()> {
        surrealkit::Sync::embedded(SCHEMA).run(db).await
    }
}

The generated SCHEMA static is public, so when you need to customise sync behaviour you can pass it to the Sync builder directly instead of calling embedded_schema::sync:

use surrealkit::Sync;

Sync::embedded(embedded_schema::SCHEMA)
    .prune(false)
    .run(&db)
    .await?;

Available since: v1.0

A project split into schema modules names each one and the directory it comes from:

surrealkit::embed_schema!(
    core    = "database/modules/core/schema",
    billing = "database/modules/billing/schema",
);

This generates a submodule per arm, alongside a top-level sync that applies all of them:

embedded_schema::sync(&db).await?;           // every module, in declaration order
embedded_schema::billing::sync(&db).await?;  // one module

Order the arms so that a module follows the ones it depends on. embedded_schema::sync exists in both the single- and named-module forms, so moving from one module to several needs no change at the call site. embed_schema!() and embed_schema!("dir") are unchanged.

embed_seed! is the counterpart for seed data, reading database/seed/**/*.surql at compile time:

surrealkit::embed_seed!();

embedded_seed::seed(&db).await?;

Embedded seeds are tracked in __seed exactly as filesystem seeds are, so each file runs once even though the application calls this on every start.

Cargo re-runs the macro whenever a .surql file in database/schema/ changes, because the macro registers each file with include_str!. This means schema changes always produce a fresh build, and there is no risk of shipping stale SQL.

Warning

include_str! registers the files that exist when the macro runs, so the directory listing itself is not tracked. Adding a .surql file does not trigger a rebuild, and the new file is silently absent from the binary. Add a build.rs to cover that:

-embed_schema!Runtime Sync
Schema locationCompiled into binaryBuilt from files at runtime
DeploymentNo schema files neededSchema directory must be present
Dev iterationRebuild required on changeFiles can be swapped without rebuild
Best forProduction builds, Docker imagesDevelopment, mounted volumes

For most production deployments embed_schema! is the right choice. For local development or environments that mount schema as a volume, building an EmbeddedSchemaFile slice at runtime and passing it to Sync::embedded is more convenient.

Was this page helpful?