Skip to content

Commit e2d2aef

Browse files
Add validators when using sqlalchemy.and_(). (#796)
1 parent 4f4e065 commit e2d2aef

File tree

2 files changed

+46
-30
lines changed

2 files changed

+46
-30
lines changed

aiohttp_admin/backends/sqlalchemy.py

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -367,38 +367,47 @@ def _get_validators(self, table: sa.Table, c: sa.Column[object]) -> list[Functio
367367
for constr in table.constraints:
368368
if not isinstance(constr, sa.CheckConstraint):
369369
continue
370-
if isinstance(constr.sqltext, sa.BinaryExpression):
371-
left = constr.sqltext.left
372-
right = constr.sqltext.right
373-
op = constr.sqltext.operator
374-
if left.expression is c:
375-
if not isinstance(right, sa.BindParameter) or right.value is None:
376-
continue
377-
if op is operator.ge: # type: ignore[comparison-overlap]
378-
validators.append(func("minValue", (right.value,)))
379-
elif op is operator.gt: # type: ignore[comparison-overlap]
380-
validators.append(func("minValue", (right.value + 1,)))
381-
elif op is operator.le: # type: ignore[comparison-overlap]
382-
validators.append(func("maxValue", (right.value,)))
383-
elif op is operator.lt: # type: ignore[comparison-overlap]
384-
validators.append(func("maxValue", (right.value - 1,)))
385-
elif isinstance(left, sa.Function):
386-
if left.name == "char_length":
387-
if next(iter(left.clauses)) is not c:
388-
continue
370+
371+
if isinstance(constr.sqltext, sa.BooleanClauseList):
372+
if constr.sqltext.operator is not operator.and_: # type: ignore[comparison-overlap]
373+
continue
374+
exprs = constr.sqltext.clauses
375+
else:
376+
exprs = (constr.sqltext,)
377+
378+
for expr in exprs:
379+
if isinstance(expr, sa.BinaryExpression):
380+
left = expr.left
381+
right = expr.right
382+
op = expr.operator
383+
if left.expression is c:
389384
if not isinstance(right, sa.BindParameter) or right.value is None:
390385
continue
391386
if op is operator.ge: # type: ignore[comparison-overlap]
392-
validators.append(func("minLength", (right.value,)))
387+
validators.append(func("minValue", (right.value,)))
393388
elif op is operator.gt: # type: ignore[comparison-overlap]
394-
validators.append(func("minLength", (right.value + 1,)))
395-
elif isinstance(constr.sqltext, sa.Function):
396-
if constr.sqltext.name in ("regexp", "regexp_like"):
397-
clauses = tuple(constr.sqltext.clauses)
398-
if clauses[0] is not c or not isinstance(clauses[1], sa.BindParameter):
399-
continue
400-
if clauses[1].value is None:
401-
continue
402-
validators.append(func("regex", (regex(clauses[1].value),)))
389+
validators.append(func("minValue", (right.value + 1,)))
390+
elif op is operator.le: # type: ignore[comparison-overlap]
391+
validators.append(func("maxValue", (right.value,)))
392+
elif op is operator.lt: # type: ignore[comparison-overlap]
393+
validators.append(func("maxValue", (right.value - 1,)))
394+
elif isinstance(left, sa.Function):
395+
if left.name == "char_length":
396+
if next(iter(left.clauses)) is not c:
397+
continue
398+
if not isinstance(right, sa.BindParameter) or right.value is None:
399+
continue
400+
if op is operator.ge: # type: ignore[comparison-overlap]
401+
validators.append(func("minLength", (right.value,)))
402+
elif op is operator.gt: # type: ignore[comparison-overlap]
403+
validators.append(func("minLength", (right.value + 1,)))
404+
elif isinstance(expr, sa.Function):
405+
if expr.name in ("regexp", "regexp_like"):
406+
clauses = tuple(expr.clauses)
407+
if clauses[0] is not c or not isinstance(clauses[1], sa.BindParameter):
408+
continue
409+
if clauses[1].value is None:
410+
continue
411+
validators.append(func("regex", (regex(clauses[1].value),)))
403412

404413
return validators

tests/test_backends_sqlalchemy.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,12 +276,16 @@ class TestCC(base): # type: ignore[misc,valid-type]
276276
min_length: Mapped[str] = mapped_column()
277277
min_length_gt: Mapped[str] = mapped_column()
278278
regex: Mapped[str] = mapped_column()
279+
with_and: Mapped[int] = mapped_column()
280+
with_or: Mapped[int] = mapped_column()
279281

280282
__table_args__ = (sa.CheckConstraint(gt > 3), sa.CheckConstraint(gte >= 3),
281283
sa.CheckConstraint(lt < 3), sa.CheckConstraint(lte <= 3),
282284
sa.CheckConstraint(sa.func.char_length(min_length) >= 5),
283285
sa.CheckConstraint(sa.func.char_length(min_length_gt) > 5),
284-
sa.CheckConstraint(sa.func.regexp(regex, r"abc.*")))
286+
sa.CheckConstraint(sa.func.regexp(regex, r"abc.*")),
287+
sa.CheckConstraint(sa.and_(with_and > 7, with_and < 12)),
288+
sa.CheckConstraint(sa.or_(with_or > 7, with_or < 12)))
285289

286290
r = SAResource(mock_engine, TestCC)
287291

@@ -300,6 +304,9 @@ class TestCC(base): # type: ignore[misc,valid-type]
300304
assert f["min_length"]["props"]["validate"] == [required, func("minLength", (5,))]
301305
assert f["min_length_gt"]["props"]["validate"] == [required, func("minLength", (6,))]
302306
assert f["regex"]["props"]["validate"] == [required, func("regex", (regex("abc.*"),))]
307+
assert f["with_and"]["props"]["validate"] == [
308+
required, func("minValue", (8,)), func("maxValue", (11,))]
309+
assert f["with_or"]["props"]["validate"] == [required]
303310

304311

305312
async def test_nonid_pk(base: type[DeclarativeBase], mock_engine: AsyncEngine) -> None:

0 commit comments

Comments
 (0)