From fbd625d1083141007c42eff51afca6b5432784e6 Mon Sep 17 00:00:00 2001 From: Yian Shang Date: Mon, 20 Apr 2026 09:15:00 -0700 Subject: [PATCH 1/4] The per resolver session pattern means that we need to return strawberry objects rather than ORM objects --- .../api/graphql/scalars/node.py | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/node.py b/datajunction-server/datajunction_server/api/graphql/scalars/node.py index 0039de2ff..2c2bbe1c8 100644 --- a/datajunction-server/datajunction_server/api/graphql/scalars/node.py +++ b/datajunction-server/datajunction_server/api/graphql/scalars/node.py @@ -7,7 +7,7 @@ import strawberry from strawberry.scalars import JSON from strawberry.types import Info -from sqlalchemy.orm.attributes import InstrumentedAttribute +from sqlalchemy.orm.attributes import InstrumentedAttribute, set_committed_value from datajunction_server.api.graphql.scalars import BigInt from datajunction_server.api.graphql.scalars.availabilitystate import ( @@ -221,14 +221,32 @@ def columns( def dimension_links(self) -> list[DimensionLink]: """ Returns the dimension links for this node revision. + + By the time this resolver runs, the parent's session has closed and + each DimensionLink is detached from it. The `foreign_keys` + hybrid_property walks `link.node_revision.name` — a lazy relationship + that would fail on a detached instance. We already have the owning + NodeRevision (`self`), so pre-seed `link.node_revision` via + `set_committed_value` to short-circuit the lazy load, then compute + `foreign_keys` while we still control the call site. """ - return [ - link - for link in self.dimension_links - if link.dimension is not None # handles hard-deleted dimension nodes - and link.dimension.deactivated_at - is None # handles deactivated dimension nodes - ] + links = [] + for link in self.dimension_links: + if link.dimension is None or link.dimension.deactivated_at is not None: + continue # hard-deleted or deactivated dimension node + set_committed_value(link, "node_revision", self) + links.append( + DimensionLink( # type: ignore[call-arg] + dimension=NodeName(name=link.dimension.name), # type: ignore[call-arg] + join_type=link.join_type or JoinType_.LEFT, + join_sql=link.join_sql, + join_cardinality=link.join_cardinality, + role=link.role, + foreign_keys=link.foreign_keys, + default_value=link.default_value, + ), + ) + return links parents: List[NodeNameVersion] From 85f54d3dabed8169387eb3149db803be6cf59147 Mon Sep 17 00:00:00 2001 From: Yian Shang Date: Mon, 20 Apr 2026 15:08:20 -0700 Subject: [PATCH 2/4] Fix --- .../api/graphql/scalars/node.py | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/node.py b/datajunction-server/datajunction_server/api/graphql/scalars/node.py index 2c2bbe1c8..acc3ed819 100644 --- a/datajunction-server/datajunction_server/api/graphql/scalars/node.py +++ b/datajunction-server/datajunction_server/api/graphql/scalars/node.py @@ -223,29 +223,19 @@ def dimension_links(self) -> list[DimensionLink]: Returns the dimension links for this node revision. By the time this resolver runs, the parent's session has closed and - each DimensionLink is detached from it. The `foreign_keys` - hybrid_property walks `link.node_revision.name` — a lazy relationship - that would fail on a detached instance. We already have the owning - NodeRevision (`self`), so pre-seed `link.node_revision` via - `set_committed_value` to short-circuit the lazy load, then compute - `foreign_keys` while we still control the call site. + each DimensionLink is detached. The `foreign_keys` hybrid_property + walks `link.node_revision.name` — a lazy relationship that would fail + on a detached instance. We already have the owning NodeRevision + (`self`), so pre-seed `link.node_revision` via `set_committed_value` + to short-circuit the lazy load. Strawberry duck-types the raw ORM + link the rest of the way. """ links = [] for link in self.dimension_links: if link.dimension is None or link.dimension.deactivated_at is not None: continue # hard-deleted or deactivated dimension node set_committed_value(link, "node_revision", self) - links.append( - DimensionLink( # type: ignore[call-arg] - dimension=NodeName(name=link.dimension.name), # type: ignore[call-arg] - join_type=link.join_type or JoinType_.LEFT, - join_sql=link.join_sql, - join_cardinality=link.join_cardinality, - role=link.role, - foreign_keys=link.foreign_keys, - default_value=link.default_value, - ), - ) + links.append(link) return links parents: List[NodeNameVersion] From 63542f5fc42c513376c15b1837e3e7458f5cd8eb Mon Sep 17 00:00:00 2001 From: Yian Shang Date: Mon, 20 Apr 2026 15:12:52 -0700 Subject: [PATCH 3/4] Simplify --- .../datajunction_server/api/graphql/scalars/node.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/node.py b/datajunction-server/datajunction_server/api/graphql/scalars/node.py index acc3ed819..83e7a6633 100644 --- a/datajunction-server/datajunction_server/api/graphql/scalars/node.py +++ b/datajunction-server/datajunction_server/api/graphql/scalars/node.py @@ -230,13 +230,14 @@ def dimension_links(self) -> list[DimensionLink]: to short-circuit the lazy load. Strawberry duck-types the raw ORM link the rest of the way. """ - links = [] for link in self.dimension_links: - if link.dimension is None or link.dimension.deactivated_at is not None: - continue # hard-deleted or deactivated dimension node set_committed_value(link, "node_revision", self) - links.append(link) - return links + return [ + link + for link in self.dimension_links + if link.dimension is not None # hard-deleted dimension nodes + and link.dimension.deactivated_at is None # deactivated dimension nodes + ] parents: List[NodeNameVersion] From 05b76de848ad347db2f072f92a30a2decfed9071 Mon Sep 17 00:00:00 2001 From: Yian Shang Date: Mon, 20 Apr 2026 15:15:34 -0700 Subject: [PATCH 4/4] Clean up --- .../api/graphql/scalars/node.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/datajunction-server/datajunction_server/api/graphql/scalars/node.py b/datajunction-server/datajunction_server/api/graphql/scalars/node.py index 83e7a6633..4a20a14be 100644 --- a/datajunction-server/datajunction_server/api/graphql/scalars/node.py +++ b/datajunction-server/datajunction_server/api/graphql/scalars/node.py @@ -221,22 +221,16 @@ def columns( def dimension_links(self) -> list[DimensionLink]: """ Returns the dimension links for this node revision. - - By the time this resolver runs, the parent's session has closed and - each DimensionLink is detached. The `foreign_keys` hybrid_property - walks `link.node_revision.name` — a lazy relationship that would fail - on a detached instance. We already have the owning NodeRevision - (`self`), so pre-seed `link.node_revision` via `set_committed_value` - to short-circuit the lazy load. Strawberry duck-types the raw ORM - link the rest of the way. """ + # Pre-seed each link's node rev to short-circuit the lazy load. for link in self.dimension_links: set_committed_value(link, "node_revision", self) return [ link for link in self.dimension_links - if link.dimension is not None # hard-deleted dimension nodes - and link.dimension.deactivated_at is None # deactivated dimension nodes + if link.dimension is not None # handles hard-deleted dimension nodes + and link.dimension.deactivated_at + is None # handles deactivated dimension nodes ] parents: List[NodeNameVersion]