Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
run: cargo run --manifest-path crates/graphql-orm/tests/fixtures/repository-aggregate-consumer/Cargo.toml --locked --no-default-features --features sqlite --example runtime_strings
- name: Execute owned runtime migration preservation and compatibility
run: |
cargo test -p graphql-orm --locked --no-default-features --features postgres --test runtime_migrations --test runtime_schema_ir --test runtime_migration_memory --test migration_planner --test migration_apply --test legacy_migration_history -- --test-threads=1
cargo test -p graphql-orm --locked --no-default-features --features postgres --test runtime_mutations --test runtime_migrations --test runtime_schema_ir --test runtime_migration_memory --test migration_planner --test migration_apply --test legacy_migration_history -- --test-threads=1
cargo test -p graphql-orm --locked --no-default-features --features mssql --test runtime_migrations_unsupported
for backend in sqlite postgres mssql; do
cargo test --manifest-path crates/graphql-orm/tests/fixtures/migration-api-compatibility/Cargo.toml --locked --no-default-features --features "$backend"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ jobs:
cargo test -p graphql-orm --no-default-features --features sqlite,field-case-pascal,argument-case-snake --test federation_entity_keys_case_features --locked
cargo check -p graphql-orm --no-default-features --features postgres --locked
cargo test -p graphql-orm --no-default-features --features postgres --test federation_entity_keys_postgres --locked
cargo test -p graphql-orm --locked --no-default-features --features postgres --test runtime_migrations --test runtime_schema_ir --test runtime_migration_memory --test migration_planner --test migration_apply --test legacy_migration_history -- --test-threads=1
cargo test -p graphql-orm --locked --no-default-features --features postgres --test runtime_mutations --test runtime_migrations --test runtime_schema_ir --test runtime_migration_memory --test migration_planner --test migration_apply --test legacy_migration_history -- --test-threads=1
cargo test -p graphql-orm --locked --no-default-features --features mssql --test runtime_migrations_unsupported
for backend in sqlite postgres mssql; do
cargo test --manifest-path crates/graphql-orm/tests/fixtures/migration-api-compatibility/Cargo.toml --locked --no-default-features --features "$backend"
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,25 @@ This file is the authoritative user-facing release chronology. The former
[release-notes ledger](docs/archive/2026/graphql-orm-release-notes.md) is retained
for historical context.

## 0.41.0 - unreleased

Companion macros crate: `graphql-orm-macros` **0.41.0** (alignment only).

- Add schema-bound single-record runtime create, patch and exact-key delete on
SQLite/PostgreSQL pinned state-machine transactions. Reuse existing values,
handles, typed predicates and row decoding; preserve static repository APIs.
- Require explicit host intent/preimage/result authorization, structural CAS,
exact return projection grants and verified physical dependency environments.
- Add cancellation guards for runtime mutations and before-commit host journal
hooks. Caught errors/timeouts cannot commit partial runtime work. Static-only
transaction behavior and ambiguous commit limitations remain unchanged.
- Count backend-emitted predicate bindings before runtime target reads, including
duplicated SQLite suffix values and reused PostgreSQL native slots.
- Add conservative transaction commit-outcome inspection and safe runtime error
codes retained through the public transaction runner.
- Document unsupported modifying cascades and backend capabilities; include a
disposable create/update/delete/journal example and framework-neutral consumer.

## 0.40.0 - unreleased

