Skip to content

Latest commit

 

History

History
1389 lines (1109 loc) · 64.4 KB

File metadata and controls

1389 lines (1109 loc) · 64.4 KB
title Migration Guide
kind reference
status active
owner workspace-maintainers
last_reviewed 2026-08-31
review_by 2027-02-01
supersedes

Migration Guide

graphql-orm is distributed from GitHub only. Use a reviewed full 40-character commit in rev; neither the runtime nor macros crate is published to crates.io.

0.31.0 to 0.31.1: patched SQL Server XML dependency

Adopt runtime and macros 0.31.1 together from one reviewed workspace release. The MSSQL notification parser now uses quick-xml 0.41. Existing input-size, notification-shape and separately authorized Broker-connection requirements remain in place. No Rust call-site, GraphQL SDL, persisted-schema or database migration is required. This dependency update does not grant queue consumption, DML or schema-management authority to an existing read-only connection.

0.30.0 to 0.31.0: native SQL Server query notifications

Adopt the runtime and macros 0.31.0 together from one reviewed full Git revision. The optional MSSQL notification API uses a separately authorized connection; existing read-only pools do not acquire queue-consumption or write capability. External database owners must provision Broker and its dedicated queue/service. See SQL Server notification setup. No managed schema migration is required or performed. Notifications are one-shot invalidation hints, not durable row-change events; applications own re-registration, catch-up, permissions, and reconnect/resynchronization behavior.

0.29.0 to 0.30.0: checked calendar-date filters

Adopt graphql-orm and graphql-orm-macros 0.30.0 together from one reviewed full Git revision. Backend-neutral semantic consumers must adopt graphql-orm-operation-catalog 0.4.0 from that revision. The AI tool-profile package remains 0.10.2; rebuild its generated capabilities against the new semantic catalogue.

Generated calendar predicates now use half-open ranges over the persisted column: today is [today, tomorrow), future begins at tomorrow, the past ends at today, RecentDays(N) includes exactly today plus N - 1 preceding dates, and WithinDays(N) includes exactly N dates beginning today. LteRelative is inclusive through its selected calendar date by rendering an exclusive next-date upper bound. Ordinary Eq, Ne, Lt, Lte, Gt, and Gte continue binding the supplied value without normalization.

DateRangeInput.start and DateRangeInput.end are now required String fields. Update Rust struct literals that supplied Option<String>. Both bounds must be parseable date/timestamp values and start must not follow end. RecentDays and WithinDays accept 1 through 36,600; signed relative offsets accept -36,600 through 36,600. Generated query execution validates recursively and returns INVALID_INPUT before database work. Handwritten DatabaseFilter implementations remain source compatible because validate has a permissive default; implement it when a custom filter has structured invariants.

The meaning of today remains backend-owned and is now internally consistent: PostgreSQL uses session CURRENT_DATE, SQL Server uses the server-local date from GETDATE(), and SQLite uses UTC date('now'). SQLite's in-memory spatial fallback captures one UTC date per filtering pass and does not push clock-dependent predicates into its SQL prefilter. This release does not add a cross-backend application-timezone setting or normalize exact input strings. A future typed date/time input and context-bound calendar-clock design requires separate compatibility review.

Date semantic metadata and catalogue fingerprints change. Rebuild reviewed catalogues, automatic query capabilities, and retained bindings. There is no database schema, stored-data, backup, or migration-history change.

0.27.0 to 0.29.0: computed/relation ordering and complex relation composition

Computed fields can join generated ordering without a handwritten query:

#[graphql_orm(order_expression(
    name = "Duration",
    expression = "COALESCE(finished_at, :as_of) - started_at",
    parameters = "duration_order_parameters"
))]

The expression is trusted, backend-specific server configuration. Requests continue to supply only ASC or DESC. For :named binds, the configured synchronous function receives &async_graphql::Context and returns async_graphql::Result<OrderExpressionParameters>; use SqlValue variants for the values. Raw ?, $n, and @Pn placeholders are rejected. Programmatic queries using a contextual expression call EntityQuery::order_by_with_context. Generated pagination now adds missing primary-key columns as ascending tie-breakers. Review the resulting query plan and add an expression/index strategy where the selected backend supports one.

SortExpression now carries bound values so query rendering can number order-expression placeholders after filter placeholders on every backend. Handwritten parameter-free sort construction should use SortExpression::unbound("column ASC"); handwritten struct literals must add values: Vec::new().

A readable relation can expose a server-generated count order without a projection:

#[relation(
    target = "StaffAssignment",
    from = "id",
    to = "policy_id",
    multiple,
    order_aggregate(name = "AssignedStaffCount", aggregate = "count")
)]

Requests again supply only ASC or DESC. The macro obtains the target table from its entity type and generates the correlated count from the declared key mapping. The initial contract supports count on unconditional, readable relations; conditional relations fail compilation when combined with order_aggregate.

For a relation entity that already has a handwritten #[ComplexObject] impl, add #[graphql_orm(compose_complex_object)] to the entity and replace that impl attribute with #[graphql_complex_object]. Handwritten methods remain unchanged; generated relations retain their batching and arguments. Do not leave both complex-object attributes on the impl.

There is no table, column, constraint, or stored-data migration. Regenerate and review GraphQL SDL and semantic/capability fingerprints when adding a new ordering field. Review the query plan for correlated relation counts and add indexes on the target key columns used by frequently sorted relations.

0.26.0 to 0.27.0: conditional polymorphic relationships

Adopt graphql-orm and graphql-orm-macros 0.27.0 together from one reviewed full Git revision. Existing relation declarations need no change.

Externally managed tables that reuse one reference column for multiple target types can declare a fixed source discriminator:

#[relation(
    target = "Document",
    from = "reference_id",
    to = "id",
    source_condition(field = "reference_kind", equals = 1),
    emit_fk = false
)]
pub document: Option<Document>,

The inverse collection can constrain the target rows:

#[relation(
    target = "Activity",
    from = "id",
    to = "reference_id",
    target_condition(column = "reference_kind", equals = 1),
    multiple,
    emit_fk = false
)]
pub activity: Vec<Activity>,

Condition values accept string, integer, float, and boolean literals. A source condition must name a persisted scalar Rust field with a matching type; target conditions name a physical target column and use the same trusted, bound-value SQL path as generated filters. Conditional relationships always require emit_fk = false because the discriminator makes the join conditional rather than a physical foreign-key contract.

No database, table, column, constraint, or stored-data migration is required. After adding conditional relationships, regenerate the semantic catalogue and any derived capability fingerprints so the new selectable graph is reviewed and deployed atomically.

0.25.1 to 0.26.0: agql-auth 0.18.0 alignment

Git consumers using auth-agql must align direct agql-auth dependencies to 0.18.0 at merged revision 527d15d28e3c295a6f6b5e6d74559a7aecdc1322. Adopt aligned graphql-orm and graphql-orm-macros 0.26.0 from the same reviewed monorepo revision. The bridge remains projection-only. Hosts adopting typed authorization-role grants follow the upstream issuer/resource-server rollout; no ORM schema, migration, generated-code, database-policy, or stored-data change is required.

