rullst-orm-macros 13.0.0-alpha.2

Procedural macros and derive attributes for the rullst-orm Active Record pattern.
Documentation

rullst-orm-macros

rullst-orm-macros implements the derives re-exported by rullst-orm. Applications should normally depend on rullst-orm, not this proc-macro crate directly, because generated code calls the matching runtime API.

Derive and attribute contracts

Macro Bounded contract
#[derive(Orm)] Generates the SQLx Active Record/query surface for named-field structs, or the explicitly selected bounded Turso profile with #[orm(backend = "turso")].
#[derive(TursoModel)] Generates only the typed Turso/libSQL model contract and rejects SQLx-only relations, soft deletes, hooks, policies, tenants, audit, and search behavior.
#[rullst_orm::test] Runs an async test inside the task-scoped ORM transaction and rolls it back. The declared return type is kept, so a test returning Result can use ? and fails on Err after the rollback. Post-commit effects of sandboxed writes (committed observers, cache, Redis, Scout) are discarded like on any rollback. Code that opens a separate connection is outside that sandbox.
#[derive(PersonalData)] Declares application-selected personal-data fields (#[privacy], listed by ComplianceModel::personal_fields() in v13) and redacts them from Debug; it is metadata, not automatic privacy compliance. Its PrivacyReport names the ORM table (#[orm(table)] or the <struct>s default) and lists as encrypted only #[orm(encrypted)] and SecretString columns.
#[derive(Enum)] Generates a closed bounded label contract shared by string parsing/display, Serde, RullstValue and SQLx codecs. #[rullst_enum(type_name = "...", rename_all = "snake_case")] and per-variant rename are validated at compile time; schema DDL is owned by Blueprint::native_enum.
#[derive(Nexus)] Generates bounded model metadata consumed by the authenticated Nexus runtime. #[orm(tenant = "organization_id")] or the equivalent #[nexus(...)] opts a text field into Nexus-wide trusted-context scoping and makes it hidden/read-only. ORM relation fields and #[orm(skip)]/#[sqlx(skip)] fields are left out of the metadata; #[orm(encrypted)], SecretString and #[orm(hidden)] fields become hidden, read-only Password fields (only #[nexus(kind = "password")] exposes an #[orm(hidden)] field, write-only), #[orm(masked)] fields default to Password, and #[orm(db_default)] fields are read-only. Other shared #[orm(...)] options are skipped.

Compile-time safety boundaries

The Orm parser is fail-closed. It uses structured syn nested-meta parsing and rejects unknown or duplicate model/field options instead of silently ignoring them. Every model must expose a persisted id; explicitly configured tenant, soft-delete, and embedding targets must name persisted fields. Table, column, relation-key, and pivot identifiers use the 1–64 byte portable ASCII identifier grammar, while hook, policy, scope, and model names must also be valid Rust identifiers. Persisted Rust field names follow that same portable SQL grammar: raw identifiers, non-ASCII names, and names longer than 64 bytes are rejected before SQL generation. A field that cannot form its generated Rust column-enum variant receives a compile error instead of a macro panic. Generated SQL emits these identifiers unquoted and the derive does not check reserved words, so a table or column named like a keyword of the target database (order, desc, user on PostgreSQL, groups on MySQL 8, a default groups table for a Group struct) compiles but fails at runtime; rename it or set #[orm(table = "...")].

Exactly one relation declaration is accepted per relation field. Orphan relation options are rejected, local_key is rejected on belongs_to/morph_to and related_key on has-one/has-many/morph-one/morph-many relations (which would ignore them), belongs_to_many requires a pivot table, cascade_soft_delete is limited to has-one/has-many whose related model also uses soft deletes (otherwise the generated cascade fails to compile at the relation field rather than hard-deleting the children), and polymorphic metadata is limited to morph relations. The generated many-to-many foreign/related keys default to the owner and related model names when omitted; an omitted foreign_key defaults to <related model>_id on belongs_to and to <owner model>_id on has-one/has-many (lowercased model names).

The derive recognizes #[sqlx(skip)], #[sqlx(default)], #[sqlx(json)], and #[sqlx(json(nullable))]. #[orm(skip)] only removes a field from generated SQL; the application's FromRow still reads it, so a field without a table column also needs #[sqlx(skip)] (the two may be combined) or #[sqlx(default)]. #[orm(db_default)] (v13) keeps a field in reads, filters and to_json() but leaves it out of the generated INSERT (so the column DEFAULT applies) and of the full-row UPDATE of save(); update_partial() gets no setter for it and #[derive(Nexus)] shows it read-only. It is rejected on id, the tenant column, relations, skipped and encrypted fields, and by Turso-primary models, whose codec writes every field. chrono date/time fields (DateTime<Utc>, NaiveDateTime, NaiveDate, NaiveTime) bind through SQLx's codecs, which exist only for the strict driver features. A #[sqlx(json)] field is decoded through SQLx Json and generated writes bind it as Json(value) (a json(nullable) None as NULL), so a Serde-only type works on a JSON column; SQLx provides Json only for the strict driver features. SQLx mappings such as rename, try_from, and flatten fail compilation because the generated persistence SQL cannot honor them safely. On the model itself only #[sqlx(default)] is accepted; rename_all and other container options fail compilation for the same reason. The parser also rejects unsupported model shapes, unknown backends, missing or unbindable tenant columns, invalid encrypted field types, unsafe audit fields, malformed polymorphic relations, and SQLx-only behavior on Turso-primary models.

Generated query values remain parameterized by the runtime; raw SQL escape hatches are caller-owned. Soft-delete sentinel expressions are compile-time literals capped at 128 bytes and reject separators, NUL, and SQL comments, but they remain author-supplied SQL fragments rather than parameterized data. Comment rejection covers --, block-comment delimiters, and MySQL's #. Database enums accept 1–64 unit variants with unique labels of at most 63 bytes from the portable ASCII allowlist. PostgreSQL native enums require the strict-postgres runtime profile; SQLx Any cannot decode its custom types. Builder comparisons on a field whose type implements DatabaseEnum bind CAST(? AS "<type_name>") on PostgreSQL; the derive detects such fields at compile time through the field type, not its name.

Optional generated APIs follow the runtime's features, not the application's: rullst-orm opts this crate into runtime-feature-gates and forwards its redis and ai features, so the Redis cache/hash/event code and save_with_embedding are emitted or omitted at expansion time. Without that opt-in (an older runtime) the output keeps the legacy #[cfg(feature = "redis")]/#[cfg(feature = "ai")] attributes, which the invoking crate evaluates.

Randomized encrypted fields and SecretString columns cannot be used as ordinary generated filter/order/group columns or plucked (SecretString columns can still be selected, because their codec decrypts them). Tenant scope and model policies are generated only when explicitly declared; the macro does not authenticate a principal or authorize unscoped access. Post-commit callbacks are process-local unless the application composes the transactional outbox.

Generated mandatory scopes form separate AND groups around user filters; empty positive IN predicates remain false. Scalar projections and nested queries preserve identifier validation and the managed transaction context. Policy models reject bulk deletion rather than skipping per-row checks. See the ORM query and transaction boundaries for the explicit raw-transaction and streaming limitations.

Verification

The unit suite inspects generated SQL/bind ordering and zero-panic production tokens. Twenty-seven trybuild compile-fail cases exercise the actual parser diagnostics, including duplicate/unknown options and cross-field invariants, rather than an unresolved import:

cargo test -p rullst-orm-macros --all-features

Backend runtime behavior is verified in rullst-orm against the corresponding SQLite, PostgreSQL, MySQL/MariaDB, Turso, Redis, vector, and search matrices. This crate alone does not prove those external protocols.