Companion macros crate: `graphql-orm-macros` **0.40.0** (alignment only).
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ cynic-parser = { version = "=0.11.2", features = ["pretty"] }
futures = "0.3"
getrandom = "0.3"
graphql-composition = "=0.12.2"
graphql-orm = { path = "crates/graphql-orm", version = "0.40.0", default-features = false }
graphql-orm = { path = "crates/graphql-orm", version = "0.41.0", default-features = false }
graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.15.0" }
graphql-orm-backup = { path = "crates/graphql-orm-backup", version = "0.7.2", default-features = false }
graphql-orm-operation-catalog = { path = "crates/graphql-orm-operation-catalog", version = "0.4.0" }
Expand Down
12 changes: 12 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ supersedes: []
`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.40.0 to 0.41.0: transactional runtime mutations

Adopt aligned ORM/macros 0.41.0. This is an additive, unpublished successor to A;
no stored-data migration, macro syntax or static GraphQL SDL change is required.
Use `TransactionMode::StateMachine`, a verified runtime mutation environment and
explicit host authority. The host pins its complete policy revision and external
DDL fence. Runtime mutation errors/cancellation now poison only transactions that
perform runtime work; static-only timeout behavior is unchanged. Use the guarded
before-commit hook for atomic host journal writes. Effects remain pending until
the runner commits. See [runtime mutations](docs/reference/graphql-orm/runtime-mutations.md)
for unsupported capabilities, exact omission/null semantics and ambiguous commits.

## 0.39.0 to 0.40.0: owned runtime migration targets

Adopt runtime and macros 0.40.0 together. This unpublished identity retains the released
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm-macros"
version = "0.40.0"
version = "0.41.0"
edition = "2024"
authors = ["Toby Martin"]
description = "Procedural macros for async-graphql and ORM-backed entities, relations, and CRUD operations."
Expand Down
6 changes: 3 additions & 3 deletions crates/graphql-orm-macros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ macro/runtime versions aligned:

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

Direct use is supported for tooling that needs the macro package:

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

The direct dependency still requires a compatible `graphql-orm` runtime in the
Expand Down Expand Up @@ -183,6 +183,6 @@ See [host timestamp inputs, migration compatibility and cancellation limits](../

Complete SQLite/PostgreSQL text-group pages and SQL-visible ordinary aggregates: see [typed aggregates](../../docs/reference/graphql-orm/typed-aggregates.md).

Version 0.40.0 aligns with the owned runtime migration addition. Released host-managed
Version 0.41.0 aligns with the schema-bound transactional runtime mutation addition. Released host-managed
timestamps, framework-neutral repository aggregate helpers and static GraphQL SDL
remain unchanged.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/graphql-orm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm"
version = "0.40.0"
version = "0.41.0"
edition = "2024"
description = "Runtime support crate for graphql-orm-macros"
license = "MIT"
Expand Down Expand Up @@ -68,7 +68,7 @@ futures = "0.3"
geo = { version = "0.33", optional = true, default-features = false }
geo-types = { version = "0.7", optional = true }
geojson = { version = "1", optional = true, default-features = true }
graphql-orm-macros = { path = "../graphql-orm-macros", version = "0.40.0", default-features = false }
graphql-orm-macros = { path = "../graphql-orm-macros", version = "0.41.0", default-features = false }
graphql-orm-operation-catalog = { workspace = true }
rust_decimal = { workspace = true }
serde = { version = "1", features = ["derive"] }
Expand Down
6 changes: 5 additions & 1 deletion crates/graphql-orm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ backend:

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

This unpublished package has no docs.rs release. Use this Git README and the
Expand Down Expand Up @@ -204,3 +204,7 @@ are documented in [runtime migrations](../../docs/reference/graphql-orm/runtime-
Live capability checks reject FK actions/deferral the physical model cannot retain,
including incoming system/unowned constraints, and recheck on the apply transaction.
Static migration APIs remain unchanged.

Schema-bound runtime create/patch/delete with pinned authority, structural CAS,
atomic host journals and cancellation poisoning are documented in
[runtime mutations](../../docs/reference/graphql-orm/runtime-mutations.md).
169 changes: 169 additions & 0 deletions crates/graphql-orm/examples/runtime_mutation_journal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//! Owned disposable runtime mutation + repository journal example.
#[cfg(any(
all(feature = "sqlite", not(any(feature = "postgres", feature = "mssql"))),
all(feature = "postgres", not(any(feature = "sqlite", feature = "mssql")))
))]
mod example {
use graphql_orm::futures::future::BoxFuture;
use graphql_orm::{graphql::orm::*, prelude::*};
use std::sync::Arc;
#[cfg(feature = "sqlite")]
type Backend = SqliteBackend;
#[cfg(feature = "postgres")]
type Backend = PostgresBackend;
#[derive(RepositoryEntity, Clone, serde::Serialize, serde::Deserialize)]
#[repository_entity(table = "example_journal", plural = "ExampleJournal")]
struct Journal {
#[primary_key]
#[graphql_orm(auto_generated = false)]
id: String,
action: String,
}
#[derive(GraphQLSchemaEntity, Clone, serde::Serialize, serde::Deserialize)]
#[graphql_entity(table = "example_records", plural = "ExampleRecords")]
struct Record {
#[primary_key]
#[graphql_orm(auto_generated = false)]
id: i64,
#[filterable(type = "string")]
label: String,
}
// Explicit public authority for this isolated example. A real host validates its
// pinned identity, policy/public revision, input/CAS fields and return fields here.
struct PublicAuthority(RuntimeProjection);
impl RuntimeWriteAuthority<Backend> for PublicAuthority {
fn authorize_intent<'a>(
&'a self,
_: RuntimeWriteIntent<'a>,
_: &'a mut MutationContext<'_, Backend>,
) -> BoxFuture<'a, Result<RuntimeWriteGrant, RuntimeMutationError>> {
Box::pin(async move { RuntimeWriteGrant::new(self.0.clone(), None) })
}
fn authorize_preimage<'a>(
&'a self,
_: RuntimePreimageCheck<'a>,
_: &'a mut MutationContext<'_, Backend>,
) -> BoxFuture<'a, Result<(), RuntimeMutationError>> {
Box::pin(async { Ok(()) })
}
fn authorize_result<'a>(
&'a self,
check: RuntimeResultCheck<'a>,
_: &'a mut MutationContext<'_, Backend>,
) -> BoxFuture<'a, Result<RuntimeReturnGrant, RuntimeMutationError>> {
Box::pin(async move { Ok(RuntimeReturnGrant::new(check.intent.returning().cloned())) })
}
}
pub async fn run(db: Database<Backend>) -> Result<(), Box<dyn std::error::Error>> {
let schema =
Arc::new(RuntimeSchema::from_static_entities(&[Record::metadata()])?.validate()?);
let target = schema
.physical_schema::<Backend>(Default::default())?
.with_static_entities(&[Journal::metadata()])?;
let ownership = ManagedTableSet::new(["example_records".into(), "example_journal".into()])?;
let plan = db
.schema()
.plan_owned_migration(
"example",
"isolated example",
&target,
&ownership,
PlanOptions::strict(),
)
.await?;
db.schema()
.apply_owned_migration(&plan, ApplyOptions::default())
.await?;
let environment = Arc::new(
db.schema()
.runtime_mutation_environment(schema.clone(), &target, &ownership)
.await?,
);
let collection = schema.resolve_collection(&schema.schema().collections[0].id)?;
let id = schema.resolve_field(&collection, &FieldId::new("example_records.id")?)?;
let label = schema.resolve_field(&collection, &FieldId::new("example_records.label")?)?;
let projection = schema.resolve_projection(&collection, &[id.clone(), label.clone()])?;
let expected = schema.runtime_compare(
&collection,
&label,
RuntimeScalarOperator::Eq,
RuntimeValue::String("draft".into()),
Default::default(),
)?;
let requests = [
schema.runtime_create_request(
&collection,
&[
(id.clone(), RuntimeValue::Integer(7)),
(label.clone(), RuntimeValue::String("draft".into())),
],
Some(projection.clone()),
Default::default(),
)?,
schema.runtime_update_request(
schema.runtime_key(&collection, &[(id.clone(), RuntimeValue::Integer(7))])?,
&[(label, RuntimeValue::String("published".into()))],
Some(expected),
Some(projection.clone()),
Default::default(),
)?,
schema.runtime_delete_request(
schema.runtime_key(&collection, &[(id, RuntimeValue::Integer(7))])?,
None,
Some(projection.clone()),
Default::default(),
)?,
];
for (index, request) in requests.into_iter().enumerate() {
let environment = environment.clone();
let authority = PublicAuthority(projection.clone());
let committed = db
.transaction(TransactionMode::StateMachine, move |tx| {
Box::pin(async move {
let pending = tx
.mutate_runtime(&environment, &request, &authority)
.await?;
tx.insert::<Journal>(CreateJournalInput {
id: index.to_string(),
action: format!("{:?}", pending.action()),
})
.await?;
Ok(pending)
})
})
.await?;
println!("Committed {:?}", committed.action());
}
assert_eq!(Journal::count_all(&db).await?, 3);
db.pool().close().await;
Ok(())
}
}
#[cfg(all(feature = "postgres", not(any(feature = "sqlite", feature = "mssql"))))]
#[path = "../tests/support/owned_postgres.rs"]
mod owned_postgres;
#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(all(feature = "sqlite", not(any(feature = "postgres", feature = "mssql"))))]
example::run(graphql_orm::db::Database::connect_sqlite("sqlite::memory:").await?).await?;
#[cfg(all(feature = "postgres", not(any(feature = "sqlite", feature = "mssql"))))]
{
let mut owned = owned_postgres::OwnedPostgres::start("runtime-journal-example")?;
example::run(graphql_orm::db::Database::connect_postgres(&owned.url).await?).await?;
owned.cleanup()?;
}
#[cfg(any(
all(feature = "sqlite", not(any(feature = "postgres", feature = "mssql"))),
all(feature = "postgres", not(any(feature = "sqlite", feature = "mssql")))
))]
{
Ok(())
}
#[cfg(not(any(
all(feature = "sqlite", not(any(feature = "postgres", feature = "mssql"))),
all(feature = "postgres", not(any(feature = "sqlite", feature = "mssql")))
)))]
{
Err("runtime mutation example requires exactly SQLite or PostgreSQL".into())
}
}
12 changes: 12 additions & 0 deletions crates/graphql-orm/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,18 @@ where
return Err(classified);
}
};
if context.runtime_cannot_commit() {
context
.rollback()
.await
.map_err(classify_transaction_error::<B>)?;
return Err(crate::graphql::orm::TransactionError::Rejected(
OrmPublicError::with_message(
OrmErrorCode::Conflict,
"unfinished or failed runtime work requires rollback",
),
));
}
context
.commit_and_emit()
.await
Expand Down
8 changes: 8 additions & 0 deletions crates/graphql-orm/src/graphql/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub struct OrmPublicError {
/// Internal diagnostic text never exposed by default GraphQL extensions.
pub(crate) internal: Option<String>,
pub(crate) retryable: bool,
pub(crate) runtime_mutation_code: Option<&'static str>,
}

impl fmt::Debug for OrmPublicError {
Expand Down Expand Up @@ -125,6 +126,7 @@ impl OrmPublicError {
correlation_id: None,
internal: None,
retryable: false,
runtime_mutation_code: None,
}
}

Expand All @@ -136,6 +138,7 @@ impl OrmPublicError {
correlation_id: None,
internal: None,
retryable: false,
runtime_mutation_code: None,
}
}

Expand All @@ -151,6 +154,11 @@ impl OrmPublicError {
self
}

/// Safe runtime mutation code retained through the transaction runner, when applicable.
pub fn runtime_mutation_code(&self) -> Option<&'static str> {
self.runtime_mutation_code
}

/// Whether retrying the complete transaction is safe and recommended.
pub const fn is_retryable(&self) -> bool {
self.retryable
Expand Down
Loading