0.25.0 to 0.25.1: agql-auth 0.17.1 alignment

Git consumers using auth-agql must align direct agql-auth dependencies to 0.17.1 at merged revision b792241b7d9bb46bad81dde4534ae5b39857f614. Adopt aligned graphql-orm and graphql-orm-macros 0.25.1 from the same reviewed monorepo revision. The upstream patch makes default session-context serialization compact; no API, schema, migration, generated-code, database-policy, or stored-data change is required in this workspace.

0.24.0 to 0.25.0: agql-auth 0.17 alignment

Git consumers using auth-agql must align direct agql-auth dependencies to 0.17.0 at merged revision 82650a447f3b6875160254fa1293b3a0e305c224. The role-scope expansion contract is additive and opt-in; the ORM bridge does not fetch catalogues or change authorization decisions. No schema, migration, generated code, or database-policy change is required. Adopt aligned graphql-orm and graphql-orm-macros 0.25.0 from the same reviewed revision.

0.23.0 to 0.24.0: agql-auth 0.16 alignment

Hosts enabling auth-agql or depending directly on agql-auth must align to version 0.16.0 at merged revision 3bc38cd94794f1e868a9cc3a5551047b95a32105. Adopt graphql-orm and the aligned graphql-orm-macros 0.24.0 from the same reviewed monorepo revision. This retains one public auth type universe. Version 0.16 adds consumer-supplied exact-only hierarchical matcher policy; the ORM bridge remains projection-only and does not select or install that policy. No database, schema, GraphQL SDL, generated-code, token, backup, or stored-data migration is required.

0.22.1 to 0.23.0: explicit collection-bound semantics

Update graphql-orm, graphql-orm-macros, and graphql-orm-operation-catalog together to 0.23.0 / 0.3.0 at one reviewed full Git revision. No database, table, column, constraint, or AI schema-module migration is required.

Regenerate semantic catalogues and automatic query capabilities. Many relationships now require collection_bound. Old catalogue version 1 payloads are rejected. Server-fixed object lists no longer accept a model-selected maximumItems and use the declared semantic ceiling in disclosure.

0.22.0 to 0.22.1: relationship semantic argument correction

Update graphql-orm and graphql-orm-macros together to 0.22.1 at one reviewed full Git revision. No resolver SDL, database schema, table, column, constraint, data, or AI schema-module migration is required.

The canonical semantic catalogue now describes a generated to-many relationship's nullable ordering argument as ChildOrderByInput, matching the resolver SDL. Root list queries continue to describe and accept [ChildOrderByInput!] as before. Regenerate semantic catalogues and automatic query capability fingerprints after adoption; do not preserve fingerprints derived from the incorrect relationship argument shape.

AiGraphqlQueryCapabilityCatalog::compile remains strict. Any future catalogue/SDL type, nullability, list-item, name, or argument-set mismatch continues to fail readiness.

0.22.0 canonical GraphQL semantic catalogue

Update graphql-orm and graphql-orm-macros together to 0.22.0 at one reviewed full Git revision. Backend-neutral consumers use graphql-orm-operation-catalog 0.2.0.

Existing entity and operation declarations continue to compile. The former minimal borrowed GraphqlEntitySemanticMetadata values are now owned, serializable catalogue values with typed field capabilities. Code constructing those structs directly must migrate to the expanded fields or, preferably, consume Entity::graphql_semantic_metadata() and graphql_orm_semantic_catalog().

Descriptions resolve from an explicit semantic description, then Rust doc comments, then a stable humanized public-name fallback. Use the same doc text when an entity also derives async_graphql::SimpleObject; that external derive owns its SDL field documentation. Sensitive fields are now structurally Secret and NeverExport, while private and read = false fields remain absent.

Handwritten resolver roots can publish canonical metadata beside their async-graphql impl:

#[derive(Default)]
struct HealthQueries;

#[graphql_orm_custom_operations(kind = "query", authorization = true)]
#[async_graphql::Object]
impl HealthQueries {
    /// Returns current service health.
    #[graphql_orm(
        result_classification = "public",
        result_export = "exportable"
    )]
    async fn health(&self) -> String {
        "ok".to_owned()
    }
}

schema_roots! {
    entities: [Account],
    described_query_types: [HealthQueries],
}

The described_query_types, described_mutation_types, and described_subscription_types lists replace the need to repeat one root in both an extra_* list and semantic_custom_operations; direct GraphQLSemanticObject results are collected as well. Legacy lists remain supported, but the same root cannot use both forms.

Unclassified custom scalar/enum leaves now fail safe as Secret/NeverExport and therefore do not become automatic AI capabilities. Declare result_classification and result_export together only for a reviewed exportable root. An exportable scalar/enum list additionally requires positive result_maximum_items; custom enum/scalar wrappers may need the explicit result_type_kind. Object results continue deriving authority-neutral disclosure from selected fields, and a root declaration can only tighten it.

Generated aggregate operation fingerprints now include their owning public entity identity. Compiled aggregate plan, projection, and disclosure fingerprints also bind exact selected grouping fields and metric field/operator pairs. Refresh any stored host allowlists; a response with unselected or drifted aggregate identities is rejected before provider egress.

The catalogue and per-operation semantic fingerprints intentionally change when public descriptions, fields, relationships, capabilities, classifications or exposure change. They are drift evidence, never authority. There is no database, stored-data, backup, migration-history, or AI schema-module migration. Generated broadcast subscriptions are now described as best-effort and are not eligible for a durable wait. A custom subscription may declare bounded replay_then_live semantics only beside its resolver; downstream runtimes must still register and verify the matching authoritative cursor/watermark source.

Public mutations now carry a closed AI execution classification in semantic catalogue v1. Existing declarations remain Prohibited and therefore acquire no AI capability. Opt in generated categories with ai_mutations(...), or add ai_execution = "automatic" | "approval_required" | "prohibited" to an existing mutation graphql_orm_custom_operations declaration. This changes semantic fingerprints only. It does not change SDL, resolver behavior, database schema, stored rows, or authorization.

Typed grouped aggregates are additive. Every GraphQLEntity now emits a closed aggregate-field enum for repository use, while no GraphQL root is added unless the entity declares aggregate = true. That opt-in changes the schema and operation catalogue by adding the bounded aggregate query. Decimal fields must declare #[graphql_orm(decimal(precision = P, scale = S))]; SQLite stores their exact scaled integer representation while PostgreSQL and SQL Server use native fixed-precision types. Adding a new Decimal column is an ordinary application schema migration, but adopting this release without declaring one requires no database or stored-data migration. Logical backups containing a Decimal field use the new exact Decimal value kind and include precision/scale in the schema hash. Produce and restore those backups with 0.22.0 or later; string-typed Decimal backup values are rejected instead of being coerced.

