More attempts to fix a bug.
This commit is contained in:
parent
bd2daf921a
commit
dd863dba99
3 changed files with 102 additions and 21 deletions
|
|
@ -12,7 +12,7 @@ from sqlalchemy.sql.elements import UnaryExpression, ColumnElement
|
|||
|
||||
from crudkit.core import to_jsonable, deep_diff, diff_to_patch, filter_to_columns, normalize_payload
|
||||
from crudkit.core.base import Version
|
||||
from crudkit.core.meta import rel_map
|
||||
from crudkit.core.meta import rel_map, column_names_for_model
|
||||
from crudkit.core.params import is_truthy, normalize_fields_param
|
||||
from crudkit.core.spec import CRUDSpec, CollPred
|
||||
from crudkit.core.types import OrderSpec, SeekWindow
|
||||
|
|
@ -281,7 +281,10 @@ class CRUDService(Generic[T]):
|
|||
def _stable_order_by(self, root_alias, given_order_by):
|
||||
order_by = list(given_order_by or [])
|
||||
if not order_by:
|
||||
# Safe default: primary key(s) only. No unlabeled expressions.
|
||||
return _dedupe_order_by(self._default_order_by(root_alias))
|
||||
|
||||
# Dedupe what the user gave us, then ensure PK tie-breakers exist
|
||||
order_by = _dedupe_order_by(order_by)
|
||||
mapper: Mapper[Any] = cast(Mapper[Any], inspect(self.model))
|
||||
present = {_order_identity(_unwrap_ob(ob)[0]) for ob in order_by}
|
||||
|
|
@ -355,7 +358,16 @@ class CRUDService(Generic[T]):
|
|||
join_paths = tuple(spec.get_join_paths())
|
||||
filter_tables = _collect_tables_from_filters(filters)
|
||||
fkeys = set()
|
||||
_, proj_opts = compile_projection(self.model, req_fields) if req_fields else ([], [])
|
||||
# Build projection opts only if there are true scalar columns requested.
|
||||
# Bare relationship fields like "owner" should not force root column pruning.
|
||||
column_names = set(column_names_for_model(self.model))
|
||||
has_scalar_column_tokens = any(
|
||||
(("." not in f) and (f in column_names))
|
||||
for f in (req_fields or [])
|
||||
)
|
||||
_, proj_opts = (compile_projection(self.model, req_fields)
|
||||
if (req_fields and has_scalar_column_tokens)
|
||||
else ([], []))
|
||||
|
||||
# filter_tables = ()
|
||||
# fkeys = set()
|
||||
|
|
@ -374,8 +386,25 @@ class CRUDService(Generic[T]):
|
|||
|
||||
def _apply_firsthop_strategies(self, query, root_alias, plan: _Plan):
|
||||
joined_rel_keys: set[str] = set()
|
||||
rels = rel_map(self.model)
|
||||
rels = rel_map(self.model) # {name: RelInfo}
|
||||
|
||||
# Eager join to-one relationships requested as bare fields (e.g., fields=owner)
|
||||
requested_scalars = set(plan.root_field_names or [])
|
||||
for key in requested_scalars:
|
||||
info = rels.get(key)
|
||||
if info and not info.uselist and key not in joined_rel_keys:
|
||||
query = query.join(getattr(root_alias, key), isouter=True)
|
||||
joined_rel_keys.add(key)
|
||||
|
||||
# 1) Join to-one relationships explicitly requested as bare fields
|
||||
requested_scalars = set(plan.root_field_names or []) # names like "owner", "supervisor"
|
||||
for key in requested_scalars:
|
||||
info = rels.get(key)
|
||||
if info and not info.uselist and key not in joined_rel_keys:
|
||||
query = query.join(getattr(root_alias, key), isouter=True)
|
||||
joined_rel_keys.add(key)
|
||||
|
||||
# 2) Join to-one relationships from parsed join_paths
|
||||
for base_alias, rel_attr, target_alias in plan.join_paths:
|
||||
if base_alias is not root_alias:
|
||||
continue
|
||||
|
|
@ -384,6 +413,7 @@ class CRUDService(Generic[T]):
|
|||
query = query.join(target_alias, rel_attr.of_type(target_alias), isouter=True)
|
||||
joined_rel_keys.add(prop.key if prop is not None else rel_attr.key)
|
||||
|
||||
# 3) Ensure to-one touched by filters is joined
|
||||
if plan.filter_tables:
|
||||
for key, info in rels.items():
|
||||
if info.uselist or not info.target_cls:
|
||||
|
|
@ -393,6 +423,7 @@ class CRUDService(Generic[T]):
|
|||
query = query.join(getattr(root_alias, key), isouter=True)
|
||||
joined_rel_keys.add(key)
|
||||
|
||||
# 4) Collections via selectinload, optionally load_only for requested child columns
|
||||
for base_alias, rel_attr, _target_alias in plan.join_paths:
|
||||
if base_alias is not root_alias:
|
||||
continue
|
||||
|
|
@ -408,10 +439,46 @@ class CRUDService(Generic[T]):
|
|||
if cols:
|
||||
opt = opt.load_only(*cols)
|
||||
query = query.options(opt)
|
||||
|
||||
for path, _names in (plan.rel_field_names or {}).items():
|
||||
if not path:
|
||||
continue
|
||||
# Build a chained selectinload for each relationship segment in the path
|
||||
first = path[0]
|
||||
info = rels.get(first)
|
||||
if not info or info.target_cls is None:
|
||||
continue
|
||||
|
||||
# Start with selectinload on the first hop
|
||||
opt = selectinload(getattr(root_alias, first))
|
||||
|
||||
# Walk deeper segments
|
||||
cur_cls = info.target_cls
|
||||
for seg in path[1:]:
|
||||
sub = rel_map(cur_cls).get(seg)
|
||||
if not sub or sub.target_cls is None:
|
||||
# if segment isn't a relationship, we stop the chain
|
||||
break
|
||||
opt = opt.selectinload(getattr(cur_cls, seg))
|
||||
cur_cls = sub.target_cls
|
||||
|
||||
query = query.options(opt)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def _apply_proj_opts(self, query, plan: _Plan):
|
||||
return query.options(*plan.proj_opts) if plan.proj_opts else query
|
||||
if not plan.proj_opts:
|
||||
return query
|
||||
try:
|
||||
return query.options(*plan.proj_opts)
|
||||
except KeyError as e:
|
||||
# Seen "KeyError: 'col'" when alias-column remapping meets unlabeled exprs.
|
||||
log.debug("Projection options disabled due to %r; proceeding without them.", e)
|
||||
return query
|
||||
except Exception as e:
|
||||
log.debug("Projection options failed (%r); proceeding without them.", e)
|
||||
return query
|
||||
|
||||
def _projection_meta(self, plan: _Plan):
|
||||
if plan.req_fields:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue