sera-2 1.24.1__tar.gz → 1.26.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sera_2-1.24.1 → sera_2-1.26.0}/PKG-INFO +4 -3
- {sera_2-1.24.1 → sera_2-1.26.0}/pyproject.toml +2 -2
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/base_service.py +85 -21
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/search_helper.py +4 -2
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_python_model.py +70 -3
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_typescript_model.py +6 -45
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/ts_frontend/make_enums.py +1 -20
- sera_2-1.26.0/sera/make/ts_frontend/make_query.py +146 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/ts_frontend/misc.py +2 -10
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/__init__.py +8 -1
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_class.py +2 -1
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_collection.py +5 -1
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_module.py +0 -1
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_parse.py +10 -1
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_property.py +8 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/typing.py +1 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/README.md +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/__init__.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/constants.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/exports/__init__.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/exports/schema.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/exports/test.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/__init__.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/api_helper.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/api_test_helper.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/base_orm.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/__init__.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_dcg.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_edge.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_flow.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_fn_signature.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_node.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_runtime.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_type_conversion.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/middlewares/__init__.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/middlewares/auth.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/middlewares/uscp.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/__init__.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/__main__.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_app.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_python_api.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_python_services.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/ts_frontend/__init__.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/ts_frontend/make_class_schema.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/misc/__init__.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/misc/_formatter.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/misc/_utils.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_constraints.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_datatype.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_default.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_enum.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_multi_lingual_string.py +0 -0
- {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_schema.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: sera-2
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.26.0
|
4
4
|
Summary:
|
5
5
|
Author: Binh Vu
|
6
6
|
Author-email: bvu687@gmail.com
|
@@ -8,8 +8,9 @@ Requires-Python: >=3.12,<4.0
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Programming Language :: Python :: 3.12
|
10
10
|
Classifier: Programming Language :: Python :: 3.13
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
11
12
|
Requires-Dist: black (==25.1.0)
|
12
|
-
Requires-Dist: codegen-2 (>=2.
|
13
|
+
Requires-Dist: codegen-2 (>=2.15.1,<3.0.0)
|
13
14
|
Requires-Dist: graph-wrapper (>=1.7.3,<2.0.0)
|
14
15
|
Requires-Dist: isort (==6.0.1)
|
15
16
|
Requires-Dist: litestar (>=2.15.1,<3.0.0)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "sera-2"
|
3
|
-
version = "1.
|
3
|
+
version = "1.26.0"
|
4
4
|
description = ""
|
5
5
|
authors = ["Binh Vu <bvu687@gmail.com>"]
|
6
6
|
readme = "README.md"
|
@@ -9,7 +9,7 @@ repository = "https://github.com/binh-vu/sera"
|
|
9
9
|
|
10
10
|
[tool.poetry.dependencies]
|
11
11
|
python = "^3.12"
|
12
|
-
codegen-2 = "^2.
|
12
|
+
codegen-2 = "^2.15.1"
|
13
13
|
msgspec = "^0.19.0"
|
14
14
|
litestar = "^2.15.1"
|
15
15
|
loguru = "^0.7.0"
|
@@ -10,8 +10,8 @@ from sqlalchemy.orm import contains_eager, load_only
|
|
10
10
|
|
11
11
|
from sera.libs.base_orm import BaseORM
|
12
12
|
from sera.libs.search_helper import Query, QueryOp
|
13
|
-
from sera.misc import assert_not_null, to_snake_case
|
14
|
-
from sera.models import Cardinality, Class, DataProperty, ObjectProperty
|
13
|
+
from sera.misc import assert_isinstance, assert_not_null, to_snake_case
|
14
|
+
from sera.models import Cardinality, Class, DataProperty, IndexType, ObjectProperty
|
15
15
|
|
16
16
|
R = TypeVar("R", bound=BaseORM)
|
17
17
|
ID = TypeVar("ID") # ID of a class
|
@@ -20,6 +20,7 @@ SqlResult = TypeVar("SqlResult", bound=Result)
|
|
20
20
|
|
21
21
|
class QueryResult(NamedTuple, Generic[R]):
|
22
22
|
records: Sequence[R]
|
23
|
+
extra_columns: Mapping[str, Sequence]
|
23
24
|
total: Optional[int]
|
24
25
|
|
25
26
|
|
@@ -36,6 +37,7 @@ class BaseAsyncService(Generic[ID, R]):
|
|
36
37
|
self._cls_id_prop = getattr(self.orm_cls, self.id_prop.name)
|
37
38
|
self.is_id_auto_increment = assert_not_null(self.id_prop.db).is_auto_increment
|
38
39
|
|
40
|
+
# mapping from property name to ORM class for object properties
|
39
41
|
self.prop2orm: dict[str, type] = {
|
40
42
|
prop.name: orm_classes[prop.target.name]
|
41
43
|
for prop in cls.properties.values()
|
@@ -103,15 +105,15 @@ class BaseAsyncService(Generic[ID, R]):
|
|
103
105
|
# A -> B is either 1:1 or N:1, we will store the foreign key is in A
|
104
106
|
# .join(B, A.<foreign_key> == B.id)
|
105
107
|
self.join_clauses[prop.name] = [
|
106
|
-
{
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
},
|
108
|
+
# {
|
109
|
+
# "class": target_tbl,
|
110
|
+
# "condition": getattr(
|
111
|
+
# target_tbl,
|
112
|
+
# assert_not_null(target_cls.get_id_property()).name,
|
113
|
+
# )
|
114
|
+
# == getattr(self.orm_cls, source_fk),
|
115
|
+
# "contains_eager": getattr(self.orm_cls, source_relprop),
|
116
|
+
# },
|
115
117
|
]
|
116
118
|
|
117
119
|
@classmethod
|
@@ -135,6 +137,7 @@ class BaseAsyncService(Generic[ID, R]):
|
|
135
137
|
session: The database session
|
136
138
|
"""
|
137
139
|
q = self._select()
|
140
|
+
extra_cols = []
|
138
141
|
|
139
142
|
if len(query.fields) > 0:
|
140
143
|
q = q.options(
|
@@ -195,12 +198,60 @@ class BaseAsyncService(Generic[ID, R]):
|
|
195
198
|
q = q.where(~getattr(self.orm_cls, clause.field).in_(clause.value))
|
196
199
|
else:
|
197
200
|
assert clause.op == QueryOp.fuzzy
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
clause.value
|
202
|
-
)
|
201
|
+
clause_prop = self.cls.properties[clause.field]
|
202
|
+
assert (
|
203
|
+
isinstance(clause_prop, DataProperty) and clause_prop.db is not None
|
203
204
|
)
|
205
|
+
clause_orm_field = getattr(self.orm_cls, clause.field)
|
206
|
+
extra_cols.append(f"{clause.field}_score")
|
207
|
+
|
208
|
+
if clause_prop.db.index_type == IndexType.POSTGRES_FTS_SEVI:
|
209
|
+
# fuzzy search is implemented using Postgres Full-Text Search
|
210
|
+
# sevi is a custom text search configuration that we defined in `configs/postgres-fts.sql`
|
211
|
+
q = q.where(
|
212
|
+
func.to_tsvector("sevi", clause_orm_field).bool_op("@@")(
|
213
|
+
func.plainto_tsquery("sevi", clause.value)
|
214
|
+
)
|
215
|
+
)
|
216
|
+
# TODO: figure out which rank function is better
|
217
|
+
# https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING
|
218
|
+
q = q.order_by(
|
219
|
+
func.ts_rank_cd(
|
220
|
+
func.to_tsvector("sevi", clause_orm_field),
|
221
|
+
func.plainto_tsquery("sevi", clause.value),
|
222
|
+
).desc()
|
223
|
+
)
|
224
|
+
q = q.add_columns(
|
225
|
+
func.ts_rank_cd(
|
226
|
+
func.to_tsvector("sevi", clause_orm_field),
|
227
|
+
func.plainto_tsquery("sevi", clause.value),
|
228
|
+
).label(f"{clause.field}_score")
|
229
|
+
)
|
230
|
+
elif clause_prop.db.index_type == IndexType.POSTGRES_TRIGRAM:
|
231
|
+
# fuzzy search is implemented using Postgres trigram index
|
232
|
+
# using a custom function f_unaccent to ignore accents -- see `configs/postgres-fts.sql`
|
233
|
+
q = q.where(
|
234
|
+
func.f_unaccent(clause_orm_field).bool_op("%>")(
|
235
|
+
func.f_unaccent(clause.value)
|
236
|
+
)
|
237
|
+
)
|
238
|
+
q = q.order_by(
|
239
|
+
func.f_unaccent(clause_orm_field).op("<->>")(
|
240
|
+
func.f_unaccent(clause.value)
|
241
|
+
)
|
242
|
+
)
|
243
|
+
q = q.add_columns(
|
244
|
+
(
|
245
|
+
1
|
246
|
+
- func.f_unaccent(clause_orm_field).op("<->>")(
|
247
|
+
func.f_unaccent(clause.value)
|
248
|
+
)
|
249
|
+
).label(f"{clause.field}_score")
|
250
|
+
)
|
251
|
+
else:
|
252
|
+
raise NotImplementedError(
|
253
|
+
f"Fuzzy search is not implemented for index type {clause_prop.db.index_type}"
|
254
|
+
)
|
204
255
|
|
205
256
|
for join_condition in query.join_conditions:
|
206
257
|
for join_clause in self.join_clauses[join_condition.prop]:
|
@@ -211,16 +262,29 @@ class BaseAsyncService(Generic[ID, R]):
|
|
211
262
|
full=join_condition.join_type == "full",
|
212
263
|
).options(contains_eager(join_clause["contains_eager"]))
|
213
264
|
|
214
|
-
|
215
|
-
|
216
|
-
cq = select(func.count()).select_from(q.subquery())
|
265
|
+
# Create count query without order_by clause to improve performance
|
266
|
+
cq = select(func.count()).select_from(q.order_by(None).subquery())
|
217
267
|
rq = q.limit(query.limit).offset(query.offset)
|
218
|
-
|
268
|
+
|
269
|
+
if len(extra_cols) == 0:
|
270
|
+
records = self._process_result(await session.execute(rq)).scalars().all()
|
271
|
+
extra_columns = {}
|
272
|
+
else:
|
273
|
+
records = []
|
274
|
+
raw_extra_columns = [[] for col in extra_cols]
|
275
|
+
for row in self._process_result(await session.execute(rq)):
|
276
|
+
records.append(row[0])
|
277
|
+
for i in range(len(extra_cols)):
|
278
|
+
raw_extra_columns[i].append(row[i + 1])
|
279
|
+
extra_columns = {
|
280
|
+
col: raw_extra_columns[i] for i, col in enumerate(extra_cols)
|
281
|
+
}
|
282
|
+
|
219
283
|
if query.return_total:
|
220
284
|
total = (await session.execute(cq)).scalar_one()
|
221
285
|
else:
|
222
286
|
total = None
|
223
|
-
return QueryResult(records, total)
|
287
|
+
return QueryResult(records, extra_columns, total)
|
224
288
|
|
225
289
|
async def get_by_id(self, id: ID, session: AsyncSession) -> Optional[R]:
|
226
290
|
"""Retrieving a record by ID."""
|
@@ -253,9 +253,9 @@ class Query(msgspec.Struct):
|
|
253
253
|
else:
|
254
254
|
if join_clause.join_type != "inner":
|
255
255
|
output[target_name].extend(
|
256
|
-
deser_func(
|
256
|
+
deser_func(val)
|
257
257
|
for record in result.records
|
258
|
-
if val is not None
|
258
|
+
if (val := getattr(record, source_relprop)) is not None
|
259
259
|
)
|
260
260
|
else:
|
261
261
|
output[target_name].extend(
|
@@ -265,6 +265,8 @@ class Query(msgspec.Struct):
|
|
265
265
|
|
266
266
|
deser_func = dataschema[cls.name].from_db
|
267
267
|
output[cls.name] = [deser_func(record) for record in result.records]
|
268
|
+
# include extra columns such as fuzzy search scores
|
269
|
+
output.update(result.extra_columns)
|
268
270
|
|
269
271
|
return output
|
270
272
|
|
@@ -22,6 +22,7 @@ from sera.models import (
|
|
22
22
|
Cardinality,
|
23
23
|
Class,
|
24
24
|
DataProperty,
|
25
|
+
IndexType,
|
25
26
|
ObjectProperty,
|
26
27
|
Package,
|
27
28
|
PyTypeWithDep,
|
@@ -1207,14 +1208,79 @@ def make_python_relational_model(
|
|
1207
1208
|
)
|
1208
1209
|
|
1209
1210
|
index_stmts = []
|
1210
|
-
|
1211
|
+
|
1212
|
+
if len(cls.db.indices) > 0 or any(
|
1213
|
+
isinstance(prop, DataProperty)
|
1214
|
+
and prop.db is not None
|
1215
|
+
and prop.db.is_indexed
|
1216
|
+
and (
|
1217
|
+
prop.db.index_type == IndexType.POSTGRES_FTS_SEVI
|
1218
|
+
or prop.db.index_type == IndexType.POSTGRES_TRIGRAM
|
1219
|
+
)
|
1220
|
+
for prop in cls.properties.values()
|
1221
|
+
):
|
1211
1222
|
program.import_("sqlalchemy.Index", True)
|
1223
|
+
|
1224
|
+
fts_index = []
|
1225
|
+
for prop in cls.properties.values():
|
1226
|
+
if (
|
1227
|
+
not isinstance(prop, DataProperty)
|
1228
|
+
or prop.db is None
|
1229
|
+
or not prop.db.is_indexed
|
1230
|
+
):
|
1231
|
+
continue
|
1232
|
+
if prop.db.index_type == IndexType.POSTGRES_FTS_SEVI:
|
1233
|
+
fts_index.append(
|
1234
|
+
expr.ExprFuncCall(
|
1235
|
+
expr.ExprIdent("Index"),
|
1236
|
+
[
|
1237
|
+
expr.ExprConstant(
|
1238
|
+
f"ix_{cls.db.table_name}_{get_property_name(prop)}_gin"
|
1239
|
+
),
|
1240
|
+
expr.ExprFuncCall(
|
1241
|
+
ident_manager.use("text"),
|
1242
|
+
[
|
1243
|
+
expr.ExprConstant(
|
1244
|
+
f"to_tsvector('sevi', {get_property_name(prop)})"
|
1245
|
+
)
|
1246
|
+
],
|
1247
|
+
),
|
1248
|
+
PredefinedFn.keyword_assignment(
|
1249
|
+
"postgresql_using", expr.ExprConstant("gin")
|
1250
|
+
),
|
1251
|
+
],
|
1252
|
+
)
|
1253
|
+
)
|
1254
|
+
if prop.db.index_type == IndexType.POSTGRES_TRIGRAM:
|
1255
|
+
fts_index.append(
|
1256
|
+
expr.ExprFuncCall(
|
1257
|
+
expr.ExprIdent("Index"),
|
1258
|
+
[
|
1259
|
+
expr.ExprConstant(
|
1260
|
+
f"ix_{cls.db.table_name}_{get_property_name(prop)}_gist"
|
1261
|
+
),
|
1262
|
+
expr.ExprFuncCall(
|
1263
|
+
expr.ExprIdent("text"),
|
1264
|
+
[
|
1265
|
+
expr.ExprConstant(
|
1266
|
+
f"f_unaccent({get_property_name(prop)}) gist_trgm_ops(siglen=256)"
|
1267
|
+
)
|
1268
|
+
],
|
1269
|
+
),
|
1270
|
+
PredefinedFn.keyword_assignment(
|
1271
|
+
"postgresql_using", expr.ExprConstant("gist")
|
1272
|
+
),
|
1273
|
+
],
|
1274
|
+
)
|
1275
|
+
)
|
1276
|
+
|
1212
1277
|
index_stmts.append(
|
1213
1278
|
stmt.DefClassVarStatement(
|
1214
1279
|
"_table_args__",
|
1215
1280
|
None,
|
1216
1281
|
PredefinedFn.tuple(
|
1217
|
-
|
1282
|
+
fts_index
|
1283
|
+
+ [
|
1218
1284
|
expr.ExprFuncCall(
|
1219
1285
|
expr.ExprIdent("Index"),
|
1220
1286
|
[expr.ExprConstant(index.name)]
|
@@ -1317,7 +1383,8 @@ def make_python_relational_model(
|
|
1317
1383
|
"unique", expr.ExprConstant(True)
|
1318
1384
|
)
|
1319
1385
|
)
|
1320
|
-
elif prop.db.is_indexed:
|
1386
|
+
elif prop.db.is_indexed and prop.db.index_type == IndexType.DEFAULT:
|
1387
|
+
# only add index=True for default index type
|
1321
1388
|
propvalargs.append(
|
1322
1389
|
PredefinedFn.keyword_assignment(
|
1323
1390
|
"index", expr.ExprConstant(True)
|
@@ -8,6 +8,7 @@ from codegen.models.var import DeferredVar
|
|
8
8
|
from loguru import logger
|
9
9
|
|
10
10
|
from sera.make.ts_frontend.make_class_schema import make_class_schema
|
11
|
+
from sera.make.ts_frontend.make_query import make_query
|
11
12
|
from sera.make.ts_frontend.misc import TS_GLOBAL_IDENTS, get_normalizer
|
12
13
|
from sera.misc import (
|
13
14
|
assert_isinstance,
|
@@ -1144,50 +1145,6 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1144
1145
|
|
1145
1146
|
outmod.write(program)
|
1146
1147
|
|
1147
|
-
def make_query_processor(cls: Class, pkg: Package):
|
1148
|
-
if not cls.is_public:
|
1149
|
-
# skip classes that are not public
|
1150
|
-
return
|
1151
|
-
|
1152
|
-
outmod = pkg.module(cls.name + "Query")
|
1153
|
-
|
1154
|
-
program = Program()
|
1155
|
-
program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}", True)
|
1156
|
-
program.import_(f"sera-db.QueryProcessor", True)
|
1157
|
-
|
1158
|
-
query_args = []
|
1159
|
-
for prop in cls.properties.values():
|
1160
|
-
pypropname = prop.name
|
1161
|
-
tspropname = to_camel_case(prop.name)
|
1162
|
-
|
1163
|
-
if isinstance(prop, ObjectProperty) and prop.target.db is not None:
|
1164
|
-
tspropname = tspropname + "Id"
|
1165
|
-
pypropname = prop.name + "_id"
|
1166
|
-
|
1167
|
-
if tspropname != pypropname:
|
1168
|
-
query_args.append(
|
1169
|
-
(
|
1170
|
-
expr.ExprIdent(tspropname),
|
1171
|
-
expr.ExprConstant(pypropname),
|
1172
|
-
)
|
1173
|
-
)
|
1174
|
-
|
1175
|
-
program.root(
|
1176
|
-
stmt.LineBreak(),
|
1177
|
-
stmt.TypescriptStatement(
|
1178
|
-
f"export const query = "
|
1179
|
-
+ expr.ExprNewInstance(
|
1180
|
-
expr.ExprIdent(f"QueryProcessor<{cls.name}>"),
|
1181
|
-
[
|
1182
|
-
PredefinedFn.dict(query_args),
|
1183
|
-
],
|
1184
|
-
).to_typescript()
|
1185
|
-
+ ";",
|
1186
|
-
),
|
1187
|
-
)
|
1188
|
-
|
1189
|
-
outmod.write(program)
|
1190
|
-
|
1191
1148
|
def make_index(pkg: Package):
|
1192
1149
|
outmod = pkg.module("index")
|
1193
1150
|
if outmod.exists():
|
@@ -1209,7 +1166,11 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1209
1166
|
program.import_(
|
1210
1167
|
f"@.models.{pkg.dir.name}.{cls.name}Schema.{cls.name}Schema", True
|
1211
1168
|
)
|
1169
|
+
program.import_(
|
1170
|
+
f"@.models.{pkg.dir.name}.{cls.name}Query.{cls.name}Query", True
|
1171
|
+
)
|
1212
1172
|
export_types.append(f"{cls.name}Schema")
|
1173
|
+
export_iso_types.append(f"{cls.name}Query")
|
1213
1174
|
program.import_(
|
1214
1175
|
f"@.models.{pkg.dir.name}.{cls.name}Schema.{cls.name}SchemaType", True
|
1215
1176
|
)
|
@@ -1241,7 +1202,7 @@ def make_typescript_data_model(schema: Schema, target_pkg: Package):
|
|
1241
1202
|
pkg = target_pkg.pkg(cls.get_tsmodule_name())
|
1242
1203
|
make_normal(cls, pkg)
|
1243
1204
|
make_draft(cls, pkg)
|
1244
|
-
|
1205
|
+
make_query(schema, cls, pkg)
|
1245
1206
|
make_table(cls, pkg)
|
1246
1207
|
make_class_schema(schema, cls, pkg)
|
1247
1208
|
|
@@ -1,32 +1,13 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import re
|
4
|
-
from typing import Any, Callable
|
5
3
|
|
6
|
-
from codegen.models import
|
7
|
-
from codegen.models.var import DeferredVar
|
8
|
-
from loguru import logger
|
4
|
+
from codegen.models import PredefinedFn, Program, expr, stmt
|
9
5
|
|
10
|
-
from sera.make.ts_frontend.make_class_schema import make_class_schema
|
11
|
-
from sera.make.ts_frontend.misc import TS_GLOBAL_IDENTS, get_normalizer
|
12
|
-
from sera.misc import (
|
13
|
-
assert_isinstance,
|
14
|
-
assert_not_null,
|
15
|
-
identity,
|
16
|
-
to_camel_case,
|
17
|
-
to_pascal_case,
|
18
|
-
to_snake_case,
|
19
|
-
)
|
20
6
|
from sera.models import (
|
21
|
-
Class,
|
22
|
-
DataProperty,
|
23
7
|
Enum,
|
24
|
-
ObjectProperty,
|
25
8
|
Package,
|
26
9
|
Schema,
|
27
|
-
TsTypeWithDep,
|
28
10
|
)
|
29
|
-
from sera.typing import is_set
|
30
11
|
|
31
12
|
|
32
13
|
def make_typescript_enums(schema: Schema, target_pkg: Package):
|
@@ -0,0 +1,146 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from codegen.models import AST, ImportHelper, PredefinedFn, Program, expr, stmt
|
4
|
+
from codegen.models.var import DeferredVar
|
5
|
+
from loguru import logger
|
6
|
+
|
7
|
+
from sera.misc import (
|
8
|
+
assert_isinstance,
|
9
|
+
assert_not_null,
|
10
|
+
identity,
|
11
|
+
to_camel_case,
|
12
|
+
to_pascal_case,
|
13
|
+
to_snake_case,
|
14
|
+
)
|
15
|
+
from sera.models import (
|
16
|
+
Class,
|
17
|
+
DataProperty,
|
18
|
+
Enum,
|
19
|
+
ObjectProperty,
|
20
|
+
Package,
|
21
|
+
Schema,
|
22
|
+
TsTypeWithDep,
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
def make_query(schema: Schema, cls: Class, pkg: Package):
|
27
|
+
"""Make query processor and query schema.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
schema: The overall schema of the application, which contains all classes & enums
|
31
|
+
cls: The class that we want to generate the query processor and schema
|
32
|
+
pkg: The output package (directory) for the class in the `@.models` package. For example, if the
|
33
|
+
class is `User`, then the package would be `src/models/user`.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
This function do not return anything as it writes the query helper directly to a file.
|
37
|
+
"""
|
38
|
+
if not cls.is_public:
|
39
|
+
# skip classes that are not public
|
40
|
+
return
|
41
|
+
|
42
|
+
outmod = pkg.module(cls.name + "Query")
|
43
|
+
|
44
|
+
program = Program()
|
45
|
+
program.import_(f"@.models.{pkg.dir.name}.{cls.name}.{cls.name}", True)
|
46
|
+
program.import_(f"sera-db.QueryProcessor", True)
|
47
|
+
program.import_(f"sera-db.Query", True)
|
48
|
+
|
49
|
+
query_args = []
|
50
|
+
for prop in cls.properties.values():
|
51
|
+
pypropname = prop.name
|
52
|
+
tspropname = to_camel_case(prop.name)
|
53
|
+
|
54
|
+
if isinstance(prop, ObjectProperty) and prop.target.db is not None:
|
55
|
+
tspropname = tspropname + "Id"
|
56
|
+
pypropname = prop.name + "_id"
|
57
|
+
|
58
|
+
if tspropname != pypropname:
|
59
|
+
query_args.append(
|
60
|
+
(
|
61
|
+
expr.ExprIdent(tspropname),
|
62
|
+
expr.ExprConstant(pypropname),
|
63
|
+
)
|
64
|
+
)
|
65
|
+
|
66
|
+
query_condition_args = []
|
67
|
+
for prop in cls.properties.values():
|
68
|
+
if prop.db is None or prop.data.is_private:
|
69
|
+
# This property is not stored in the database or it's private, so we skip it
|
70
|
+
continue
|
71
|
+
if (
|
72
|
+
isinstance(prop, DataProperty)
|
73
|
+
and prop.db is not None
|
74
|
+
and not prop.db.is_indexed
|
75
|
+
):
|
76
|
+
# This property is not indexed, so we skip it
|
77
|
+
continue
|
78
|
+
if isinstance(prop, ObjectProperty) and prop.target.db is None:
|
79
|
+
# TODO: Implement this! This property is an embedded object property, we need to figure out
|
80
|
+
# which necessary properties are queryable and add them to the field names
|
81
|
+
continue
|
82
|
+
|
83
|
+
tspropname = to_camel_case(prop.name)
|
84
|
+
if isinstance(prop, ObjectProperty) and prop.target.db is not None:
|
85
|
+
# This property is an object property stored in the database, "Id" is added to the property name
|
86
|
+
tspropname = tspropname + "Id"
|
87
|
+
|
88
|
+
if isinstance(prop, DataProperty):
|
89
|
+
tstype = prop.datatype.get_typescript_type()
|
90
|
+
else:
|
91
|
+
assert isinstance(prop, ObjectProperty)
|
92
|
+
tstype = assert_not_null(
|
93
|
+
prop.target.get_id_property()
|
94
|
+
).datatype.get_typescript_type()
|
95
|
+
|
96
|
+
for dep in tstype.deps:
|
97
|
+
program.import_(dep, is_import_attr=True)
|
98
|
+
|
99
|
+
if tstype.type == "string":
|
100
|
+
op = '"fuzzy"'
|
101
|
+
elif tstype.type in "number" or tstype.type == "Date":
|
102
|
+
op = '"eq" | "ne" | "lt" | "lte" | "gt" | "gte"'
|
103
|
+
elif tstype.is_enum_type():
|
104
|
+
op = '"eq" | "ne"'
|
105
|
+
else:
|
106
|
+
raise NotImplementedError(tstype.type)
|
107
|
+
|
108
|
+
if tstype.type == "Date":
|
109
|
+
# for date type, we use timestamp (number) for comparison
|
110
|
+
value = "number"
|
111
|
+
else:
|
112
|
+
value = tstype.type
|
113
|
+
|
114
|
+
query_condition_args.append(
|
115
|
+
(
|
116
|
+
expr.ExprIdent(tspropname + "?"),
|
117
|
+
PredefinedFn.dict(
|
118
|
+
[
|
119
|
+
(expr.ExprIdent("op"), expr.ExprIdent(op)),
|
120
|
+
(expr.ExprIdent("value"), expr.ExprIdent(value)),
|
121
|
+
]
|
122
|
+
),
|
123
|
+
)
|
124
|
+
)
|
125
|
+
|
126
|
+
program.root(
|
127
|
+
stmt.LineBreak(),
|
128
|
+
stmt.TypescriptStatement(
|
129
|
+
f"export const query = "
|
130
|
+
+ expr.ExprNewInstance(
|
131
|
+
expr.ExprIdent(f"QueryProcessor<{cls.name}>"),
|
132
|
+
[
|
133
|
+
PredefinedFn.dict(query_args),
|
134
|
+
],
|
135
|
+
).to_typescript()
|
136
|
+
+ ";",
|
137
|
+
),
|
138
|
+
stmt.LineBreak(),
|
139
|
+
stmt.TypescriptStatement(
|
140
|
+
f"export type {cls.name}Query = Query<{cls.name}, "
|
141
|
+
+ PredefinedFn.dict(query_condition_args).to_typescript()
|
142
|
+
+ ">;"
|
143
|
+
),
|
144
|
+
)
|
145
|
+
|
146
|
+
outmod.write(program)
|
@@ -2,17 +2,9 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from typing import Optional
|
4
4
|
|
5
|
-
from codegen.models import
|
5
|
+
from codegen.models import ImportHelper, expr
|
6
6
|
|
7
|
-
from sera.models import
|
8
|
-
Class,
|
9
|
-
DataProperty,
|
10
|
-
Enum,
|
11
|
-
ObjectProperty,
|
12
|
-
Package,
|
13
|
-
Schema,
|
14
|
-
TsTypeWithDep,
|
15
|
-
)
|
7
|
+
from sera.models import TsTypeWithDep
|
16
8
|
|
17
9
|
TS_GLOBAL_IDENTS = {
|
18
10
|
"normalizers.normalizeNumber": "sera-db.normalizers",
|
@@ -5,7 +5,13 @@ from sera.models._enum import Enum
|
|
5
5
|
from sera.models._module import App, Module, Package
|
6
6
|
from sera.models._multi_lingual_string import MultiLingualString
|
7
7
|
from sera.models._parse import parse_schema
|
8
|
-
from sera.models._property import
|
8
|
+
from sera.models._property import (
|
9
|
+
Cardinality,
|
10
|
+
DataProperty,
|
11
|
+
IndexType,
|
12
|
+
ObjectProperty,
|
13
|
+
Property,
|
14
|
+
)
|
9
15
|
from sera.models._schema import Schema
|
10
16
|
|
11
17
|
__all__ = [
|
@@ -14,6 +20,7 @@ __all__ = [
|
|
14
20
|
"Property",
|
15
21
|
"DataProperty",
|
16
22
|
"ObjectProperty",
|
23
|
+
"IndexType",
|
17
24
|
"Class",
|
18
25
|
"Cardinality",
|
19
26
|
"DataType",
|
@@ -5,7 +5,7 @@ from typing import Optional
|
|
5
5
|
|
6
6
|
from sera.misc import to_kebab_case, to_snake_case
|
7
7
|
from sera.models._multi_lingual_string import MultiLingualString
|
8
|
-
from sera.models._property import DataProperty, ObjectProperty
|
8
|
+
from sera.models._property import DataProperty, IndexType, ObjectProperty
|
9
9
|
|
10
10
|
|
11
11
|
@dataclass(kw_only=True)
|
@@ -13,6 +13,7 @@ class Index:
|
|
13
13
|
name: str
|
14
14
|
columns: list[str]
|
15
15
|
unique: bool = False
|
16
|
+
index_type: IndexType = IndexType.DEFAULT
|
16
17
|
|
17
18
|
|
18
19
|
@dataclass(kw_only=True)
|
@@ -55,7 +55,11 @@ class DataCollection:
|
|
55
55
|
"""Get the fields of this collection that can be used in join queries."""
|
56
56
|
output = {}
|
57
57
|
for prop in self.cls.properties.values():
|
58
|
-
if
|
58
|
+
if (
|
59
|
+
isinstance(prop, DataProperty)
|
60
|
+
and prop.db is not None
|
61
|
+
and prop.db.foreign_key is not None
|
62
|
+
):
|
59
63
|
# This property is a foreign key, so we add it to the output
|
60
64
|
output[prop.name] = DataCollection(
|
61
65
|
prop.db.foreign_key
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import re
|
4
4
|
from copy import deepcopy
|
5
|
+
from operator import index
|
5
6
|
from pathlib import Path
|
6
7
|
from typing import Sequence
|
7
8
|
|
@@ -30,6 +31,7 @@ from sera.models._property import (
|
|
30
31
|
ForeignKeyOnDelete,
|
31
32
|
ForeignKeyOnUpdate,
|
32
33
|
GetSCPropValueFunc,
|
34
|
+
IndexType,
|
33
35
|
ObjectPropDBInfo,
|
34
36
|
ObjectProperty,
|
35
37
|
PropDataAttrs,
|
@@ -150,7 +152,7 @@ def _parse_property(
|
|
150
152
|
|
151
153
|
assert isinstance(prop, dict), prop
|
152
154
|
if "datatype" in prop:
|
153
|
-
|
155
|
+
return_prop = DataProperty(
|
154
156
|
name=prop_name,
|
155
157
|
label=_parse_multi_lingual_string(prop.get("label", prop_name)),
|
156
158
|
description=_parse_multi_lingual_string(prop.get("desc", "")),
|
@@ -164,6 +166,9 @@ def _parse_property(
|
|
164
166
|
is_indexed=db.get("is_indexed", False)
|
165
167
|
or db.get("is_unique", False)
|
166
168
|
or db.get("is_primary_key", False),
|
169
|
+
index_type=(
|
170
|
+
IndexType(db["index_type"]) if "index_type" in db else None
|
171
|
+
),
|
167
172
|
foreign_key=schema.classes.get(db.get("foreign_key")),
|
168
173
|
)
|
169
174
|
if "db" in prop
|
@@ -173,6 +178,10 @@ def _parse_property(
|
|
173
178
|
default_value=_parse_default_value(prop.get("default_value", None)),
|
174
179
|
default_factory=_parse_default_factory(prop.get("default_factory", None)),
|
175
180
|
)
|
181
|
+
if return_prop.db is not None and return_prop.db.is_indexed:
|
182
|
+
if return_prop.db.index_type is None:
|
183
|
+
return_prop.db.index_type = IndexType.DEFAULT
|
184
|
+
return return_prop
|
176
185
|
|
177
186
|
assert "target" in prop, prop
|
178
187
|
return ObjectProperty(
|
@@ -16,6 +16,12 @@ if TYPE_CHECKING:
|
|
16
16
|
from sera.models._class import Class
|
17
17
|
|
18
18
|
|
19
|
+
class IndexType(str, Enum):
|
20
|
+
DEFAULT = "default"
|
21
|
+
POSTGRES_FTS_SEVI = "postgres_fts_sevi"
|
22
|
+
POSTGRES_TRIGRAM = "postgres_trigram"
|
23
|
+
|
24
|
+
|
19
25
|
class ForeignKeyOnDelete(str, Enum):
|
20
26
|
CASCADE = "cascade"
|
21
27
|
SET_NULL = "set null"
|
@@ -150,6 +156,8 @@ class DataPropDBInfo:
|
|
150
156
|
is_unique: bool = False
|
151
157
|
# whether this property is indexed or not
|
152
158
|
is_indexed: bool = False
|
159
|
+
# type of the index if it is indexed --- if is_indexed is True, this must be not None
|
160
|
+
index_type: Optional[IndexType] = None
|
153
161
|
# this is used in conjunction with is_primary_key = True for the case of
|
154
162
|
# extending a table with frequently updated properties. The value for the `foreign_key`
|
155
163
|
# will be a target class. The cardinality is one-to-one, on target class deletion,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|