SQL Server compatibility constructors remain physically read-only, so existing MSSQL applications acquire no write authority after upgrading. To adopt DML against an externally managed schema, change both boundaries explicitly:

#[repository_entity(
    backend = "mssql",
    table = "dbo.WorkItems",
    schema_policy = "external_writable"
)]
struct WorkItem { /* externally reviewed columns */ }

let database = Database::<MssqlBackend>
    ::connect_ado_external_writable(connection_string)
    .await?;

Review the database principal, table contract, entity/field/row policies, upsert key uniqueness, and transaction behavior before adopting this mode. ExternalWritable permits generated row DML only. It does not enable SQL Server migration planning/application, managed RLS/search structures, runtime-schema writes, or backup/restore. No ORM schema or stored-data migration is performed by this release.

Generated writes in 0.22.0 also tighten the row-authorization boundary without changing public top-level method signatures. Update/delete/bulk helpers now evaluate the locked preimage and perform exact-key DML on one pinned transaction. Generated upsert helpers select state-machine isolation. If application code calls MutationContext::upsert inside an explicit Database::transaction(TransactionMode::Default, ...), change that transaction to TransactionMode::StateMachine and retry the complete callback when a serialization conflict is classified retryable. Default-mode upserts now fail closed instead of accepting an unfenced absent-key decision. No table, column, stored-data, migration-history, backup, GraphQL SDL, or AI schema-module migration is required.

0.21.1 agql-auth 0.15 session-bound delegation alignment

Update graphql-orm and graphql-orm-macros together to 0.21.1 at one reviewed full Git revision. Applications that directly use agql-auth must align that dependency to the same exact source:

agql-auth = { git = "https://github.com/Dastari/agql-auth.git", rev = "e841ffd382082ad7419be259fe957f949b956ff7", version = "0.15.0" }

The optional auth-agql bridge remains a one-way principal projection and requires no source changes. Version 0.15 adds upstream session-bound, access-token-only delegation types; it does not make the ORM an issuer or session authority. Hosts using those APIs must install an authoritative VerifiedActiveUserSessionResolver, narrow current authority, and retain the exact actor, resource, correlation, and registered-operation bindings.

There is no ORM schema, GraphQL SDL, migration-history, backup, stored-data, or agql-auth database migration. Remove older direct pins rather than resolving two auth package/type universes.

0.21.0 Backend-neutral operation catalog and companion derives

Update graphql-orm and graphql-orm-macros together to 0.21.0 at one reviewed full Git revision. Existing imports of resolver-operation metadata through graphql_orm::graphql::orm remain source compatible; the canonical types now live in graphql-orm-operation-catalog 0.1.0 so database-neutral manifest producers do not have to select an ORM backend.

Reusable companion packages exposing mutually exclusive sqlite, postgres, and mssql features may replace #[graphql_entity(...)] with #[backend_selected_graphql_entity(...)]. The latter accepts the same entity arguments except backend and supplies the backend from the companion crate's own selected feature. Application entities with a fixed backend should keep the ordinary explicit backend = "..." declaration.

No database, SDL, migration-history, backup, operation fingerprint, or stored data migration is required.

0.20.0 Semantic entity descriptions

Update graphql-orm and graphql-orm-macros together to 0.20.0 at one reviewed full Git revision. Existing derives and handwritten Entity implementations require no source changes.

Hosts may add a public entity description and public field descriptions:

#[graphql_entity(
    table = "records",
    plural = "Records",
    description = "Records visible in reviewed application workflows"
)]
struct Record {
    #[primary_key]
    #[graphql_orm(description = "Stable public record identity")]
    id: String,
}

Read the resulting GraphqlEntitySemanticMetadata through Entity::graphql_semantic_metadata. The derive omits private/non-readable fields and never includes physical column identities or policy keys. This is descriptive metadata only; consumers must still explicitly select fields, assign disclosure classification, and perform ordinary authorization.

There is no database, SDL, backup, or stored-data migration. The optional router-protocol feature now resolves protocol crate 0.2.0, whose wire payload remains protocol major 1.

0.19.0 Compound foreign keys and directional indexes

Update graphql-orm and graphql-orm-macros together to 0.19.0 at one reviewed full Git revision. Existing entity declarations using the original index = "a,b" and single-column relation forms remain source compatible.

Compound managed relations now produce one physical constraint. Declare the ordered Rust source fields and ordered target database columns together:

#[relation(
    target = "SnapshotRecord",
    from = ["provider", "tenant_key", "generation"],
    to = ["provider", "tenant_key", "generation"],
    on_delete = "cascade"
)]
snapshot: Option<SnapshotRecord>,

Every target member must be present in the same managed schema model and the ordered tuple must be an exact primary key, composite unique declaration, or unconditional unique index. Source members are translated through #[graphql_orm(db_column = "...")]; target members remain database-column names. A migration that previously passed only the referencing entity may need to include the referenced repository/schema entity so uniqueness and type compatibility can be proven.

Named directional ordinary indexes use the nested form:

#[graphql_entity(index(
    name = "idx_snapshot_latest",
    columns = ["provider", "tenant_key", "generation"],
    directions = ["asc", "asc", "desc"]
))]

Omitting directions keeps every column ascending. If supplied, its arity must match columns. Existing string index declarations keep generated names.

Use #[graphql_orm(min_exclusive = 0)] or #[graphql_orm(max_exclusive = 100)] when an existing check uses strict comparison. Use #[graphql_orm(default = false)] only to suppress the conventional implicit default inferred for a created_at or updated_at field; SQL-expression defaults remain string literals.

Public model migration

ForeignKeyModel now owns column_pairs: Vec<ForeignKeyColumnPairModel> and an optional observed constraint_name instead of scalar source/target column fields. Replace single-column struct literals with ForeignKeyModel::single(...); compound callers construct the ordered pair list. IndexDef adds column_directions; constructor-based callers need no change, while direct struct literals must supply &[] for compatibility or an exact direction slice. These changes are why the aligned packages advance as a pre-1.0 minor version.

Existing database adoption

There is no adoption flag or unchecked migration-history bypass. Plan against the live database normally. When introspected primary keys, unique keys, foreign-key members/order/target/delete action, ordinary index name/order, and supported check expressions are semantically equal, the plan is empty and ordinary apply_migration records the version without DDL. This preserves all rows and constraints.

The check comparator intentionally accepts only a closed grammar of simple identifiers, literals, comparison operators, parentheses, and commas. It normalizes physical names, whitespace, keyword case, redundant balanced outer parentheses, and quoting of ordinary lowercase portable identifiers; case-sensitive quoted identifiers remain distinct. It does not infer mathematical or type-affinity equivalence. Unsupported, weakened, partial, reordered, or ambiguous contracts remain explicit drift and must not be force-adopted.

Schema hashes now bind ordered compound-FK members and effective index directions while treating physical check/FK names as non-semantic. Refresh any reviewed hash expectations and run an empty replan plus backend integrity checks before removing a legacy migration path. No application row or GraphQL schema migration is otherwise required.

