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/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 --no-default-features --features postgres --test projection_visibility --test cross_crate_relations_fixture --locked
cargo test -p graphql-orm --no-default-features --features postgres --test projection_visibility --test cross_crate_relations_fixture --test repository_counts --locked
cargo test -p graphql-orm --no-default-features --features postgres --test grouped_aggregates_postgres --locked -- --ignored --test-threads=1
cargo run --manifest-path crates/graphql-orm/tests/fixtures/repository-aggregate-consumer/Cargo.toml --locked --no-default-features --features sqlite --example policy_projections
cargo run --manifest-path crates/graphql-orm/tests/fixtures/repository-aggregate-consumer/Cargo.toml --locked --no-default-features --features sqlite --example complete_events
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ for historical context.

## 0.35.2 - 2026-10-01

- Keep standalone fixture lockfiles aligned with this package set and execute
transaction count authorization regressions on test-owned PostgreSQL as well
as SQLite during release validation.

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

- Allow transaction-bound repository `count` and `exists` with an installed
Expand Down

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

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

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

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

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

96 changes: 82 additions & 14 deletions crates/graphql-orm/tests/repository_counts.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
#![cfg(feature = "sqlite")]
#![cfg(any(feature = "sqlite", feature = "postgres"))]

use graphql_orm::prelude::*;

#[cfg(feature = "postgres")]
#[path = "support/owned_postgres.rs"]
mod owned_postgres;
#[cfg(feature = "sqlite")]
type Backend = SqliteBackend;
#[cfg(feature = "postgres")]
type Backend = PostgresBackend;

#[derive(RepositoryEntity, Clone, Debug, serde::Serialize, serde::Deserialize)]
#[repository_entity(backend = "sqlite", table = "count_rows", plural = "CountRows")]
#[cfg_attr(
feature = "sqlite",
repository_entity(backend = "sqlite", table = "count_rows", plural = "CountRows")
)]
#[cfg_attr(
feature = "postgres",
repository_entity(backend = "postgres", table = "count_rows", plural = "CountRows")
)]
struct CountRow {
#[primary_key]
#[graphql_orm(auto_generated = false)]
Expand All @@ -23,19 +38,26 @@ enum Visibility {
}

#[derive(RepositoryEntity, Clone, Debug, serde::Serialize, serde::Deserialize)]
#[repository_entity(backend = "sqlite", table = "other_rows", plural = "OtherRows")]
#[cfg_attr(
feature = "sqlite",
repository_entity(backend = "sqlite", table = "other_rows", plural = "OtherRows")
)]
#[cfg_attr(
feature = "postgres",
repository_entity(backend = "postgres", table = "other_rows", plural = "OtherRows")
)]
struct OtherRow {
#[primary_key]
#[graphql_orm(auto_generated = false)]
#[filterable(type = "string")]
id: String,
}

