raise ValueError when querying on private columns

This commit is contained in:
Michael Genson
2026-05-14 18:45:47 +00:00
parent b0acd415af
commit 7eb8836c14

View File

@@ -199,10 +199,18 @@ class QueryFilterBuilder:
if i == len(group) - 1: if i == len(group) - 1:
return consolidated_group_builder.self_group() return consolidated_group_builder.self_group()
@classmethod
def _get_model_attr(cls, model: type[SqlAlchemyBase], attr_name: str) -> InstrumentedAttribute:
model_attr: InstrumentedAttribute = getattr(model, attr_name)
if getattr(model_attr, "info", {}).get("private"):
raise ValueError(f"cannot filter on private field '{model.__name__}.{attr_name}'")
return model_attr
@classmethod @classmethod
def get_model_and_model_attr_from_attr_string[Model: SqlAlchemyBase]( def get_model_and_model_attr_from_attr_string[Model: SqlAlchemyBase](
cls, attr_string: str, model: type[Model], *, query: sa.Select | None = None cls, attr_string: str, model: type[Model], *, query: sa.Select | None = None
) -> tuple[SqlAlchemyBase, InstrumentedAttribute, sa.Select | None]: ) -> tuple[type[SqlAlchemyBase], InstrumentedAttribute, sa.Select | None]:
""" """
Take an attribute string and traverse a database model and its relationships to get the desired Take an attribute string and traverse a database model and its relationships to get the desired
model and model attribute. Optionally provide a query to apply the necessary table joins. model and model attribute. Optionally provide a query to apply the necessary table joins.
@@ -222,17 +230,17 @@ class QueryFilterBuilder:
if not attribute_chain: if not attribute_chain:
raise ValueError("invalid query string: attribute name cannot be empty") raise ValueError("invalid query string: attribute name cannot be empty")
current_model: SqlAlchemyBase = model # type: ignore current_model: type[SqlAlchemyBase] = model
for i, attribute_link in enumerate(attribute_chain): for i, attribute_link in enumerate(attribute_chain):
try: try:
model_attr = getattr(current_model, attribute_link) model_attr = cls._get_model_attr(current_model, attribute_link)
# proxied attributes can't be joined to the query directly, so we need to inspect the proxy # proxied attributes can't be joined to the query directly, so we need to inspect the proxy
# and get the actual model and its attribute # and get the actual model and its attribute
if isinstance(model_attr, AssociationProxyInstance): if isinstance(model_attr, AssociationProxyInstance):
proxied_attribute_link = model_attr.target_collection proxied_attribute_link = model_attr.target_collection
next_attribute_link = model_attr.value_attr next_attribute_link = model_attr.value_attr
model_attr = getattr(current_model, proxied_attribute_link) model_attr = cls._get_model_attr(current_model, proxied_attribute_link)
if query is not None: if query is not None:
query = query.join(model_attr, isouter=True) query = query.join(model_attr, isouter=True)
@@ -240,7 +248,7 @@ class QueryFilterBuilder:
mapper = sa.inspect(current_model) mapper = sa.inspect(current_model)
relationship = mapper.relationships[proxied_attribute_link] relationship = mapper.relationships[proxied_attribute_link]
current_model = relationship.mapper.class_ current_model = relationship.mapper.class_
model_attr = getattr(current_model, next_attribute_link) model_attr = cls._get_model_attr(current_model, next_attribute_link)
# at the end of the chain there are no more relationships to inspect # at the end of the chain there are no more relationships to inspect
if i == len(attribute_chain) - 1: if i == len(attribute_chain) - 1:
@@ -299,7 +307,9 @@ class QueryFilterBuilder:
if len(value) == 1: if len(value) == 1:
element = model_attr.in_(value) element = model_attr.in_(value)
else: else:
primary_model_attr: InstrumentedAttribute = getattr(model, component.attribute_name.split(".")[0]) primary_model_attr: InstrumentedAttribute = cls._get_model_attr(
model, component.attribute_name.split(".")[0]
)
element = sa.and_(*(primary_model_attr.any(model_attr == v) for v in value)) element = sa.and_(*(primary_model_attr.any(model_attr == v) for v in value))
elif component.relationship is RelationalKeyword.LIKE: elif component.relationship is RelationalKeyword.LIKE:
element = model_attr.ilike(value) element = model_attr.ilike(value)
@@ -368,7 +378,7 @@ class QueryFilterBuilder:
else: else:
component = cast(QueryFilterBuilderComponent, component) component = cast(QueryFilterBuilderComponent, component)
base_attribute_name = component.attribute_name.split(".")[-1] base_attribute_name = component.attribute_name.split(".")[-1]
model_attr = getattr(attr_model_map[i], base_attribute_name) model_attr = self._get_model_attr(attr_model_map[i], base_attribute_name)
if (column_alias := column_aliases.get(base_attribute_name)) is not None: if (column_alias := column_aliases.get(base_attribute_name)) is not None:
model_attr = column_alias model_attr = column_alias