0.18.0 Router metadata and agql-auth 0.14 scopes

Update graphql-orm and graphql-orm-macros together to 0.18.0 at one reviewed full Git revision. Generated operation authorization declarations, the optional router-protocol export, and standard Federation authorization metadata are additive source and schema capabilities, but generated operation authorization fingerprints advance to version 2. Rebuild generated code and review any consumer that persists or compares those fingerprints.

Existing databases, schema modules, RLS policy, and stored application data do not require migration. Router adoption is separately opt-in and requires each host to expose its finished SDL and matching protocol descriptor.

The workspace now pins agql-auth 0.14.0 at exact revision 413fda3435f060604cd653c11e2cc18a668aace1:

agql-auth = { git = "https://github.com/Dastari/agql-auth.git", rev = "413fda3435f060604cd653c11e2cc18a668aace1", version = "0.14.0" }

Update any direct host dependency at the same time so Cargo resolves one package and type universe. The auth-agql bridge continues consuming the normalized AuthPrincipal::scopes() vector, so it has no Rust API, GraphQL, database, RLS, or stored-data migration.

The upstream access-token wire default is breaking: newly issued access JWTs use the OAuth space-delimited scope string instead of the pre-0.14 scopes array. Upstream validators accept both by default for a bounded transition; direct JWT decoders must add standard-claim support before issuer cutover. Purpose tokens retain their separate scopes array. Follow the upstream 0.13-to-0.14 staged migration and wait for the maximum old access-token TTL plus validation leeway before rejecting legacy claims.

0.17.0 Provider-Neutral Operation Assurance

Update graphql-orm and graphql-orm-macros together to 0.17.0 at the final reviewed full revision. If auth-agql is enabled, match its exact upstream dependency:

agql-auth = { git = "https://github.com/Dastari/agql-auth.git", rev = "413fda3435f060604cd653c11e2cc18a668aace1", version = "0.14.0" }

This release is additive and opt-in. Existing schemas install no assurance enforcement, apply no interactive-mutation default, and retain their current query, mutation, subscription, authorization, RLS, SDL, and database behavior.

Before

let schema = schema_builder(database).finish();

After: staged adoption

First build a compatibility registry and audit the current mutation surface:

let registry = OperationAssuranceRegistry::builder(
    graphql_orm_operation_catalog(),
    AssuranceSchemaConfig::legacy(),
).build()?;
let audit = registry.audit();

Then register custom resolver fields, assign machine/service/safety-teardown actor classes, and give every exposed mutation either require(...) or exempt(...). Custom fields use DeclaredAssuranceGuard; generated fields already call the runtime hook.

Next configure an interactive default while strict mode remains off:

let config = AssuranceSchemaConfig::legacy()
    .with_default_interactive_mutation_policy("interactive.recent-auth")?;

Install AssuranceEnforcement only after the upstream policy set, injected clock, accepted request principal, error handling, and step-up UX are ready. With auth-agql, use AgqlAssuranceEvaluator. Server denials expose lowercase extension key code with STEP_UP_REQUIRED, UNAUTHENTICATED, or FORBIDDEN.

Finally enable with_strict_mutation_classification(true) and make ensure_complete() or audit().assert_complete() a CI gate. Export manifest() for client codegen and schema_metadata() for directive-aware schema tooling. Both are descriptive/advisory; keep server enforcement enabled for every protected operation.

Compatibility and rollback

No database, stored-data, GraphQL-data, session, or token migration is needed. Queries and subscriptions never inherit the mutation default. Ordinary resolver auth and authorization continue independently; an assurance exemption does not grant authority. Machine/API-token principals cannot satisfy a user session requirement through the upstream evaluator and must be explicitly classified according to server policy.

To roll back, remove AssuranceEnforcement from schema data and restore AssuranceSchemaConfig::legacy() (or stop building the registry), then return both ORM crates to the prior reviewed exact revision. Remove the client manifest expectation at the same time. Existing database rows and authentication sessions require no rewrite.

See Operation assurance for the complete API and trust boundaries.

0.16.0 Generated Resolver Operation Metadata

Update both Git-only graphql-orm crates to 0.16.0 at the reviewed final full revision. Existing entity declarations and schema_roots! blocks require no source changes. GraphQLOperations now also implements GraphqlOperationMetadata; schema_roots! adds the graphql_orm_operation_catalog() helper in the same module as its existing schema helpers.

Hosts that previously reconstructed resolver names from entity/plural names should instead select the exact generated root coordinate from the catalog:

let operation = graphql_orm_operation_catalog()
    .resolve(GraphqlOperationKind::Query, "users")
    .expect("reviewed generated operation remains exposed");
let generated_fingerprint = operation.fingerprint();

Do not substitute the catalog fingerprint for a complete finished-schema registry fingerprint when a host composes custom roots. Do not treat metadata discovery as registration, enablement, authorization, or disclosure approval. Document operation names, server-authored document hashes, selected result fields, model-facing argument schemas, static disclosure contracts, and current resolver/RLS authorization remain separate host/downstream bindings.

Generated mutation none/allowlist/denylist policy now appears as is_exposed() == false on the corresponding resolved descriptors; repository writes and generated subscription behavior are unchanged. A root-level external_read_only policy also reports derive-generated mutation and subscription descriptors as unexposed because that root composes neither operation type. Private typed read projections remain repository-only and do not add descriptors or change generated GraphQL operation fingerprints. List categories describe connection shape rather than a fixed runtime bound; retain explicit host/tool output limits because PaginationConfig remains configurable.

This is an additive pre-1.0 public API/generated-code release. Existing GraphQL SDL, resolver execution, database schema, stored data, migrations, backups, authorization, and RLS behavior do not change. No database or data migration is required. Refresh lockfiles, update both aligned versions, and rerun backend, naming, schema exposure, and consumer fingerprint gates.

0.15.0 Exact Bounded-Mutation Sentinels

Update both Git-only graphql-orm crates to 0.15.0 at the final reviewed full revision. The release corrects generated single/composite bounded update and delete plus retention purge when MutationLimit is 100 or greater. Hosts do not need to change existing calls: the generated implementation now obtains the exact maximum + 1 sentinel without applying the public 100-row page cap.

Public GraphQL, connection, repository, runtime-query, and PageInput limits are unchanged. No uncapped public read surface is introduced. Residual or in-memory filters on bounded mutations now return stable INVALID_INPUT before mutation; replace such filters with a completely database-rendered predicate. Database-renderable filters preserve exact all-or-nothing results, and any selected-versus-affected cardinality change fails the transaction.

No database schema, stored data, GraphQL SDL, entity declaration, or backend migration is required. Refresh lockfiles and rerun high-ceiling overflow, authorization/RLS, event, rollback, and restart checks. The optional agql-auth bridge remains released 0.12.0 at exact revision 3f3b0c5365adfbe436514a681d977b600991b797; matching direct host dependencies must keep that exact version and full revision so one type universe resolves.

0.14.0 agql-auth 0.12 Bridge Alignment

Update both Git-only graphql-orm crates to 0.14.0 at the final reviewed full revision. When the host also depends on agql-auth, its dependency must match the bridge exactly:

agql-auth = { git = "https://github.com/Dastari/agql-auth.git", rev = "3f3b0c5365adfbe436514a681d977b600991b797", version = "0.12.0" }

This yields one agql-auth package/type universe. Do not use a branch, local path override, abbreviated revision, or different version requirement.

The public converter functions and exact identity/authorization-context mappings remain available. The bridge now omits malformed or structurally inconsistent session assurance, and it copies only the documented string policy_version from AccessTokenMetadata.additional. Hosts that read other custom values from AuthSubject.claims.additional must move those values into an explicit application-owned request context. This observable narrowing makes the release 0.14.0; macro syntax/output is unchanged, but the companion version advances under the aligned release policy.

Direct agql-auth users must separately follow its 0.10→0.12 migration. The 0.11 AuthRateLimitStore contract is atomic and versioned; graphql-orm supplies no implementation and requires no ORM schema migration. The 0.12 typed EssentialAcrs request and matched_acrs outcome remain provider evidence. They do not enter the bridge or imply local MFA. Only a host-constructed, session-bound, structurally consistent SessionAssurance can populate AuthAssurance/DbAuthContext.assurance; the exact MfaAcceptance decision is retained. A mapped microsoft-entra/acrs/c1 context remains distinct from standard scalar acr, AMR, roles, scopes, tenant, and policy_version.

No database schema, stored data, GraphQL SDL, generated code, resolver naming, or backend migration is required. Refresh lockfiles and inspect cargo tree for one agql-auth revision before running the host's authorization and live restart gates; compilation alone is not endpoint approval.

0.13.0 Runtime Relation Batching and PostgreSQL Constraint Introspection

This Git-only 0.13.0 release contains two coordinated prompts: validated runtime relation batching and PostgreSQL constraint-index upgrade idempotency. Update both aligned crates to the final reviewed full revision.

Existing static entities, GraphQL SDL, generated repositories/relations, transactions, authorization/RLS, top-level gormrq1 and legacy cursors, RuntimeRecord, serialized RuntimeSchema, third-party backend traits, and stored rows require no source or data migration. The new gormrr1 relation cursor is accepted only by RuntimeRelationBatchRequest and must not be translated to or from another cursor family. Hosts opting into runtime relations replace host SQL/N+1 loaders with an anchored parent read followed by explicit bounded relation layers. Request next-layer relation keys with runtime_relation_batch_request_with_relation_keys, then pass the opaque anchors returned by RuntimeRelationBatch::relation_parents into the next batch request; see Validated runtime relation batching.

PostgreSQL operators should replan a complete generated target before rollout. Constraint-owned PRIMARY KEY/UNIQUE backing indexes are now classified as constraints, not secondary indexes, so an unchanged schema or table-additive upgrade produces no DropIndex for them. Composite UNIQUE metadata now renders as UNIQUE (a, b) during new table creation and introspects in key order. No existing constraint or index is rewritten merely by upgrading the library. If an older managed table was created without a declared composite UNIQUE because of the former CREATE TABLE omission, the live/target mismatch is real and requires an explicit reviewed migration; do not mark an old module version as newly applied.

MSSQL static reads remain compatible and runtime relation execution returns unsupported_backend before I/O. No macros or entity declarations changed; the macros version advances only under the repository's aligned Git-only release policy.

0.12.0 Runtime Query Execution

Update both Git-only crates to 0.12.0 at the reviewed full Git revision. This is an additive pre-1.0 public runtime API release; macro declaration/generated behavior is unchanged, but the aligned companion version advances under the repository release policy.

Existing static entities, repository/GraphQL reads, mutations, transactions, authorization, RLS, keyset cursors, third-party OrmBackend implementations, database schemas, and serialized RuntimeSchema documents need no source, cursor, schema, or data migration. The new gormrq1 cursor is used only by RuntimeReadRequest and is intentionally distinct from static/legacy cursors. Do not translate or accept offset cursors on this boundary.

Runtime-schema hosts may replace their own SQL/filter/order/row-decoding layer with the validated constructors on ValidatedRuntimeSchema and Database::execute_runtime_read. Re-resolve handles after schema activation; old fingerprints fail closed. Hosts must compile authorization constraints to a second RuntimePredicate and structurally combine it with the application predicate. See Runtime queries.

No migration is required. MSSQL continues to compile and retain static reads, but returns the stable unsupported capability for runtime execution.

0.11.0 Repository-Only Entities

Update both Git-only crates to 0.11.0 at the final reviewed full Git revision. This is an additive pre-1.0 minor release: it introduces the public RepositoryEntity derive, #[repository_entity(...)] declaration attribute, RepositoryQuery, repository field-policy callbacks, and generated ordinary Rust DTO/input types. Existing GraphQLEntity, GraphQLSchemaEntity, GraphQLOperations, schema roots, SDL, and stored schemas remain compatible.

To move a persisted entity out of GraphQL entirely, replace its GraphQL derives and #[graphql_entity(...)] with:

#[derive(RepositoryEntity, Clone, serde::Serialize, serde::Deserialize)]
#[repository_entity(
    backend = "sqlite",
    table = "credentials",
    plural = "Credentials",
    default_sort = "username ASC"
)]
struct Credential { /* unchanged persisted fields */ }

Remove it from schema_roots!; attempting to register it is now an intentional compile error. Replace the legacy pool-bound list builder with Credential::query(&database). Primary/unique lookups, writes, projections, and MutationContext calls retain their generated names. Repository-only create/update types include writable private fields, so host duplicate DTOs are not needed.

Existing FieldPolicy implementations compile unchanged. Fields with no declared key keep the existing repository decision; a field carrying read_policy/write_policy is denied by the new default repository callbacks until the provider implements can_read_repository_field and/or can_write_repository_field. This deliberate fail-closed behavior prevents a missing GraphQL Context from becoming authority.

Changing only the generation surface does not alter SchemaModel, migration plans, schema/module fingerprints, backup descriptors, tables, indexes, constraints, RLS, or data. No data or schema migration is required. Sensitive repository mutation-hook snapshots/events are more restrictive by design: hook state is redacted and cannot be downcast to the original entity, and change events omit the entity payload when the declaration has a sensitive field. Before-write input hooks remain typed and can deliberately transform the value. See Repository-only entities.

0.10.0 Runtime Record Read Foundation

Update both Git-only crates to 0.10.0 at the reviewed full Git revision. Repository release policy keeps companion versions aligned for a public runtime API release; declaration syntax and generated code are unchanged.

