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.
Files changed (53) hide show
  1. {sera_2-1.24.1 → sera_2-1.26.0}/PKG-INFO +4 -3
  2. {sera_2-1.24.1 → sera_2-1.26.0}/pyproject.toml +2 -2
  3. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/base_service.py +85 -21
  4. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/search_helper.py +4 -2
  5. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_python_model.py +70 -3
  6. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_typescript_model.py +6 -45
  7. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/ts_frontend/make_enums.py +1 -20
  8. sera_2-1.26.0/sera/make/ts_frontend/make_query.py +146 -0
  9. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/ts_frontend/misc.py +2 -10
  10. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/__init__.py +8 -1
  11. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_class.py +2 -1
  12. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_collection.py +5 -1
  13. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_module.py +0 -1
  14. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_parse.py +10 -1
  15. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_property.py +8 -0
  16. {sera_2-1.24.1 → sera_2-1.26.0}/sera/typing.py +1 -0
  17. {sera_2-1.24.1 → sera_2-1.26.0}/README.md +0 -0
  18. {sera_2-1.24.1 → sera_2-1.26.0}/sera/__init__.py +0 -0
  19. {sera_2-1.24.1 → sera_2-1.26.0}/sera/constants.py +0 -0
  20. {sera_2-1.24.1 → sera_2-1.26.0}/sera/exports/__init__.py +0 -0
  21. {sera_2-1.24.1 → sera_2-1.26.0}/sera/exports/schema.py +0 -0
  22. {sera_2-1.24.1 → sera_2-1.26.0}/sera/exports/test.py +0 -0
  23. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/__init__.py +0 -0
  24. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/api_helper.py +0 -0
  25. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/api_test_helper.py +0 -0
  26. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/base_orm.py +0 -0
  27. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/__init__.py +0 -0
  28. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_dcg.py +0 -0
  29. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_edge.py +0 -0
  30. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_flow.py +0 -0
  31. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_fn_signature.py +0 -0
  32. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_node.py +0 -0
  33. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_runtime.py +0 -0
  34. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/directed_computing_graph/_type_conversion.py +0 -0
  35. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/middlewares/__init__.py +0 -0
  36. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/middlewares/auth.py +0 -0
  37. {sera_2-1.24.1 → sera_2-1.26.0}/sera/libs/middlewares/uscp.py +0 -0
  38. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/__init__.py +0 -0
  39. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/__main__.py +0 -0
  40. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_app.py +0 -0
  41. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_python_api.py +0 -0
  42. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/make_python_services.py +0 -0
  43. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/ts_frontend/__init__.py +0 -0
  44. {sera_2-1.24.1 → sera_2-1.26.0}/sera/make/ts_frontend/make_class_schema.py +0 -0
  45. {sera_2-1.24.1 → sera_2-1.26.0}/sera/misc/__init__.py +0 -0
  46. {sera_2-1.24.1 → sera_2-1.26.0}/sera/misc/_formatter.py +0 -0
  47. {sera_2-1.24.1 → sera_2-1.26.0}/sera/misc/_utils.py +0 -0
  48. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_constraints.py +0 -0
  49. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_datatype.py +0 -0
  50. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_default.py +0 -0
  51. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_enum.py +0 -0
  52. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_multi_lingual_string.py +0 -0
  53. {sera_2-1.24.1 → sera_2-1.26.0}/sera/models/_schema.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: sera-2
3
- Version: 1.24.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.14.0,<3.0.0)
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.24.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.14.0"
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
- "class": target_tbl,
108
- "condition": getattr(
109
- target_tbl,
110
- assert_not_null(target_cls.get_id_property()).name,
111
- )
112
- == getattr(self.orm_cls, source_fk),
113
- "contains_eager": getattr(self.orm_cls, source_relprop),
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
- # Assuming fuzzy search is implemented as a full-text search
199
- q = q.where(
200
- func.to_tsvector(getattr(self.orm_cls, clause.field)).match(
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
- print(">>>", join_clause)
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
- records = self._process_result(await session.execute(rq)).scalars().all()
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((val := getattr(record, source_relprop)))
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
- if len(cls.db.indices) > 0:
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
- make_query_processor(cls, pkg)
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 AST, ImportHelper, PredefinedFn, Program, expr, stmt
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 AST, ImportHelper, PredefinedFn, Program, expr, stmt
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 Cardinality, DataProperty, ObjectProperty, Property
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 isinstance(prop, DataProperty) and prop.db.foreign_key is not None:
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
@@ -183,4 +183,3 @@ class App:
183
183
  def name(self) -> str:
184
184
  """Get the name of the application"""
185
185
  return self.root.dir.name
186
- return self.root.dir.name
@@ -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
- return DataProperty(
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,
@@ -39,4 +39,5 @@ GLOBAL_IDENTS = {
39
39
  "UNSET": "sera.typing.UNSET",
40
40
  "ForeignKey": "sqlalchemy.ForeignKey",
41
41
  "Optional": "typing.Optional",
42
+ "text": "sqlalchemy.text",
42
43
  }
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