impl RowPolicy<SqliteBackend> for Visibility {
impl RowPolicy<Backend> for Visibility {
fn read_visibility<'a>(
&'a self,
ctx: Option<&'a async_graphql::Context<'_>>,
_db: &'a Database<SqliteBackend>,
_db: &'a Database<Backend>,
_entity: &'static str,
_key: Option<&'static str>,
surface: EntityAccessSurface,
Expand All @@ -47,7 +69,7 @@ impl RowPolicy<SqliteBackend> for Visibility {
Self::Unrestricted => ReadVisibility::Unrestricted,
Self::CallbackOnly => ReadVisibility::CallbackOnly,
Self::WrongEntity => {
ReadVisibility::Complete(ReadPredicate::from_filter::<SqliteBackend, _>(
ReadVisibility::Complete(ReadPredicate::from_filter::<Backend, _>(
&OtherRowWhereInput {
id: Some(StringFilter {
eq: Some("1".into()),
Expand All @@ -59,7 +81,7 @@ impl RowPolicy<SqliteBackend> for Visibility {
}
Self::Complete | Self::Prefilter => {
let predicate =
ReadPredicate::from_filter::<SqliteBackend, _>(&CountRowWhereInput {
ReadPredicate::from_filter::<Backend, _>(&CountRowWhereInput {
tenant: Some(StringFilter {
eq: Some("alpha".into()),
..Default::default()
Expand All @@ -79,7 +101,7 @@ impl RowPolicy<SqliteBackend> for Visibility {
fn can_read_row<'a>(
&'a self,
_ctx: Option<&'a async_graphql::Context<'_>>,
_db: &'a Database<SqliteBackend>,
_db: &'a Database<Backend>,
_entity: &'static str,
_key: Option<&'static str>,
_surface: EntityAccessSurface,
Expand All @@ -91,7 +113,7 @@ impl RowPolicy<SqliteBackend> for Visibility {
fn can_write_row<'a>(
&'a self,
_ctx: Option<&'a async_graphql::Context<'_>>,
_db: &'a Database<SqliteBackend>,
_db: &'a Database<Backend>,
_entity: &'static str,
_key: Option<&'static str>,
_surface: EntityAccessSurface,
Expand All @@ -103,11 +125,11 @@ impl RowPolicy<SqliteBackend> for Visibility {

struct DenyEntity;

impl EntityPolicy<SqliteBackend> for DenyEntity {
impl EntityPolicy<Backend> for DenyEntity {
fn can_access_entity<'a>(
&'a self,
_ctx: Option<&'a async_graphql::Context<'_>>,
_db: &'a Database<SqliteBackend>,
_db: &'a Database<Backend>,
_entity: &'static str,
_key: Option<&'static str>,
_kind: EntityAccessKind,
Expand All @@ -117,8 +139,42 @@ impl EntityPolicy<SqliteBackend> for DenyEntity {
}
}

async fn fixture(visibility: Option<Visibility>) -> Database<SqliteBackend> {
let mut db = Database::<SqliteBackend>::connect_sqlite("sqlite::memory:")
struct Fixture {
db: Database<Backend>,
#[cfg(feature = "postgres")]
owned: owned_postgres::OwnedPostgres,
}
impl std::ops::Deref for Fixture {
type Target = Database<Backend>;
fn deref(&self) -> &Self::Target {
&self.db
}
}
impl std::ops::DerefMut for Fixture {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.db
}
}
impl Fixture {
async fn finish(self) {
self.db.pool().close().await;
#[cfg(feature = "postgres")]
{
let mut owned = self.owned;
owned.cleanup().unwrap();
}
}
}

async fn fixture(visibility: Option<Visibility>) -> Fixture {
#[cfg(feature = "sqlite")]
let mut db = Database::<Backend>::connect_sqlite("sqlite::memory:")
.await
.unwrap();
#[cfg(feature = "postgres")]
let owned = owned_postgres::OwnedPostgres::start("repository-counts").unwrap();
#[cfg(feature = "postgres")]
let mut db = Database::<Backend>::connect_postgres(&owned.url)
.await
.unwrap();
sqlx::query("CREATE TABLE count_rows(id TEXT PRIMARY KEY, tenant TEXT NOT NULL)")
Expand All @@ -132,7 +188,11 @@ async fn fixture(visibility: Option<Visibility>) -> Database<SqliteBackend> {
if let Some(policy) = visibility {
db.set_row_policy(policy);
}
db
Fixture {
db,
#[cfg(feature = "postgres")]
owned,
}
}

#[tokio::test]
Expand All @@ -145,6 +205,7 @@ async fn unrestricted_policy_counts_inside_repository_transaction() {
.await
.unwrap();
assert_eq!(total, 3);
db.finish().await;
}

#[tokio::test]
Expand All @@ -171,6 +232,7 @@ async fn complete_visibility_intersects_caller_filter_and_exists() {
.await
.unwrap();
assert_eq!(result, (2, false));
db.finish().await;
}

#[tokio::test]
Expand All @@ -184,6 +246,7 @@ async fn callback_and_prefilter_counts_remain_rejected() {
.await
.is_err()
);
db.finish().await;
}
}

Expand All @@ -197,6 +260,7 @@ async fn no_policy_count_keeps_existing_behavior() {
.await
.unwrap();
assert_eq!(count, 3);
db.finish().await;
}

#[tokio::test]
Expand All @@ -209,6 +273,7 @@ async fn wrong_entity_predicate_is_rejected_before_sql() {
.await
.is_err()
);
db.finish().await;
}

#[tokio::test]
Expand All @@ -222,6 +287,7 @@ async fn unrestricted_visibility_does_not_override_entity_denial() {
.await
.is_err()
);
db.finish().await;
}

#[tokio::test]
Expand All @@ -241,6 +307,7 @@ async fn counts_use_the_current_policy_instead_of_reusing_visibility() {
.await
.unwrap();
assert_eq!((before, after), (3, 2));
db.finish().await;
}

#[tokio::test]
Expand Down Expand Up @@ -270,4 +337,5 @@ async fn count_sees_writes_in_the_same_transaction_and_rollback_keeps_them_priva
.await
.unwrap();
assert_eq!(after, 2);
db.finish().await;
}
8 changes: 8 additions & 0 deletions docs/reference/graphql-orm/runtime-and-writes.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,11 @@ impl Job {
```

If a computed field needs database access, use request-scoped `DataLoader` or another batching mechanism. Generated relation fields already use the relation batching runtime; custom computed fields should follow the same rule to avoid N+1 behavior.

Transaction count regressions run on memory SQLite and individually owned
PostgreSQL containers (never an ambient application database):

```bash
cargo test -p graphql-orm --locked --no-default-features --features sqlite --test repository_counts
cargo test -p graphql-orm --locked --no-default-features --features postgres --test repository_counts -- --test-threads=1
```
Loading