This is an additive pre-1.0 runtime API release. Existing derived entities, repositories, GraphQL SDL, database schemas, migration history, backups, authorization, and static backend behavior require no source or data migration. Existing OrmBackend implementations remain source-compatible; the new RuntimeRowDecoder capability is separate and defaults to a fail-closed unsupported result.

Runtime-schema hosts may replace host-owned value/row-decoding models with RuntimeValue, RuntimeRecord, and handles resolved from their existing ValidatedRuntimeSchema. Resolve fresh handles after catalog/schema activation: owned handles and records are bound to the creating schema fingerprint, and mixing generations returns schema_mismatch. SQLite UUID, JSON, and datetime fields must use the documented text representation; PostgreSQL uses native UUID/JSON(B)/TIMESTAMPTZ types. Datetimes canonicalize to UTC at PostgreSQL-compatible rounded microsecond precision.

No database migration is required. MSSQL runtime-row decoding is deliberately unsupported in this slice; its static generated reads are unchanged. See Runtime values, records, handles, and row decoding.

0.9.0 Bounded Append-Only Retention Purge

This release adds public runtime and generated surfaces and extends public schema/backup descriptors, so it is a pre-1.0 minor release rather than a patch. Update both crates to 0.9.0 at the final reviewed release revision.

Derive-generated entities need no source change unless they opt in. Code that constructs public descriptors manually must add disabled retention state:

  • RuntimeCollection { retention_purge: false, .. };
  • TableModel { retention_purge: false, .. };
  • EntityBackupDescriptor { retention_purge: false, .. } (older serialized descriptors default this field to false);
  • EntityMetadata literals add retention_policy: None. Existing EntityMetadata::from_schema calls remain source-compatible; only code that deliberately supplies a retention key uses from_schema_with_retention; and
  • exhaustive matches on EntityAccessSurface, RuntimeSchemaDiagnosticCode, or MigrationStep::SetAppendOnly must handle the new retention variant/field.

Low-level backend capability traits gained safe default methods that reject retention maintenance, so out-of-tree implementations remain source-compatible unless they deliberately opt into this contract. SQLite and PostgreSQL provide the supported implementations. Existing format-v1 owned runtime-schema JSON without retention_purge continues to deserialize with the capability disabled.

Existing append-only entities and fingerprints remain unchanged. To opt in, add a dedicated policy key such as retention_purge = "audit.retention.purge", register an EntityPolicy that allows only EntityAccessSurface::RetentionMaintenance, and replace raw purge SQL with Database::retention_transaction[_with_auth] plus RetentionContext::purge. Keep ordinary write policy keys separate.

Enabling or disabling retention changes managed enforcement and requires a new host/module migration version. SQLite adds the reserved, structurally validated __graphql_orm_retention_context table and replaces the DELETE trigger. PostgreSQL replaces the append-only function contract and, when managed RLS is enabled, adds the transaction-local retention DELETE policy. There is no row data rewrite. Validate foreign-key behavior and the intended cutoff before enabling physical deletion. See Bounded append-only retention maintenance.

0.8.0 Owned Runtime Schema IR

Update both graphql-orm and graphql-orm-macros to 0.8.0 at the same reviewed Git revision. This is a breaking pre-1.0 release, not an additive one, in two respects:

1. Public metadata struct fields. ColumnDef and FieldMetadata gained api_name: &'static str, is_sortable: bool, and is_date_time: bool. Code built through the derives or the ColumnDef const builders needs no changes. Code constructing either struct as a literal must add the new fields (api_name defaults to the column name through ColumnDef::new; both flags default to false), or switch to the builders: .api_name(...), .sortable(), .date_time().

2. Nullable-byte logical identity. Option<Vec<u8>> fields previously reported logical type Json while storing BYTEA/BLOB; they now correctly report Bytes. Column DDL, stored data, row decoding, and generated GraphQL are unchanged. For entities with nullable byte columns:

  • stable_schema_hash and schema-module fingerprints that include such an entity change. Bump the semantic version of any OrmSchemaModule whose fingerprint covers one, and regenerate recorded fingerprints.
  • Backups taken before 0.8.0 that contain such an entity will fail 0.8.0 hash-compatibility verification. Take fresh backups after upgrading. Keep the old archives: they remain readable by pre-0.8.0 binaries, and their bytes are not corrupted — only the recorded schema identity differs. Do not overwrite or prune them until a post-upgrade backup has been verified.
  • Logical backups written by 0.8.0 record byte columns as Bytes (their values were already binary).

Entities without nullable byte columns keep their existing hashes, fingerprints, and backup compatibility; no database or data migration is required for anyone.

The new runtime_schema module is additive API surface. Runtime query execution, migration planning from the IR, and dynamic GraphQL registration are not part of this release.

0.7.1 Backend Dependency Isolation

Update graphql-orm to 0.7.1 at the reviewed full Git revision. The companion graphql-orm-macros crate remains 0.7.0. Existing feature declarations do not change:

graphql-orm = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.7.1", default-features = false, features = ["sqlite"] }

Cargo now resolves only the selected backend driver. There is no Rust API, generated-code, GraphQL SDL, schema, configuration, or database migration. Remove any downstream workaround that patched SQLx features, refresh the lock file, and confirm the selected graph with the commands in Testing and verification.

0.7.0 Schema Modules, Fenced Leases, and Bidirectional Keysets

Update both graphql-orm and graphql-orm-macros to 0.7.0 at the same reviewed Git revision. If auth-agql is enabled or the host directly uses agql-auth, align it to version 0.10.0 at revision c92dcb441237bbe308499b26525945f60ffa394a so Cargo resolves one public type universe.

Existing entities, generated GraphQL SDL, mutations, offset connections, and stored cursors remain valid. No automatic database or data migration is required.

The ORM bridge API and mapped principal/session-assurance data are unchanged. Hosts that directly use agql-auth OIDC state and opt into 0.10 bound reauthentication must follow its 0.10 migration guide: persisted OAuthLoginState records gain an optional authorization-policy value, and a decomposed relational store needs a nullable column before enabling the new writer. Hosts that only consume the ORM principal bridge need no data migration.

Dependency crates that own private tables may implement OrmSchemaModule and compose a SchemaModuleCatalog. Applying the resulting schema target remains a host-controlled migration operation and must use a fresh host migration/module version whenever an owned entity, index, constraint, or persistent semantic changes. Backup and restore code should persist SchemaModulesSnapshot and run the declared restore phases through the owning dependency.

FencedLeaseState is a backend-neutral transition contract, not a replacement for an atomic database predicate. Claims, heartbeats, child writes, and release must compare resource, owner, attempt, fencing token, unexpired deadline, and CAS row version in the same persistence operation.

Entities with configured keyset ordering gain the additive repository method keyset_connection_page. Use first with optional after for forward reads, or last with optional before for backward/tail reads. Limits remain bounded by the database pagination configuration. Existing generated GraphQL keyset and offset fields are unchanged.

0.6.3 Federation Operation Roots

