1+ import contextlib
2+ import logging
13from dataclasses import dataclass
2- from typing import Generic , TypeVar , Union , cast
4+ from typing import Any , Generic , TypeVar , Union , cast
5+
6+ import orjson as json
37
48# noinspection PyProtectedMember
59from pydantic .fields import FieldInfo
610from sqlalchemy import func
11+ from sqlalchemy .dialects .postgresql import JSONB as JSONB_SQLA
712from sqlalchemy .orm import InstrumentedAttribute
813from sqlalchemy .sql .expression import BinaryExpression , BooleanClauseList
914
15+ from fastapi_jsonapi .exceptions import InvalidFilters
16+
17+ log = logging .getLogger (__name__ )
18+
1019ColumnType = TypeVar ("ColumnType" )
1120ExpressionType = TypeVar ("ExpressionType" )
1221
@@ -29,6 +38,68 @@ class CustomFilterSQLA(CustomFilterSQL[InstrumentedAttribute, Union[BinaryExpres
2938 """Base class for custom SQLAlchemy filters"""
3039
3140
41+ def _get_pg_jsonb_contains_expression (
42+ model_column : InstrumentedAttribute ,
43+ value : Any ,
44+ ) -> BinaryExpression :
45+ with contextlib .suppress (ValueError ):
46+ value = json .loads (value )
47+
48+ return model_column .cast (JSONB_SQLA ).op ("@>" )(value )
49+
50+
51+ def _get_sqlite_json_contains_expression (
52+ model_column : InstrumentedAttribute ,
53+ value : Any ,
54+ ) -> BinaryExpression :
55+ with contextlib .suppress (ValueError ):
56+ value = json .loads (value )
57+
58+ return model_column .ilike (value )
59+
60+
61+ def _get_pg_jsonb_ilike_expression (
62+ model_column : InstrumentedAttribute ,
63+ value : list ,
64+ operator : str ,
65+ ) -> BinaryExpression :
66+ try :
67+ target_field , regex = value
68+ except ValueError :
69+ msg = f'The "value" field has to be list of two values for op `{ operator } `'
70+ raise InvalidFilters (msg )
71+
72+ if isinstance (regex , (list , dict )):
73+ return model_column [target_field ].cast (JSONB_SQLA ).op ("@>" )(regex )
74+ elif isinstance (regex , bool ):
75+ regex = f"{ regex } " .lower ()
76+ else :
77+ regex = f"{ regex } "
78+
79+ return model_column .op ("->>" )(target_field ).ilike (regex )
80+
81+
82+ def _get_sqlite_json_ilike_expression (
83+ model_column : InstrumentedAttribute ,
84+ value : list ,
85+ operator : str ,
86+ ) -> BinaryExpression :
87+ try :
88+ target_field , regex = value
89+ except ValueError :
90+ msg = f'The "value" field has to be list of two values for op `{ operator } `'
91+ raise InvalidFilters (msg )
92+
93+ if isinstance (regex , (list , dict )):
94+ regex = json .dumps (regex ).decode ()
95+ elif isinstance (regex , bool ):
96+ return model_column .op ("->>" )(target_field ).is_ (regex )
97+ else :
98+ regex = f"{ regex } "
99+
100+ return model_column .op ("->>" )(target_field ).ilike (regex )
101+
102+
32103class LowerEqualsFilterSQL (CustomFilterSQLA ):
33104 def get_expression (
34105 self ,
@@ -43,17 +114,76 @@ def get_expression(
43114 )
44115
45116
46- # TODO: tests coverage
47- class JSONBContainsFilterSQL (CustomFilterSQLA ):
117+ class PGJSONContainsFilterSQL (CustomFilterSQLA ):
48118 def get_expression (
49119 self ,
50120 schema_field : FieldInfo ,
51121 model_column : InstrumentedAttribute ,
52- value : str ,
122+ value : Any ,
123+ operator : str ,
124+ ) -> BinaryExpression :
125+ return _get_pg_jsonb_contains_expression (model_column , value )
126+
127+
128+ class PGJSONBContainsFilterSQL (CustomFilterSQLA ):
129+ def get_expression (
130+ self ,
131+ schema_field : FieldInfo ,
132+ model_column : InstrumentedAttribute ,
133+ value : Any ,
134+ operator : str ,
135+ ) -> BinaryExpression :
136+ return _get_pg_jsonb_contains_expression (model_column , value )
137+
138+
139+ class PGJSONIlikeFilterSQL (CustomFilterSQLA ):
140+ def get_expression (
141+ self ,
142+ schema_field : FieldInfo ,
143+ model_column : InstrumentedAttribute ,
144+ value : list [str ],
145+ operator : str ,
146+ ) -> BinaryExpression :
147+ return _get_pg_jsonb_ilike_expression (model_column , value , operator )
148+
149+
150+ class PGJSONBIlikeFilterSQL (CustomFilterSQLA ):
151+ def get_expression (
152+ self ,
153+ schema_field : FieldInfo ,
154+ model_column : InstrumentedAttribute ,
155+ value : list [str ],
156+ operator : str ,
157+ ) -> BinaryExpression :
158+ return _get_pg_jsonb_ilike_expression (model_column , value , operator )
159+
160+
161+ class SQLiteJSONContainsFilterSQL (CustomFilterSQLA ):
162+ def get_expression (
163+ self ,
164+ schema_field : FieldInfo ,
165+ model_column : InstrumentedAttribute ,
166+ value : Any ,
167+ operator : str ,
168+ ) -> BinaryExpression :
169+ return _get_sqlite_json_contains_expression (model_column , value )
170+
171+
172+ class SQLiteJSONIlikeFilterSQL (CustomFilterSQLA ):
173+ def get_expression (
174+ self ,
175+ schema_field : FieldInfo ,
176+ model_column : InstrumentedAttribute ,
177+ value : list [str ],
53178 operator : str ,
54179 ) -> BinaryExpression :
55- return model_column . op ( "@>" )( value )
180+ return _get_sqlite_json_ilike_expression ( model_column , value , operator )
56181
57182
58183sql_filter_lower_equals = LowerEqualsFilterSQL (op = "lower_equals" )
59- sql_filter_jsonb_contains = JSONBContainsFilterSQL (op = "jsonb_contains" )
184+ sql_filter_pg_json_contains = PGJSONContainsFilterSQL (op = "pg_json_contains" )
185+ sql_filter_pg_jsonb_contains = PGJSONBContainsFilterSQL (op = "pg_jsonb_contains" )
186+ sql_filter_pg_json_ilike = PGJSONIlikeFilterSQL (op = "pg_json_ilike" )
187+ sql_filter_pg_jsonb_ilike = PGJSONBIlikeFilterSQL (op = "pg_jsonb_ilike" )
188+ sql_filter_sqlite_json_contains = SQLiteJSONContainsFilterSQL (op = "sqlite_json_contains" )
189+ sql_filter_sqlite_json_ilike = SQLiteJSONIlikeFilterSQL (op = "sqlite_json_ilike" )
0 commit comments