From 225c49ab697e7808f71ea0ac7cb32e6faaacfddc Mon Sep 17 00:00:00 2001 From: "molotkov.v" Date: Tue, 21 Jul 2026 18:38:31 +0300 Subject: [PATCH 1/4] feat: Add support validating indexes with where statement --- docs/db_validation.md | 15 ++- docs/ru/db_validation.md | 18 ++- .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 504 bytes .../__pycache__/filters.cpython-314.pyc | Bin 0 -> 313 bytes .../__pycache__/model_manager.cpython-314.pyc | Bin 0 -> 75588 bytes .../__pycache__/ordering.cpython-314.pyc | Bin 0 -> 4184 bytes .../__pycache__/utils.cpython-314.pyc | Bin 0 -> 3367 bytes fastapi_sqlalchemy_toolkit/model_manager.py | 85 ++++++++++++- tests/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 170 bytes ...partial_where.cpython-314-pytest-9.1.1.pyc | Bin 0 -> 18715 bytes tests/models.py | 64 +++++++++- tests/test_partial_where.py | 115 ++++++++++++++++++ tests/test_public_methods.py | 68 +++++++++++ 13 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/__init__.cpython-314.pyc create mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/filters.cpython-314.pyc create mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/model_manager.cpython-314.pyc create mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/ordering.cpython-314.pyc create mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/utils.cpython-314.pyc create mode 100644 tests/__pycache__/__init__.cpython-314.pyc create mode 100644 tests/__pycache__/test_partial_where.cpython-314-pytest-9.1.1.pyc create mode 100644 tests/test_partial_where.py diff --git a/docs/db_validation.md b/docs/db_validation.md index 7722381..12d584c 100644 --- a/docs/db_validation.md +++ b/docs/db_validation.md @@ -24,4 +24,17 @@ If the uniqueness constraint is violated, an `fastapi.HTTPException` will be rai If the model defines unique constraints using `sqlalchemy.UniqueConstraint`, then when using the `create` or `update` methods, an SQL query will be executed to verify that no other objects with the same combination of field values in the unique constraint exist. -If the unique constraint is violated, an `fastapi.HTTPException` will be raised. \ No newline at end of file +If the unique constraint is violated, an `fastapi.HTTPException` will be raised. + +## Unique Indexes Validation + +If the model defines a unique `sqlalchemy.Index`, then when using the `create` or `update` methods, an SQL query will be executed to verify that no other object violates that index. + +For **partial** unique indexes (`postgresql_where=...`), validation runs only when the created/updated row itself matches the `WHERE` predicate. Rows outside the partial index are not checked against it. + +Supported `postgresql_where` forms for deciding whether the row is covered: + +- Column expressions such as `Model.kind == "main"` (including `and_(...)` of equalities) +- Simple `text()` equalities such as `text("kind = 'main'")` + +Unsupported / complex predicates are treated as matching (fail-safe): validation still runs. \ No newline at end of file diff --git a/docs/ru/db_validation.md b/docs/ru/db_validation.md index d887c13..14e3d58 100644 --- a/docs/ru/db_validation.md +++ b/docs/ru/db_validation.md @@ -32,4 +32,20 @@ Ecли `Post.slug` -- поле, определённое с `unique=True`, и з при использовании методов `create` или `update` будет выполнен SQL запрос для проверки существования других объектов с таким же набором значений полей, входящих в уникальное огранчение. -Если уникальное ограничение будет нарушено, вызывается `fastapi.HTTPException`. \ No newline at end of file +Если уникальное ограничение будет нарушено, вызывается `fastapi.HTTPException`. + +## Валидация уникальных индексов + +Если в модели определён уникальный `sqlalchemy.Index`, то при использовании методов +`create` или `update` будет выполнен SQL запрос для проверки, что индекс не нарушается. + +Для **частичных** уникальных индексов (`postgresql_where=...`) валидация выполняется +только если создаваемая/обновляемая запись сама попадает под условие `WHERE`. +Записи вне partial index по этому индексу не проверяются. + +Поддерживаемые формы `postgresql_where` для определения покрытия записи: + +- выражения по колонкам, например `Model.kind == "main"` (включая `and_(...)` из равенств) +- простые равенства через `text()`, например `text("kind = 'main'")` + +Неподдерживаемые / сложные предикаты считаются подходящими (fail-safe): валидация всё равно выполняется. \ No newline at end of file diff --git a/fastapi_sqlalchemy_toolkit/__pycache__/__init__.cpython-314.pyc b/fastapi_sqlalchemy_toolkit/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c48f00efac55f021eff94273407cb1baf259a4f0 GIT binary patch literal 504 zcmYLF%}PQ+6rS;V{WruGwG#>g3G)S_O%eBkN*V2iLp~@#At*wzhPwVSs%O5j6{}-Y?o5TCPlDY}nVQ!< zlW)t2i7Va%Q(D6|9n%3*=1@-r6}@K`&q?fR2uKpyI;&EVDq@O{2P9$%8%Np-l`mx+ z(>s=|mCb(7g+#N2Ox#bwHJEBlM~oy?0jFZ4+NqaJw$BAHrIlxgH04dVcr%Ae+nn;9 z%Q!Z-ZLP8{^ryguRyoJ#opX16Lo4f%d1&5iE8C2vkT8<7vsv>-$sy~3ZuY4Ln()SM z%LYg(MjW;j*->_Z4Mhkab0Fj_nVa0X9ds(#g{s2WCb_#=8ts4+O+#a9tHpZs5HkqHu{pum}`l0Mg4? AuK)l5 literal 0 HcmV?d00001 diff --git a/fastapi_sqlalchemy_toolkit/__pycache__/model_manager.cpython-314.pyc b/fastapi_sqlalchemy_toolkit/__pycache__/model_manager.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20541b2c053ebfb29fddc19c79bb438297e62ca7 GIT binary patch literal 75588 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

yn9S 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 Po}Jh1yS`!+#+&s&&j!f4 literal 0 HcmV?d00001 diff --git a/fastapi_sqlalchemy_toolkit/__pycache__/ordering.cpython-314.pyc b/fastapi_sqlalchemy_toolkit/__pycache__/ordering.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a73b9ad77d437b68d3672edca2d109a81562a865 GIT binary patch literal 4184 zcmc&%U2Id=9Y5FCcI=x#LOw{#SBy;oFOI!JG^UWEMJ+U0OO`pp>>;XW;tQBMwz>D3 zHQhReq}{02F&Iom+N7xx(&*CyONN0YtS@`Mx4|O2Bih5*L*J4G#P+oP{^$C}b^_Ag zb}XO!Ki~iJ`+uJHjT~0M zR03Vg>H270fCV>vMGOQ5(=#Za&C8i#8MO0oCO?9&w3wJvfmNfV$%#L4Y}=B#DP1BU(p(~HLE0L zC6zf9Kbex#Ni{x_$mP&9-HNQ`l}ylSI+Lkk%@lD5#e*dZodE9LC$q{(LQ{!hzc~~+ zrpk&M8Of%z+Pm3v;d7AzC3{95*3`(!gsLTSsSfpQI*}eeEsu!@iOGHsm>D7S(+6SI!odrG2}nMu%v&c&*?Yyf-$B~>eI%! zkCe-&7xddc{T7C9>ochTsLvWdGd}X6Ijc_^Kh>x4E$DZA#(90t7&FHC26I?(S13cl|^L3;-JBva@xT}fFTjt509T%{f?DTEI?>hLio z8SKOp&|TG(xT3@}i4j?)+wS)Hlua~g(J3i{AG0deJ9TnVlNo#m}ZST?1quHdK4r?hbEr)z|b6Aln2Es`M zCH6A3lg@lv=8`= zg9TDZ2o{@67#EF?aRzWMXtl~Aa{vdSasRRmsaKy zr&1ZN)zll3EI&Zn0)SsEATp_OGA2?m`0HBt-o_f;Joi$&FO~78&srB2G(YL~P>%&_hdcW}!N>wDUABbJO^Uu7s=1V8R{WnQSU!b4>@+0L|zB0+4u~ zu7#!kb+MKL@XGg5bqg=ZOIOpOLBV#(h?LLCQ7avJRIF_AXzH}0smGQe52O7^N+_?e z;;0u?(7{6|PgPVp#XnXKpyuvX50V!GcWD5{%hc zl`8USX6VIoT0fgh>uu~w(n6ZpywJ4u(?gdJJ!}dYO`#iu^G&Z_cw?ct{YLNa`#v-qQ`=eh(3N63u`mF0l{>wn;7ZJlB)je&`oY26h zq^3_cv$~c?p6%D`i=N2juA=7~k398ygMZ!=SP}*AD^EqS&b`D$m;2xU+{yxjf=V~} zZA;I7XMeE4tRK#%)5t;b235m}6T>`DHJv<#C&8I+ZIm8`{Qjns-njgEVAhlpCys>q za`_b(#@jGV?*;zWRXJv6YsEOou$-3ZQLHLt#FkAHET1zj2n#t+_eeIGPs@iC(o1qI o>Mm5cl^`s#U5l(`k!@RKoBzSKF0zA*Z2wa?UVG2B*s1S-0p#zMKmY&$ literal 0 HcmV?d00001 diff --git a/fastapi_sqlalchemy_toolkit/__pycache__/utils.cpython-314.pyc b/fastapi_sqlalchemy_toolkit/__pycache__/utils.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f5c8a9752b5a7521018c71dc4c34f774bba8d682 GIT binary patch literal 3367 zcmb7GU2GKB6~6PgyPntsR1ZHh>Y>L2;J}teVQsXz`AljrWf` zvqWyyXd$F63XP3ai98S`ig-v~B4R;tsEvtKsjoA^bjkEDQTmcMI}VDXKD6iD*;(7j zqUx3A-gD2-y?4$z-??{Zb5thK{_{Ki67m3CVk^#=*wjs?A z@&ke;42YIEAX(BtzzQ(FFc`FgAQuKhRtQFMuxUWHxD(8l>hEX0Zl_yluZ8}gamaL3rWL@Dval#G#q-^C%#Z2A}9-k;1=?onL z3+Y(JpcAfqFk>3em9j?uI5JO`D#h$5ZeKqFL8l4uEx!j{m88jw{O)svWP$f6ImW%j zY1}YKyq2o}v_Ho&C9a-%w8jtn!63sSU;h8Ev(`aL9$X@Pg=cW3v7C|5x&dlf6!ZfIvER^yk zYrJ$iaXOizrI(FS%S?`COe<5)Jz<{8XY!*j8HI@_tWqgIp0j+Bz9Q0nQN_yT%|v;^ zjp%x&9xGj^8)3UJeC8hLs^nV|;yRYF`&+nm@|p9`)WTg(xNA`W&c7u2LhtWGmtq%V zwYciURXeQSmAaQCkS&8XVIf2U8-|aC&<3JPhW*e&F#GCsnM;%Pf;ZdTCx)3=hyFwnG;&2FOYvu1x$b-bhsx6^vV@}XiirudL!oWRXwwUsaYJV* zqnGMHqK|=yh3Xg(Rq{=Mgj=VcdLvow|2)3&QuJc<%44;T{Z7aJh4}vS;%(#4g-;5L z0+B-Jj)Lx{_P2~$d%|f?+}#K!=Z5Y@Vv|GXhu-S9!-_2`6bs545TYko1Q82My*SmbR?JQ=47TVeU3_ zfhFuNXRqwJ+n!iN`dZ(te7Cos@mKy43|PZ`;UC52e*ghl>T#i8FE61^V#bOMr3Mip zKjeEkjZZ_x290&HHVLv+qv$jYNeUzSK-Ha=3yM8s-%#v%`?^yF4(P)wb0Gh?NA`4>tvwA3 zUPrxKU^TgF342BX{p-O0$eDy4uR3SFmF+p_4cKWWA=~eu6>x9h$_gxtOE_no*MT>y zqz)YKA5hTLo`*x+Vn?4&$Zjx|p;itqn~<)bgu6x8U0K&5oho^t)gX<*pSzJ!YQPPJ ze1oYLGX)S+OdmJGxE|(ASISvN!DNtA1-cCtWDkyL+uTj3#gw`Y^1-@Gj!Q8~`{3NpZfZAMn0Z998i0B#nCX3ev9MQvb5T4(F)8`=S zAA3RkgBO!k3E-TCv_LhM5s(~SWF~?Cn)4=HYT>M5j?x^fU~6g%zCT!Pfnp9mdH4md z@vGk}ny^wyG;!swSgV!9mx@%cE2r?)F$5PbR21E-FM=zW7NrVo?~3`HX+5mSBtXkl z*-7z&jN=6vzihsH{OwT6$%L4Hz zcp%ldu{=KZ8t3C1Y5`y26Ico04m;J+K~}Io#2lO0^=-F^7jFQNZkUptE^-^kR*U;KyXi^ zK0sE?Kvp4^p-Oo8en_hLr(+R<)4d02H~+vEn1|E2_dHwrCRQ1*K(OSU^Ue=NWriJe zniVK0ak9(xu*aua&gU$EGlr8Up>xB`8~W)?zG8Sl_OIa9(Ndw1(M_YQpMp1(jwwQS zfep)rr+|P9CqhEed%O1D82@PecIU#b!*^mwoLz_C{pFN2HU5|QcA%Zu5j%9`vvBOY zuLTkx;7k!LFUNl%svBGppU&PRA^`uLEB>6KlsyR9TY(}AOSvz9g}~k~Wxr5pA0qet%0%-Rr1(7oz*B^8Elj`|fXo8+LIAX?f(H82oM5>(5@4 a?Crl=h`(x!uih8<7QPz&RtA8ueg6ygUXtwq literal 0 HcmV?d00001 diff --git a/fastapi_sqlalchemy_toolkit/model_manager.py b/fastapi_sqlalchemy_toolkit/model_manager.py index da5c094..a0a1281 100644 --- a/fastapi_sqlalchemy_toolkit/model_manager.py +++ b/fastapi_sqlalchemy_toolkit/model_manager.py @@ -1,5 +1,6 @@ # ruff: noqa: UP006 from collections.abc import Callable, Iterable +from enum import Enum from typing import Any, Generic, List, TypeVar, overload # noqa: UP035 from fastapi import HTTPException, status @@ -25,9 +26,16 @@ from sqlalchemy.orm import DeclarativeBase, contains_eager, load_only from sqlalchemy.orm.attributes import InstrumentedAttribute from sqlalchemy.orm.relationships import Relationship -from sqlalchemy.sql import Select -from sqlalchemy.sql.elements import BindParameter, Null, UnaryExpression -from sqlalchemy.sql.expression import BinaryExpression, ColumnElement +from sqlalchemy.sql import Select, operators +from sqlalchemy.sql.elements import ( + BinaryExpression, + BindParameter, + BooleanClauseList, + Null, + TextClause, + UnaryExpression, +) +from sqlalchemy.sql.expression import ColumnElement from sqlalchemy.sql.functions import Function from sqlalchemy.sql.schema import ScalarElementColumnDefault from sqlalchemy.sql.selectable import Exists @@ -1483,11 +1491,77 @@ async def validate_unique_fields( ), ) + @staticmethod + def _partial_index_values_equal(actual: Any, expected: Any) -> bool: + """Сравнить обычные и Enum-значения из объекта и условия индекса.""" + actual_normalized = actual.name if isinstance(actual, Enum) else actual + expected_normalized = expected.name if isinstance(expected, Enum) else expected + if actual_normalized == expected_normalized: + return True + if isinstance(actual, Enum) and actual.value == expected_normalized: + return True + if isinstance(expected, Enum) and expected.value == actual_normalized: + return True + return str(actual_normalized) == str(expected_normalized) + + @classmethod + def _matches_partial_index_where(cls, condition: Any, in_obj: ModelDict) -> bool: + """ + Проверить, покрывается ли объект условием partial index. + + Неподдерживаемое условие считается совпавшим, чтобы не пропустить + потенциальное нарушение уникальности. + """ + if isinstance(condition, BooleanClauseList): + return all( + cls._matches_partial_index_where(clause, in_obj) for clause in condition + ) + + column_name: str | None = None + expected: Any = None + if isinstance(condition, TextClause): + raw_column, separator, raw_expected = condition.text.partition("=") + column_name = raw_column.strip() + raw_expected = raw_expected.strip() + valid_column = column_name.isascii() and column_name.isidentifier() + + if not separator or not valid_column or not raw_expected: + column_name = None + elif raw_expected[0] in {'"', "'"}: + quote = raw_expected[0] + if raw_expected[-1] == quote and quote not in raw_expected[1:-1]: + expected = raw_expected[1:-1] + else: + column_name = None + elif all(char.isalnum() or char in "._" for char in raw_expected): + expected = raw_expected + else: + column_name = None + elif ( + isinstance(condition, BinaryExpression) + and condition.operator is operators.eq + ): + column_name = getattr(condition.left, "name", None) or getattr( + condition.left, "key", None + ) + expected = getattr( + condition.right, + "value", + getattr(condition.right, "effective_value", condition.right), + ) + + if not isinstance(column_name, str) or column_name not in in_obj: + return True + return cls._partial_index_values_equal(in_obj[column_name], expected) + async def validate_unique_indexes( self, session: AsyncSession, in_obj: ModelDict ) -> None: """ - Валидирует соблюдение уникальности индексов + Валидирует соблюдение уникальности индексов. + + Для partial unique index (`postgresql_where`) проверка выполняется только + если создаваемая/обновляемая запись сама попадает под WHERE. """ for index in self.unique_indexes: condition = ( @@ -1495,6 +1569,9 @@ async def validate_unique_indexes( if index.dialect_options.get("postgresql") else None ) + if not self._matches_partial_index_where(condition, in_obj): + continue + filters = [] for column in index.columns: diff --git a/tests/__pycache__/__init__.cpython-314.pyc b/tests/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e3369df76cc7f94bbe5708bc01e08c2393441ea GIT binary patch literal 170 zcmdPq_I|p@<2{{|u76<*pxEoLW?@ zpPQePUy_|)rdOsPP?VpQnp{$>pO#o$l30+bTU?lvn3J55np>$`lAoWGomrw^l3EN@ s5FekJmst`YuUAlci^C>2KczG$)vkyYXfDX+Vi4mKGb1Bo5i^hl0OL+7>Hq)$ literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_partial_where.cpython-314-pytest-9.1.1.pyc b/tests/__pycache__/test_partial_where.cpython-314-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00c98f36d2d4016c142df30a83277ba7d452f937 GIT binary patch literal 18715 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 literal 0 HcmV?d00001 diff --git a/tests/models.py b/tests/models.py index 87841b0..76bfddf 100644 --- a/tests/models.py +++ b/tests/models.py @@ -2,7 +2,17 @@ from uuid import UUID, uuid4 from pydantic import BaseModel -from sqlalchemy import Column, DateTime, ForeignKey, Table, UniqueConstraint, func +from sqlalchemy import ( + Column, + DateTime, + ForeignKey, + Index, + Table, + UniqueConstraint, + column, + func, + text, +) from sqlalchemy.orm import ( DeclarativeBase, Mapped, @@ -112,3 +122,55 @@ class ItemSchema(BaseModel): item_manager = ModelManager[Item, ItemSchema, ItemSchema](Item) + + +class PartialUniqueText(Base): + """Partial unique index via text() postgresql_where.""" + + group_id: Mapped[UUID] + kind: Mapped[str] + + __table_args__ = ( + Index( + "uix_partial_unique_text_one_main_per_group", + "group_id", + unique=True, + postgresql_where=text("kind = 'main'"), + ), + ) + + +class PartialUniqueTextSchema(BaseModel): + group_id: UUID + kind: str + + +partial_unique_text_manager = ModelManager[ + PartialUniqueText, PartialUniqueTextSchema, PartialUniqueTextSchema +](PartialUniqueText) + + +class PartialUniqueExpr(Base): + """Partial unique index via Column expression postgresql_where.""" + + group_id: Mapped[UUID] + kind: Mapped[str] + + __table_args__ = ( + Index( + "uix_partial_unique_expr_one_main_per_group", + "group_id", + unique=True, + postgresql_where=(column("kind") == "main"), + ), + ) + + +class PartialUniqueExprSchema(BaseModel): + group_id: UUID + kind: str + + +partial_unique_expr_manager = ModelManager[ + PartialUniqueExpr, PartialUniqueExprSchema, PartialUniqueExprSchema +](PartialUniqueExpr) diff --git a/tests/test_partial_where.py b/tests/test_partial_where.py new file mode 100644 index 0000000..b59cfd6 --- /dev/null +++ b/tests/test_partial_where.py @@ -0,0 +1,115 @@ +# ruff: noqa: S101 +from enum import Enum + +from sqlalchemy import Index, and_, text +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + +from fastapi_sqlalchemy_toolkit.model_manager import ModelManager + + +class _Base(DeclarativeBase): + pass + + +class _Kind(Enum): + MAIN = "main" + EXTRA = "extra" + + +class _Model(_Base): + __tablename__ = "partial_where_model" + id: Mapped[int] = mapped_column(primary_key=True) + kind: Mapped[str] + group_id: Mapped[int] + active: Mapped[bool] + + +def test_none_condition_matches() -> None: + assert ModelManager._matches_partial_index_where(None, {"kind": "extra"}) is True + + +def test_text_clause_main_matches() -> None: + for condition in ( + text("kind = 'main'"), + text('kind = "main"'), + text("kind = main"), + ): + assert ( + ModelManager._matches_partial_index_where(condition, {"kind": "main"}) + is True + ) + assert ( + ModelManager._matches_partial_index_where(condition, {"kind": "extra"}) + is False + ) + + +def test_binary_expression_matches() -> None: + index = Index( + "ix", + _Model.group_id, + unique=True, + postgresql_where=(_Model.kind == "main"), + ) + condition = index.dialect_options["postgresql"]["where"] + assert ( + ModelManager._matches_partial_index_where(condition, {"kind": "main"}) is True + ) + assert ( + ModelManager._matches_partial_index_where(condition, {"kind": "extra"}) is False + ) + + +def test_binary_expression_with_enum_matches() -> None: + index = Index( + "ix_enum", + _Model.group_id, + unique=True, + postgresql_where=(_Model.kind == _Kind.MAIN), + ) + condition = index.dialect_options["postgresql"]["where"] + assert ( + ModelManager._matches_partial_index_where(condition, {"kind": "main"}) is True + ) + assert ( + ModelManager._matches_partial_index_where(condition, {"kind": _Kind.MAIN}) + is True + ) + assert ( + ModelManager._matches_partial_index_where(condition, {"kind": "extra"}) is False + ) + + +def test_boolean_clause_list_matches() -> None: + index = Index( + "ix_and", + _Model.group_id, + unique=True, + postgresql_where=and_(_Model.kind == "main", _Model.active == True), # noqa: E712 + ) + condition = index.dialect_options["postgresql"]["where"] + assert ( + ModelManager._matches_partial_index_where( + condition, {"kind": "main", "active": True} + ) + is True + ) + assert ( + ModelManager._matches_partial_index_where( + condition, {"kind": "main", "active": False} + ) + is False + ) + assert ( + ModelManager._matches_partial_index_where( + condition, {"kind": "extra", "active": True} + ) + is False + ) + + +def test_unsupported_text_fail_safe() -> None: + condition = text("kind IN ('main', 'special')") + assert ( + ModelManager._matches_partial_index_where(condition, {"kind": "extra"}) is True + ) diff --git a/tests/test_public_methods.py b/tests/test_public_methods.py index 5dcb481..36c4e86 100644 --- a/tests/test_public_methods.py +++ b/tests/test_public_methods.py @@ -14,10 +14,14 @@ ItemSchema, Parent, ParentSchema, + PartialUniqueExprSchema, + PartialUniqueTextSchema, category_manager, child_manager, item_manager, parent_manager, + partial_unique_expr_manager, + partial_unique_text_manager, ) @@ -1422,3 +1426,67 @@ async def test_custom_pk_unique_validation_on_update(session: AsyncSession): with pytest.raises(HTTPException) as exc_info: await item_manager.update(session, item1, ItemSchema(name="upd-unique-2")) assert exc_info.value.status_code == 422 + + +async def test_partial_unique_index_text_allows_non_matching_kind( + session: AsyncSession, +): + group_id = uuid4() + await partial_unique_text_manager.create( + session, PartialUniqueTextSchema(group_id=group_id, kind="main") + ) + extra = await partial_unique_text_manager.create( + session, PartialUniqueTextSchema(group_id=group_id, kind="extra") + ) + assert extra.kind == "extra" + + +async def test_partial_unique_index_text_rejects_second_main(session: AsyncSession): + group_id = uuid4() + await partial_unique_text_manager.create( + session, PartialUniqueTextSchema(group_id=group_id, kind="main") + ) + with pytest.raises(HTTPException) as exc_info: + await partial_unique_text_manager.create( + session, PartialUniqueTextSchema(group_id=group_id, kind="main") + ) + assert exc_info.value.status_code == 400 + assert "group_id" in exc_info.value.detail + + +async def test_partial_unique_index_text_allows_main_for_other_group( + session: AsyncSession, +): + await partial_unique_text_manager.create( + session, PartialUniqueTextSchema(group_id=uuid4(), kind="main") + ) + other = await partial_unique_text_manager.create( + session, PartialUniqueTextSchema(group_id=uuid4(), kind="main") + ) + assert other.kind == "main" + + +async def test_partial_unique_index_expr_allows_non_matching_kind( + session: AsyncSession, +): + group_id = uuid4() + await partial_unique_expr_manager.create( + session, PartialUniqueExprSchema(group_id=group_id, kind="main") + ) + extra = await partial_unique_expr_manager.create( + session, PartialUniqueExprSchema(group_id=group_id, kind="extra") + ) + assert extra.kind == "extra" + + +async def test_partial_unique_index_expr_rejects_second_main(session: AsyncSession): + group_id = uuid4() + await partial_unique_expr_manager.create( + session, PartialUniqueExprSchema(group_id=group_id, kind="main") + ) + with pytest.raises(HTTPException) as exc_info: + await partial_unique_expr_manager.create( + session, PartialUniqueExprSchema(group_id=group_id, kind="main") + ) + assert exc_info.value.status_code == 400 + assert "group_id" in exc_info.value.detail From cbb97a36af18fdaa467051b55bc07049be6cd6d6 Mon Sep 17 00:00:00 2001 From: "molotkov.v" Date: Tue, 21 Jul 2026 18:43:51 +0300 Subject: [PATCH 2/4] ref: remove pycache --- .../__pycache__/__init__.cpython-314.pyc | Bin 504 -> 0 bytes .../__pycache__/filters.cpython-314.pyc | Bin 313 -> 0 bytes .../__pycache__/model_manager.cpython-314.pyc | Bin 75588 -> 0 bytes .../__pycache__/ordering.cpython-314.pyc | Bin 4184 -> 0 bytes .../__pycache__/utils.cpython-314.pyc | Bin 3367 -> 0 bytes tests/__pycache__/__init__.cpython-314.pyc | Bin 170 -> 0 bytes ...t_partial_where.cpython-314-pytest-9.1.1.pyc | Bin 18715 -> 0 bytes 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/__init__.cpython-314.pyc delete mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/filters.cpython-314.pyc delete mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/model_manager.cpython-314.pyc delete mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/ordering.cpython-314.pyc delete mode 100644 fastapi_sqlalchemy_toolkit/__pycache__/utils.cpython-314.pyc delete mode 100644 tests/__pycache__/__init__.cpython-314.pyc delete mode 100644 tests/__pycache__/test_partial_where.cpython-314-pytest-9.1.1.pyc diff --git a/fastapi_sqlalchemy_toolkit/__pycache__/__init__.cpython-314.pyc b/fastapi_sqlalchemy_toolkit/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index c48f00efac55f021eff94273407cb1baf259a4f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 504 zcmYLF%}PQ+6rS;V{WruGwG#>g3G)S_O%eBkN*V2iLp~@#At*wzhPwVSs%O5j6{}-Y?o5TCPlDY}nVQ!< zlW)t2i7Va%Q(D6|9n%3*=1@-r6}@K`&q?fR2uKpyI;&EVDq@O{2P9$%8%Np-l`mx+ z(>s=|mCb(7g+#N2Ox#bwHJEBlM~oy?0jFZ4+NqaJw$BAHrIlxgH04dVcr%Ae+nn;9 z%Q!Z-ZLP8{^ryguRyoJ#opX16Lo4f%d1&5iE8C2vkT8<7vsv>-$sy~3ZuY4Ln()SM z%LYg(MjW;j*->_Z4Mhkab0Fj_nVa0X9ds(#g{s2WCb_#=8ts4+O+#a9tHpZs5HkqHu{pum}`l0Mg4? AuK)l5 diff --git a/fastapi_sqlalchemy_toolkit/__pycache__/model_manager.cpython-314.pyc b/fastapi_sqlalchemy_toolkit/__pycache__/model_manager.cpython-314.pyc deleted file mode 100644 index 20541b2c053ebfb29fddc19c79bb438297e62ca7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75588 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

yn9S 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 Po}Jh1yS`!+#+&s&&j!f4 diff --git a/fastapi_sqlalchemy_toolkit/__pycache__/ordering.cpython-314.pyc b/fastapi_sqlalchemy_toolkit/__pycache__/ordering.cpython-314.pyc deleted file mode 100644 index a73b9ad77d437b68d3672edca2d109a81562a865..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4184 zcmc&%U2Id=9Y5FCcI=x#LOw{#SBy;oFOI!JG^UWEMJ+U0OO`pp>>;XW;tQBMwz>D3 zHQhReq}{02F&Iom+N7xx(&*CyONN0YtS@`Mx4|O2Bih5*L*J4G#P+oP{^$C}b^_Ag zb}XO!Ki~iJ`+uJHjT~0M zR03Vg>H270fCV>vMGOQ5(=#Za&C8i#8MO0oCO?9&w3wJvfmNfV$%#L4Y}=B#DP1BU(p(~HLE0L zC6zf9Kbex#Ni{x_$mP&9-HNQ`l}ylSI+Lkk%@lD5#e*dZodE9LC$q{(LQ{!hzc~~+ zrpk&M8Of%z+Pm3v;d7AzC3{95*3`(!gsLTSsSfpQI*}eeEsu!@iOGHsm>D7S(+6SI!odrG2}nMu%v&c&*?Yyf-$B~>eI%! zkCe-&7xddc{T7C9>ochTsLvWdGd}X6Ijc_^Kh>x4E$DZA#(90t7&FHC26I?(S13cl|^L3;-JBva@xT}fFTjt509T%{f?DTEI?>hLio z8SKOp&|TG(xT3@}i4j?)+wS)Hlua~g(J3i{AG0deJ9TnVlNo#m}ZST?1quHdK4r?hbEr)z|b6Aln2Es`M zCH6A3lg@lv=8`= zg9TDZ2o{@67#EF?aRzWMXtl~Aa{vdSasRRmsaKy zr&1ZN)zll3EI&Zn0)SsEATp_OGA2?m`0HBt-o_f;Joi$&FO~78&srB2G(YL~P>%&_hdcW}!N>wDUABbJO^Uu7s=1V8R{WnQSU!b4>@+0L|zB0+4u~ zu7#!kb+MKL@XGg5bqg=ZOIOpOLBV#(h?LLCQ7avJRIF_AXzH}0smGQe52O7^N+_?e z;;0u?(7{6|PgPVp#XnXKpyuvX50V!GcWD5{%hc zl`8USX6VIoT0fgh>uu~w(n6ZpywJ4u(?gdJJ!}dYO`#iu^G&Z_cw?ct{YLNa`#v-qQ`=eh(3N63u`mF0l{>wn;7ZJlB)je&`oY26h zq^3_cv$~c?p6%D`i=N2juA=7~k398ygMZ!=SP}*AD^EqS&b`D$m;2xU+{yxjf=V~} zZA;I7XMeE4tRK#%)5t;b235m}6T>`DHJv<#C&8I+ZIm8`{Qjns-njgEVAhlpCys>q za`_b(#@jGV?*;zWRXJv6YsEOou$-3ZQLHLt#FkAHET1zj2n#t+_eeIGPs@iC(o1qI o>Mm5cl^`s#U5l(`k!@RKoBzSKF0zA*Z2wa?UVG2B*s1S-0p#zMKmY&$ diff --git a/fastapi_sqlalchemy_toolkit/__pycache__/utils.cpython-314.pyc b/fastapi_sqlalchemy_toolkit/__pycache__/utils.cpython-314.pyc deleted file mode 100644 index f5c8a9752b5a7521018c71dc4c34f774bba8d682..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3367 zcmb7GU2GKB6~6PgyPntsR1ZHh>Y>L2;J}teVQsXz`AljrWf` zvqWyyXd$F63XP3ai98S`ig-v~B4R;tsEvtKsjoA^bjkEDQTmcMI}VDXKD6iD*;(7j zqUx3A-gD2-y?4$z-??{Zb5thK{_{Ki67m3CVk^#=*wjs?A z@&ke;42YIEAX(BtzzQ(FFc`FgAQuKhRtQFMuxUWHxD(8l>hEX0Zl_yluZ8}gamaL3rWL@Dval#G#q-^C%#Z2A}9-k;1=?onL z3+Y(JpcAfqFk>3em9j?uI5JO`D#h$5ZeKqFL8l4uEx!j{m88jw{O)svWP$f6ImW%j zY1}YKyq2o}v_Ho&C9a-%w8jtn!63sSU;h8Ev(`aL9$X@Pg=cW3v7C|5x&dlf6!ZfIvER^yk zYrJ$iaXOizrI(FS%S?`COe<5)Jz<{8XY!*j8HI@_tWqgIp0j+Bz9Q0nQN_yT%|v;^ zjp%x&9xGj^8)3UJeC8hLs^nV|;yRYF`&+nm@|p9`)WTg(xNA`W&c7u2LhtWGmtq%V zwYciURXeQSmAaQCkS&8XVIf2U8-|aC&<3JPhW*e&F#GCsnM;%Pf;ZdTCx)3=hyFwnG;&2FOYvu1x$b-bhsx6^vV@}XiirudL!oWRXwwUsaYJV* zqnGMHqK|=yh3Xg(Rq{=Mgj=VcdLvow|2)3&QuJc<%44;T{Z7aJh4}vS;%(#4g-;5L z0+B-Jj)Lx{_P2~$d%|f?+}#K!=Z5Y@Vv|GXhu-S9!-_2`6bs545TYko1Q82My*SmbR?JQ=47TVeU3_ zfhFuNXRqwJ+n!iN`dZ(te7Cos@mKy43|PZ`;UC52e*ghl>T#i8FE61^V#bOMr3Mip zKjeEkjZZ_x290&HHVLv+qv$jYNeUzSK-Ha=3yM8s-%#v%`?^yF4(P)wb0Gh?NA`4>tvwA3 zUPrxKU^TgF342BX{p-O0$eDy4uR3SFmF+p_4cKWWA=~eu6>x9h$_gxtOE_no*MT>y zqz)YKA5hTLo`*x+Vn?4&$Zjx|p;itqn~<)bgu6x8U0K&5oho^t)gX<*pSzJ!YQPPJ ze1oYLGX)S+OdmJGxE|(ASISvN!DNtA1-cCtWDkyL+uTj3#gw`Y^1-@Gj!Q8~`{3NpZfZAMn0Z998i0B#nCX3ev9MQvb5T4(F)8`=S zAA3RkgBO!k3E-TCv_LhM5s(~SWF~?Cn)4=HYT>M5j?x^fU~6g%zCT!Pfnp9mdH4md z@vGk}ny^wyG;!swSgV!9mx@%cE2r?)F$5PbR21E-FM=zW7NrVo?~3`HX+5mSBtXkl z*-7z&jN=6vzihsH{OwT6$%L4Hz zcp%ldu{=KZ8t3C1Y5`y26Ico04m;J+K~}Io#2lO0^=-F^7jFQNZkUptE^-^kR*U;KyXi^ zK0sE?Kvp4^p-Oo8en_hLr(+R<)4d02H~+vEn1|E2_dHwrCRQ1*K(OSU^Ue=NWriJe zniVK0ak9(xu*aua&gU$EGlr8Up>xB`8~W)?zG8Sl_OIa9(Ndw1(M_YQpMp1(jwwQS zfep)rr+|P9CqhEed%O1D82@PecIU#b!*^mwoLz_C{pFN2HU5|QcA%Zu5j%9`vvBOY zuLTkx;7k!LFUNl%svBGppU&PRA^`uLEB>6KlsyR9TY(}AOSvz9g}~k~Wxr5pA0qet%0%-Rr1(7oz*B^8Elj`|fXo8+LIAX?f(H82oM5>(5@4 a?Crl=h`(x!uih8<7QPz&RtA8ueg6ygUXtwq diff --git a/tests/__pycache__/__init__.cpython-314.pyc b/tests/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 7e3369df76cc7f94bbe5708bc01e08c2393441ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 170 zcmdPq_I|p@<2{{|u76<*pxEoLW?@ zpPQePUy_|)rdOsPP?VpQnp{$>pO#o$l30+bTU?lvn3J55np>$`lAoWGomrw^l3EN@ s5FekJmst`YuUAlci^C>2KczG$)vkyYXfDX+Vi4mKGb1Bo5i^hl0OL+7>Hq)$ diff --git a/tests/__pycache__/test_partial_where.cpython-314-pytest-9.1.1.pyc b/tests/__pycache__/test_partial_where.cpython-314-pytest-9.1.1.pyc deleted file mode 100644 index 00c98f36d2d4016c142df30a83277ba7d452f937..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18715 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 From 82c41c24e600de14275336aac7d0ccef8ec1aa80 Mon Sep 17 00:00:00 2001 From: "molotkov.v" Date: Tue, 21 Jul 2026 19:19:49 +0300 Subject: [PATCH 3/4] fix: tests --- requirements/test.txt | 2 +- tests/conftest.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/requirements/test.txt b/requirements/test.txt index 78739f0..af6daef 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,2 +1,2 @@ -pytest>=8.0.0 +pytest>=9.0.0 asyncpg>=0.29.0 diff --git a/tests/conftest.py b/tests/conftest.py index 39221d3..470d567 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -from typing import AsyncGenerator +from collections.abc import AsyncGenerator import pytest from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession, AsyncTransaction @@ -8,24 +8,24 @@ @pytest.fixture(autouse=True) -async def create_metadata(): +async def create_metadata(anyio_backend: str) -> None: async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) await conn.run_sync(CustomPKBase.metadata.create_all) @pytest.fixture(scope="session") -def anyio_backend(): +def anyio_backend() -> str: return "asyncio" @pytest.fixture(scope="session") -async def connection(anyio_backend) -> AsyncGenerator[AsyncConnection, None]: +async def connection(anyio_backend: str) -> AsyncGenerator[AsyncConnection, None]: async with engine.connect() as connection: yield connection -@pytest.fixture() +@pytest.fixture async def transaction( connection: AsyncConnection, ) -> AsyncGenerator[AsyncTransaction, None]: @@ -39,7 +39,7 @@ async def persistent_session() -> AsyncGenerator[AsyncSession, None]: yield session -@pytest.fixture() +@pytest.fixture async def session( connection: AsyncConnection, transaction: AsyncTransaction ) -> AsyncGenerator[AsyncSession, None]: From 3a9ae0dcbff1f2aa91f5a8364368708c3f05c594 Mon Sep 17 00:00:00 2001 From: "molotkov.v" Date: Tue, 21 Jul 2026 19:41:22 +0300 Subject: [PATCH 4/4] fix: tests --- tests/conftest.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 470d567..1603d08 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,24 +7,34 @@ from tests.models import Base, CustomPKBase -@pytest.fixture(autouse=True) -async def create_metadata(anyio_backend: str) -> None: - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - await conn.run_sync(CustomPKBase.metadata.create_all) - - @pytest.fixture(scope="session") def anyio_backend() -> str: return "asyncio" @pytest.fixture(scope="session") -async def connection(anyio_backend: str) -> AsyncGenerator[AsyncConnection, None]: +async def connection( + anyio_backend: str, # noqa: ARG001 - activates AnyIO's async-fixture runner +) -> AsyncGenerator[AsyncConnection, None]: async with engine.connect() as connection: yield connection +@pytest.fixture(scope="session", autouse=True) +async def create_metadata( + connection: AsyncConnection, +) -> AsyncGenerator[None, None]: + await connection.run_sync(Base.metadata.create_all) + await connection.run_sync(CustomPKBase.metadata.create_all) + await connection.commit() + + yield + + await connection.run_sync(CustomPKBase.metadata.drop_all) + await connection.run_sync(Base.metadata.drop_all) + await connection.commit() + + @pytest.fixture async def transaction( connection: AsyncConnection,