Update the runtime to 0.6.3 and the companion macros crate to 0.6.1 at the same reviewed Git revision. No Rust call-site or database migration is required: QueryRoot, MutationRoot, and SubscriptionRoot remain the generated Rust names.

The generated GraphQL object names change to Query, Mutation, and Subscription. This makes federation SDL valid when the exporter relies on GraphQL's conventional implicit operation roots. If schema tooling explicitly matched the old GraphQL type names, update those matches. Empty mutation and subscription roots remain absent rather than becoming fieldless objects.

Regenerate provider SDL, parse or validate it, and run the federation composition check described in Federation operation roots before promotion.

0.6.2 agql-auth Bridge Alignment

Update graphql-orm to the reviewed v0.6.2 commit and align any direct agql-auth dependency to version 0.8.1 at exact revision f1fb5fe8c42d29806821d5f1a9032b007dee63e4. This ensures Cargo resolves one agql-auth package and one set of public types. No bridge API, authorization behavior, database migration, or generated-code change is required. The companion macros crate remains 0.6.0.

0.6.0 Auth Assurance and Typed Composite Mutations

Pin both crates to the reviewed v0.6.0 commit. This release keeps existing single-key and 0.5 projection APIs, but adding assurance/organization/correlation fields to the public AuthSubject and DbAuthContext structs is a source change for direct struct literals. Prefer AuthSubject::builder and DbAuthContext { ..Default::default() }.

The optional auth-agql bridge now requires Git-only agql-auth 0.8.0 revision be4e0a213ce9c9b9fbe9fe985602743a584e019b. It retains session assurance and safe policy/audit metadata. Remove any direct 0.7 pin so Cargo resolves exactly one agql-auth version.

Natural composite-key writes are opt-in:

#[graphql_entity(
    repository_mutations = true,
    upsert = "tenant_id,natural_id",
    unique_composite = "tenant_id,natural_id"
)]

Mark every key field #[primary_key] and host-assigned with #[graphql_orm(auto_generated = false)]. Use the generated EntityKey, CreateEntityInput, and UpdateEntityInput with find_by_key, insert, insert_if_absent, upsert, update_by_key, delete_by_key, update_if, and bounded typed filter mutations. These APIs add no GraphQL mutation fields. See Typed Composite-Key and Bounded Mutations.

MutationLimit::new is required by bounded operations; an overflow returns LimitExceeded without changing rows. Legacy update_where/delete_where remain available for source compatibility and should be migrated when the caller needs a reviewable hard ceiling.

0.5.0 Typed Read Projections

This additive release is source-compatible with 0.4.3. Pin both crates to the reviewed v0.5.0 commit. Projection declarations change generated Rust APIs only and require no database migration.

Add #[graphql_orm(projection(name = "...", fields = [id, field_name], private = true))] to a GraphQLEntity, then replace least-privilege full-entity reads with the generated projection's repository or transaction methods. Mark secrets #[graphql_orm(private, sensitive)]; existing #[backup(redact)] also drives projection Debug redaction.

If the database registers an application RowPolicy, projection reads now return a fail-closed error rather than fetching a full entity to evaluate it. Move projection-compatible tenant or soft-delete enforcement to generated typed filters or PostgreSQL RLS before migrating that caller. No GraphQL query or DTO is added. See Typed Read Projections.

0.4.3 Structural Introspection Hardening

This compatible patch needs no application API change. Pin both crates to the reviewed v0.4.3 commit and run managed validation with a new migration version.

Conditional indexes created by graphql-orm remain restart-idempotent. A same-name live index is now accepted only when the entire stored predicate parses as the supported field IN (closed set) form or PostgreSQL's equivalent field = ANY (ARRAY[...]) representation. Extra boolean expressions, comments that SQLite persists, functions, and unsupported casts are drift. PostgreSQL discards SQL comments when storing index expressions, so comment-only spelling has no persistent structural meaning on that backend.

Append-only validation now checks complete SQLite trigger definitions and PostgreSQL trigger and function catalog contracts, including unconditional enforcement and privilege/search-path posture. If an older deployment contains a recognizable managed name with hand-edited SQL, planning will produce repair work. Reusing an already recorded migration version then fails closed; review and apply that work under a fresh version with the schema-owner migration role.

0.4.2 Legacy Migration-History Adoption

This compatible patch needs no application API change. Move both crates to the reviewed v0.4.2 commit before adopting a database created by an older migration helper.

At managed-schema preparation, a history table containing exactly version as a non-null textual sole primary key and a non-null textual/timestamp applied_at is upgraded in one transaction. Every existing version and timestamp is preserved. Missing descriptions are set to Legacy migration <version>, and missing current metadata columns are added as nullable text. No historical migration is re-executed. Repeated preparation is idempotent.

SQLite rebuilds the recognized legacy table to install the complete current schema while preserving rows verbatim. PostgreSQL requires applied_at TIMESTAMPTZ NOT NULL; arbitrary legacy text is not converted because doing so could change timestamp meaning. PostgreSQL restores the CURRENT_TIMESTAMP default for future rows without changing existing values. Unknown columns, incorrect types or nullability, an empty version, or any other primary-key identity are rejected. Recorded-version reuse and remaining-plan drift checks still run after adoption and still fail closed.

Back up the database before first preparation. If a legacy table is rejected, inspect and migrate it explicitly rather than renaming columns until it happens to pass validation.

0.4.1 Binary Keys and Conditional Indexes

This is a compatible Git-pin update. Move both crates to the reviewed v0.4.1 commit.

  • Binary Vec<u8> keys require no host encoding. Mark host-assigned keys #[graphql_orm(auto_generated = false)]; use private, skip_input, or #[graphql(skip)] when they must not appear in public GraphQL inputs. Add min_length and max_length before migration when fixed digest width is an invariant.
  • Existing repository upsert entities with hidden conflict targets now compile. Their repository and transaction helpers remain available, while the unsafe GraphQL upsert field is absent.
  • Before adding a unique conditional index, validate that rows inside the selected predicate set contain no duplicate indexed keys. Apply the generated create-index plan under a new migration version.
  • Adding gt_field, gte_field, lte_field, or lt_field creates managed checks. SQL check predicates evaluate to UNKNOWN when either nullable operand is NULL, so NULL rows pass unless a separate non-null constraint applies.

0.4.0 Portable Persistence

This is an additive migration for existing entities. Upgrade both crates together to 0.4.0.

  1. Replace host-owned pool transactions with Database::transaction; use StateMachine for security-sensitive read/decide/write flows and retry the whole callback when classified retryable.
  2. Add #[graphql_orm(version, default = "0")] to an i64 field, apply the planned column migration, then move guarded updates to compare_and_swap.
  3. Add append_only = true only after removing update/delete/upsert callers. Review and apply the trigger plan with the schema-owner migration role; remove UPDATE/DELETE grants from ordinary PostgreSQL roles as defense in depth.
  4. Add portable constraint attributes, validate existing data, and apply the planned SQLite table rebuild or PostgreSQL named checks. New checks may reject historical invalid rows during rebuild.
  5. Add a deterministic keyset = "..., id asc" order and migrate clients to the generated keyset field. Discard legacy numeric cursors; they intentionally fail strict keyset decoding.

Managed startup should validate after every step. Missing append-only triggers or named checks are schema drift and must not be ignored or repaired with a reused migration version.

0.3.0 Security Hardening

SemVer Recommendation

Release as 0.3.0 (minor with documented breaking security defaults for pagination and public error messages). A future major can flip AuthorizationMode default to DeclaredPoliciesRequired.

Authorization Mode

// Before (implicit fail-open when no policy provider)
let database = Database::new(pool);

// After (recommended production setting)
let database = Database::new(pool)
    .with_authorization_mode(AuthorizationMode::DeclaredPoliciesRequired);
database.set_entity_policy(MyEntityPolicy);
Mode Current default Secure recommended Future default
LegacyPermissive yes no removed as default
DeclaredPoliciesRequired no yes planned default
ExplicitPolicyForAllExposedOperations no for high-assurance APIs optional

AuthSubject Expansion

// Before
AuthSubject::from_parts(id, roles, scopes, tenant_id)

// After (compatible) — same helper still works
// Prefer builder for new fields:
AuthSubject::builder(id)
    .user_id(user_id)
    .roles(roles)
    .scopes(scopes)
    .tenant_id(tenant)
    .token_id(jti)
    .session_id(session)
    .actor_id(actor)
    .build()

Debug no longer prints claim JSON bodies.

Public Errors

// Before
Err(async_graphql::Error::new(error.to_string()))

// After
Err(OrmPublicError::from_sqlx(&error).into_graphql_error())

Missing auth messages changed from "missing auth" to "unauthenticated" with extensions.code = "UNAUTHENTICATED".

Pagination Defaults

// Restore 0.2.x limits
Database::new(pool).with_pagination_config(PaginationConfig::legacy())

Default limit: 100050. Max limit: 1000100.

agql-auth Bridge

graphql-orm = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.6.0", features = ["sqlite", "auth-agql"] }

The optional feature depends on upstream agql-auth 0.8.0 via git revision be4e0a213ce9c9b9fbe9fe985602743a584e019b (tag v0.8.0). It does not use a local path, sibling checkout, or Cargo [patch].

use graphql_orm::graphql::auth_agql::auth_bundle_from_principal;
let (subject, db_auth) = auth_bundle_from_principal(&principal);

Structural Tenant Helpers

let resolution = resolve_structural_auth(
    StructuralAuthMetadata::new(Some("tenant_id"), None, StructuralAuthorization::Required),
    &StructuralAuthValues::from_subject(&subject),
);

Macro-generated wiring of structural predicates on every operation path remains a follow-up; helpers are available for host and incremental macro integration.

Trusted SQL Fragments

Prefer FilterExpression::trusted_fragment(clause, values) for host-authored predicates. FilterExpression::Raw remains for generated compatibility and is documented as a trusted surface.

SQLite Default Expression Idempotency

No application migration is required. After upgrading, reopening a file-backed SQLite database and replanning the same managed schema should produce an empty plan even when live PRAGMA table_info returns unixepoch() while generated metadata previously declared (unixepoch()).

ApplyOptions::additive_only behavior is unchanged for real non-additive steps such as altering a default from unixepoch() to date('now').

Empty Migration History Idempotency

SchemaManager::apply_migration is idempotent only when:

  1. the version is already present in __graphql_orm_migrations; and
  2. the plan has no remaining steps or statements.

Restart code that re-plans and re-applies the same version when the schema is already current receives AppliedMigrationReport { already_applied: true, statements_applied: 0, .. }.

If the version is recorded but the plan still has work, apply fails with an explicit protocol error. That is intentional: it surfaces schema drift or unsafe reuse of a migration version rather than silently treating the plan as done.

For apply_schema_target, “remaining work” includes nested migration steps/statements, RLS statements, and the combined executable plan.statements. An empty nested table migration with remaining RLS work is not a no-op.

Callers that pattern-match AppliedMigrationReport must accept the new already_applied field.

0.2.21 Auth Bridge

Structural Changes

AuthExt::auth_user() is deprecated but still available. Migrate call sites based on what they need:

// Before
let user_id = ctx.auth_user()?;

// After: id only
let user_id = ctx.auth_user_id()?;

// After: id, roles, scopes, tenant id
let subject = ctx.auth_subject()?;

Applications can keep injecting the legacy String user id while migrating. graphql-orm upgrades it to AuthSubject { id, roles: [], scopes: [], tenant_id: None, ... }. New code should inject AuthSubject directly.

If a downstream crate implemented AuthExt itself, add implementations for auth_user_id, auth_subject, and auth_subject_opt. Most applications only use the built-in implementation for async_graphql::Context<'_> and do not need to change anything beyond call-site names.

let request = request.data(AuthSubject {
    id: user.id.to_string(),
    user_id: None,
    roles: user.roles.clone(),
    scopes: user.scopes.clone(),
    tenant_id: user.tenant_id.clone(),
    claims: None,
    token_id: None,
    session_id: None,
    actor_id: None,
});

DbAuthContext::from_subject(&subject) can mirror the same principal into PostgreSQL RLS settings:

let request = request
    .data(subject.clone())
    .data(DbAuthContext::from_subject(&subject));

Generated Resolver Auth Modes

Generated resolvers keep the previous fail-closed default: if no auth setting is present, they require an auth subject before database access. This preserves the old generated ctx.auth_user()? gate.

Use auth = "none" for public generated resolvers:

#[graphql_entity(table = "pages", plural = "Pages", auth = "none")]
pub struct Page {
    // fields...
}

Use auth = "optional" when a schema should read a subject if present but leave allow/deny decisions to EntityPolicy, RowPolicy, or FieldPolicy:

schema_roots! {
    auth: "optional",
    query_custom_ops: [],
    entities: [Record],
}

Use auth = "required" explicitly for new private schemas or entities:

schema_roots! {
    auth: "required",
    query_custom_ops: [],
    entities: [Ticket, Session],
}

Entity-level auth overrides the schema-root mode.

ScopeEntityPolicy

ScopeEntityPolicy is exact-match only:

let mut database = Database::new(pool);
database.set_entity_policy(ScopeEntityPolicy::new(
    &["tickets.read"],
    &["tickets.write"],
));

require_auth: true returns an unauthenticated GraphQL error when no subject exists. A subject that lacks the required exact scope returns Ok(false) from the policy and is denied by the generated access path.

Behavioral Notes

  • No JWT, OIDC, cookie, wildcard, or application-specific scope logic was added to graphql-orm.
  • PostgreSQL RLS helper functions still use exact scope matching.
  • The current auth-agql feature targets agql-auth 0.16.0 at revision 3bc38cd94794f1e868a9cc3a5551047b95a32105; earlier release sections above retain their historical pins.