diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5a0d873..91d6669b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,13 @@ jobs: run: cargo test --manifest-path crates/graphql-orm/tests/fixtures/repository-aggregate-consumer/Cargo.toml --locked --no-default-features --features postgres --test runtime_strings -- --ignored --test-threads=1 - name: Execute the private runtime string consumer on owned SQLite 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 opt-in runtime GraphQL reads and mutations on owned backends + env: + CARGO_BUILD_JOBS: "2" + run: | + cargo test -p graphql-orm --locked --no-default-features --features sqlite,runtime-graphql --lib --test runtime_graphql --test runtime_mutations --test runtime_queries --test runtime_relations -- --test-threads=1 + cargo test -p graphql-orm --locked --no-default-features --features postgres,runtime-graphql --lib --test runtime_graphql --test runtime_mutations -- --test-threads=1 + cargo test -p graphql-orm --locked --no-default-features --features mssql,runtime-graphql --test runtime_graphql_unsupported - name: Execute owned runtime migration preservation and compatibility run: | 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f4e94c7c..ee1666ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -148,6 +148,13 @@ jobs: with: components: clippy - uses: Swatinem/rust-cache@v2 + - name: Execute opt-in runtime GraphQL reads and mutations on owned backends + env: + CARGO_BUILD_JOBS: "2" + run: | + cargo test -p graphql-orm --locked --no-default-features --features sqlite,runtime-graphql --lib --test runtime_graphql --test runtime_mutations --test runtime_queries --test runtime_relations -- --test-threads=1 + cargo test -p graphql-orm --locked --no-default-features --features postgres,runtime-graphql --lib --test runtime_graphql --test runtime_mutations -- --test-threads=1 + cargo test -p graphql-orm --locked --no-default-features --features mssql,runtime-graphql --test runtime_graphql_unsupported - name: Verify ORM backend lanes run: | cargo test -p graphql-orm --locked diff --git a/CHANGELOG.md b/CHANGELOG.md index b09c1d0a..ff7a24ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ for historical context. ## 0.43.0 (unreleased) +- Reconcile accepted runtime strings/binding and A–D source at aligned 0.43.0. + Retain immutable reviewed predecessors and execute PostgreSQL prefix/suffix + CAS, SQLite empty suffixes and opt-in GraphQL conformance in owned backend lanes. + - Add opt-in typed runtime GraphQL mutations delegating to guarded B transactions, with atomic host journals and independently authorized selected outputs. - Add an object-safe, default-deny post-commit record-release callback, explicit diff --git a/MIGRATION.md b/MIGRATION.md index 42d7d317..a7c0469a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -15,6 +15,11 @@ neither the runtime nor macros crate is published to crates.io. ## 0.42.0 to 0.43.0: opt-in runtime GraphQL mutations +The combined 0.43.0 candidate contains the reviewed independent string-binding +fix and runtime slices A–D. Intermediate unreleased version entries below record +reviewed branch contracts; they are not immutable published release identities. +Owner review/merge and an exact-source workspace release remain adoption gates. + No data migration or static API/SDL change. Mutation registration requires an explicit options builder, checked mutation root, certified environment and host write authority. Existing read authorities remain source-compatible; releasing diff --git a/crates/graphql-orm/tests/runtime_mutations.rs b/crates/graphql-orm/tests/runtime_mutations.rs index 048dcae5..fc488899 100644 --- a/crates/graphql-orm/tests/runtime_mutations.rs +++ b/crates/graphql-orm/tests/runtime_mutations.rs @@ -1225,8 +1225,7 @@ async fn host_policy_only_revision_changes_reject_unchanged_schema_requests() { f.finish().await; } -// PostgreSQL executes only rejection cases until the reviewed #112/A/B reconciliation. -// The renderer's native-slot accounting is independently covered by unit tests. +// Combined #112/A/B execution checks emitted slots at and below both backend budgets. #[tokio::test] async fn emitted_suffix_bind_budgets_precede_target_reads_for_cas_and_authority() { let f = Fixture::new().await; @@ -1313,9 +1312,6 @@ async fn emitted_suffix_bind_budgets_precede_target_reads_for_cas_and_authority( postgres_slots }; for budget in [required - 1, required, required + 1] { - if Backend::DIALECT == DatabaseBackend::Postgres && budget >= required { - continue; - } let id = (case * 3 + budget + 2 - required) as i64; f.run(f.create(id), f.authority()).await.unwrap(); let request = f @@ -1368,6 +1364,138 @@ async fn emitted_suffix_bind_budgets_precede_target_reads_for_cas_and_authority( f.finish().await; } +#[tokio::test] +async fn reconciled_prefix_suffix_cas_preserves_unicode_empty_and_bound_values() { + let f = Fixture::new().await; + let samples = ["alpha", "ALPHA", "πfoo", "πfoo%", "suffix", "Ω", ""]; + for (case, (operator, operand, matched)) in [ + (RuntimeScalarOperator::StartsWith, "π", vec![2, 3]), + (RuntimeScalarOperator::EndsWith, "foo", vec![2]), + (RuntimeScalarOperator::EndsWith, "", (0..7).collect()), + (RuntimeScalarOperator::StartsWith, "", (0..7).collect()), + (RuntimeScalarOperator::StartsWith, "AL", vec![1]), + (RuntimeScalarOperator::EndsWith, "%", vec![3]), + ] + .into_iter() + .enumerate() + { + let expected = f + .schema() + .runtime_compare( + &f.collection(), + &f.field("label"), + operator, + RuntimeValue::String(operand.into()), + Default::default(), + ) + .unwrap(); + // SQL-renderable authority uses another repeated native value as well as + // the tenant restriction; no caller value is interpolated into SQL. + let tenant_suffix = f + .schema() + .runtime_compare( + &f.collection(), + &f.field("tenant"), + RuntimeScalarOperator::EndsWith, + RuntimeValue::String("orth".into()), + Default::default(), + ) + .unwrap(); + let visible = f + .schema() + .runtime_and( + &f.collection(), + vec![f.authority().predicate.unwrap(), tenant_suffix], + Default::default(), + ) + .unwrap(); + for (offset, label) in samples.iter().enumerate() { + let id = (case * samples.len() + offset) as i64; + let create = f + .schema() + .runtime_create_request( + &f.collection(), + &[ + (f.field("id"), RuntimeValue::Integer(id)), + (f.field("tenant"), RuntimeValue::String("north".into())), + (f.field("label"), RuntimeValue::String((*label).into())), + ], + None, + Default::default(), + ) + .unwrap(); + f.run(create, f.authority()).await.unwrap(); + let update = f + .schema() + .runtime_update_request( + f.key(id), + &[(f.field("score"), RuntimeValue::Integer(8))], + Some(expected.clone()), + Some(f.projection()), + Default::default(), + ) + .unwrap(); + let mut authority = f.authority(); + authority.predicate = Some(visible.clone()); + let result = f.run(update.clone(), authority).await; + if matched.contains(&offset) { + let effect = result.unwrap(); + assert!(matches!( + effect.record().unwrap().state(&f.field("score")).unwrap(), + RuntimeFieldState::Value(RuntimeValue::Integer(8)) + )); + assert!( + matches!(effect.record().unwrap().state(&f.field("label")).unwrap(), RuntimeFieldState::Value(RuntimeValue::String(actual)) if actual == label) + ); + // A structural condition independent of a built-in version field + // catches the now-stale score even when the string still matches. + let score = f + .schema() + .runtime_compare( + &f.collection(), + &f.field("score"), + RuntimeScalarOperator::Eq, + RuntimeValue::Integer(7), + Default::default(), + ) + .unwrap(); + let stale = f + .schema() + .runtime_and( + &f.collection(), + vec![expected.clone(), score], + Default::default(), + ) + .unwrap(); + let retry = f + .schema() + .runtime_update_request( + f.key(id), + &[(f.field("score"), RuntimeValue::Integer(9))], + Some(stale), + None, + Default::default(), + ) + .unwrap(); + assert_eq!( + f.run(retry, f.authority()) + .await + .unwrap_err() + .public_error() + .runtime_mutation_code(), + Some("conflict") + ); + } else { + assert_eq!( + result.unwrap_err().public_error().runtime_mutation_code(), + Some("conflict") + ); + } + } + } + f.finish().await; +} + #[cfg(feature = "runtime-graphql")] #[path = "support/runtime_graphql_mutations.rs"] mod graphql_mutations; diff --git a/crates/graphql-orm/tests/support/runtime_graphql_mutations.rs b/crates/graphql-orm/tests/support/runtime_graphql_mutations.rs index 295a0368..b7f9ddd8 100644 --- a/crates/graphql-orm/tests/support/runtime_graphql_mutations.rs +++ b/crates/graphql-orm/tests/support/runtime_graphql_mutations.rs @@ -1009,3 +1009,46 @@ async fn repeated_groups_keep_validation_directives_and_projection_denial_before assert_eq!(f.count().await, 1); f.finish().await; } + +#[tokio::test] +async fn reconciled_string_cas_runs_once_through_graphql_and_atomic_journals() { + let f = Fixture::new().await; + f.run(f.create(1), f.authority()).await.unwrap(); + let schema = api(&f); + let probe = Arc::new(GroupProbe::default()); + let field = r#"updateRecord(key:{id:"1"},input:{label:"updated"},expected:{and:[{label:{startsWith:"ini"}},{label:{endsWith:"tial"}},{label:{endsWith:""}}]},expectedSchema:$schema)"#; + let response = schema + .execute(request(&f, &repeated(field)).data(group_context( + &f, + &probe, + 0, + 0, + Default::default(), + ))) + .await; + assert!(response.errors.is_empty(), "{:?}", response.errors); + assert_eq!( + response.data.into_json().unwrap()["same"]["record"], + serde_json::json!({"label":"updated","score":"7","renamed":"updated"}) + ); + assert_eq!(probe.intents.load(Ordering::SeqCst), 1); + assert_eq!(probe.hooks.lock().unwrap().len(), 1); + assert_eq!(probe.released.lock().unwrap().len(), 1); + assert_eq!(Journal::count_all(&f.db).await.unwrap(), 1); + let stale = schema + .execute(request(&f, &repeated(field)).data(group_context( + &f, + &probe, + 0, + 0, + Default::default(), + ))) + .await; + assert_eq!(stale.errors.len(), 3); + assert!(stale.errors.iter().all(|error| error.message == "conflict")); + assert_eq!(probe.intents.load(Ordering::SeqCst), 2); + assert_eq!(probe.hooks.lock().unwrap().len(), 1); + assert_eq!(probe.released.lock().unwrap().len(), 1); + assert_eq!(Journal::count_all(&f.db).await.unwrap(), 1); + f.finish().await; +} diff --git a/docs/plans/active/runtime-platform-capabilities/README.md b/docs/plans/active/runtime-platform-capabilities/README.md index ced9a684..3c84df58 100644 --- a/docs/plans/active/runtime-platform-capabilities/README.md +++ b/docs/plans/active/runtime-platform-capabilities/README.md @@ -21,8 +21,10 @@ PR A implements the owned migration slice; its current public mechanics are in B is accepted at `04a1216aff86141ff6500b1dd0bf9adb4b0c6447` as C's committed predecessor. C is accepted at `9d4cca34cb04ab0fd27c8313215001591f495c43`; its [current mechanics and approved custom-directive limitation](../../../reference/graphql-orm/runtime-graphql.md) -are documented independently. D is implemented for review on that exact C revision, including the approved -additive post-commit output callback. It remains unaccepted and unreleased. +are documented independently. D is accepted at +`3a59d08be5b663ac952de418eb02dc6321ee08e7`, including the post-commit output +callback and D1 response-key grouping. All slices remain unreleased; final +combined review is pending. Final reconciliation and owner merge/release remain #112 → A → B → C → D. A proposal or unpublished implementation is not downstream adoption evidence. @@ -1189,25 +1191,31 @@ is advertised until its contract is committed and reviewed. ## Current checkpoint -The revised A–D contract at `c53a5de966ca5089a5e4aa2d727e144bae83e6bd` -is approved. Digibase accepts A at `f2665ce68ed7fdbccb272cb8a783b6379ae5fd82`: -FK corrections R1/R2 and PostgreSQL DateTime correction R3 are resolved. A's -reviewed-predecessor gate for B is satisfied. A remains unmerged/unreleased, -with ORM/macros 0.40.0. Owner merge/release of independent #112 precedes A's final -reconciliation; Actions remain paused. - -B's independent implementation is based on that exact committed A revision, -with aligned unpublished ORM/macros 0.41.0. It adds schema-bound requests, -explicit intent/preimage/result authority, pinned structural CAS, exact return -projections, atomic repository journal writes and guarded operation/hook -cancellation. B is accepted at `04a1216aff86141ff6500b1dd0bf9adb4b0c6447`. Its reference and compiled -host example describe environment/policy revision pinning, backend gates and -ambiguous commit limitations. The static-only inner-timeout limitation remains. - -C is accepted at `9d4cca34cb04ab0fd27c8313215001591f495c43`. D is implemented with response-key grouping corrections -for review, but remains unaccepted/unreleased and dependent on its -respective reviewed predecessors. Preserve host-managed AEAD cursor protection, -lossless JSON text, required create keys and independent post-commit payload -read authorization. Reconcile B onto the final reviewed A after #112 integration -and rerun combined backend contracts before downstream adoption. No product or -sibling repository changes, owner merge or release publication are included. +All A–D implementations are accepted at exact committed revisions: + +| Slice | PR | Accepted revision | +| --- | --- | --- | +| Independent strings/binding | #112 | `01d4134da18028ec58e318e0ffd30ab244d0d8e1` | +| A | #97 | `f2665ce68ed7fdbccb272cb8a783b6379ae5fd82` | +| B | #113 | `04a1216aff86141ff6500b1dd0bf9adb4b0c6447` | +| C | #114 | `9d4cca34cb04ab0fd27c8313215001591f495c43` | +| D | #115 | `3a59d08be5b663ac952de418eb02dc6321ee08e7` | + +The independent combined candidate merges these reviewed revisions in that order +and keeps each as an ancestor. ORM/macros are aligned at unpublished 0.43.0, +including #112's runtime predicates/native binding identity and all A–D +corrections. The canonical owned backend runner now includes opt-in C/D tests. +PostgreSQL success cases in B's emitted-bind budget test are enabled; new +Unicode/empty prefix/suffix CAS and repeated GraphQL string-CAS/journal tests +exercise the combined engine. Individual reviewed branches and sibling +repositories remain unchanged. + +Final combined-source verification and review remain pending. Owner merge/release +order stays #112 → A → B → C → D; Actions remain paused, local builds use two jobs +and sequential backends. Retain migration/FK/DateTime/RLS preservation, pinned +authority and CAS, atomic journals, cancellation poisoning, confidential AEAD +cursors, lossless scalars, corrected aliases/filter intents and terminal repeated +mutation groups. Custom-directive factories, modifying cascades and the static +inner-timeout limitation remain documented boundaries. No merge/publication or +downstream adoption is claimed. The owner release must bind an immutable workspace +tag, full source SHA, package/lockfile hashes and external revision provenance. diff --git a/docs/reference/graphql-orm/runtime-graphql.md b/docs/reference/graphql-orm/runtime-graphql.md index 48708525..2b9e80c3 100644 --- a/docs/reference/graphql-orm/runtime-graphql.md +++ b/docs/reference/graphql-orm/runtime-graphql.md @@ -208,11 +208,12 @@ Docker containers and verify exact ownership before cleanup. MSSQL remains an unsupported runtime decoder and is rejected before ORM I/O; compilation is not live MSSQL execution. GitHub Actions remain paused. -The unreconciled B predecessor does not yet contain #112. Successful PostgreSQL -StartsWith/EndsWith execution and the SQLite empty-suffix correction remain gated -on reviewed #112 → A → B → C reconciliation. This does not weaken those release -acceptance requirements. No merge, publication or downstream pin is implied by an -unpublished C branch. D requires separate reviewed C/B predecessors. +The combined candidate contains reviewed #112 → A → B → C → D. Its owned +backend runner executes PostgreSQL prefix/suffix CAS success and exact bind-budget +boundaries, SQLite empty-suffix behavior, and repeated GraphQL string-CAS groups +with atomic journals. Individual A–D heads remain unchanged. Final combined +source review and owner-controlled merge/release are still adoption gates; local +execution does not supply an immutable release identity or authorize a downstream pin. ## Typed mutations and committed output authorization diff --git a/docs/reference/graphql-orm/runtime-mutations.md b/docs/reference/graphql-orm/runtime-mutations.md index 62457117..d640095d 100644 --- a/docs/reference/graphql-orm/runtime-mutations.md +++ b/docs/reference/graphql-orm/runtime-mutations.md @@ -151,6 +151,9 @@ retain their existing limitation: catching an inner static mutation timeout and returning `Ok` does not guarantee rollback. Bulk/nested writes, new upsert semantics, dynamic GraphQL and durable delivery are -outside B. C/D remain separate reviewed successors. Owner merge/release of #112 -and final reconciliation of A/B are required before combined downstream adoption; -this branch does not claim to contain the independent string-binding fix. +outside B. C/D remain separate reviewed successors. The combined candidate +contains the independent #112 string-binding fix and exercises successful +PostgreSQL prefix/suffix CAS and emitted-bind boundaries without test-side skips. +SQLite empty suffixes match every non-null string. Final combined review and +owner merge/release remain required before downstream adoption. Static APIs and +their cancellation limitation are unchanged. diff --git a/scripts/run-owned-database-lanes.sh b/scripts/run-owned-database-lanes.sh index cc6a292e..17b68e36 100755 --- a/scripts/run-owned-database-lanes.sh +++ b/scripts/run-owned-database-lanes.sh @@ -26,20 +26,21 @@ repository_root=$(git rev-parse --show-toplevel) cd "${repository_root}" run_sqlite() { - cargo test -p graphql-orm --locked --no-default-features --features sqlite \ + cargo test -p graphql-orm --locked --no-default-features --features sqlite,runtime-graphql \ + --lib --test runtime_graphql --test runtime_strings --test runtime_queries --test runtime_relations \ --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 --no-default-features --features sqlite \ - --test runtime_strings --test grouped_aggregates --test complete_group_pages --test portable_group_pages --test host_timestamps --locked + --test grouped_aggregates --test complete_group_pages --test portable_group_pages --test host_timestamps --locked cargo test --manifest-path \ crates/graphql-orm/tests/fixtures/repository-aggregate-consumer/Cargo.toml \ --no-default-features --features sqlite --locked } run_postgres() { - 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 \ + cargo test -p graphql-orm --locked --no-default-features --features postgres,runtime-graphql \ + --lib --test runtime_graphql --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 --no-default-features --features postgres \