python-corekit 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl

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 (64) hide show
  1. corekit/api/__init__.py +18 -3
  2. corekit/api/application.py +237 -0
  3. corekit/api/lifespan.py +210 -0
  4. corekit/api/middleware.py +93 -0
  5. corekit/api/routers.py +109 -1
  6. corekit/concurrency/worker.py +65 -65
  7. corekit/config/settings.py +3 -3
  8. corekit/connections/sql/__init__.py +31 -3
  9. corekit/connections/sql/connection.py +19 -0
  10. corekit/connections/sql/migration/__init__.py +5 -5
  11. corekit/connections/sql/migration/base.py +3 -3
  12. corekit/connections/sql/migration/operations.py +66 -42
  13. corekit/connections/sql/migration/registry.py +2 -2
  14. corekit/connections/sql/operations/__init__.py +24 -0
  15. corekit/connections/sql/operations/base.py +102 -0
  16. corekit/connections/sql/operations/statements.py +150 -0
  17. corekit/connections/sql/query.py +4 -62
  18. corekit/connections/sql/table.py +30 -4
  19. corekit/constants.py +45 -45
  20. corekit/crypto/constants.py +4 -4
  21. corekit/data/__init__.py +8 -0
  22. corekit/data/expressions/__init__.py +10 -2
  23. corekit/data/expressions/comparison.py +184 -104
  24. corekit/data/expressions/expression.py +103 -98
  25. corekit/data/expressions/operator.py +54 -0
  26. corekit/data/expressions/target.py +21 -0
  27. corekit/data/record.py +147 -147
  28. corekit/data/stats.py +159 -157
  29. corekit/decorators/__init__.py +2 -2
  30. corekit/decorators/exception_handling.py +2 -1
  31. corekit/etl/connection.py +44 -44
  32. corekit/events/websocket.py +3 -2
  33. corekit/exceptions/__init__.py +18 -0
  34. corekit/http/__init__.py +13 -0
  35. corekit/jobs/__init__.py +26 -0
  36. corekit/jobs/registry.py +87 -0
  37. corekit/jobs/runner.py +69 -0
  38. corekit/jobs/task.py +152 -0
  39. corekit/observability/__init__.py +5 -3
  40. corekit/observability/request_context.py +135 -0
  41. corekit/registry/__init__.py +11 -6
  42. corekit/registry/ordered.py +86 -0
  43. corekit/schemas/__init__.py +10 -0
  44. corekit/schemas/enum.py +49 -49
  45. corekit/schemas/models/arbitrary.py +11 -11
  46. corekit/schemas/pydantic/fields.py +35 -35
  47. corekit/schemas/types.py +40 -40
  48. corekit/serialization/__init__.py +22 -0
  49. corekit/serialization/serializer.py +1 -1
  50. corekit/utils/__init__.py +59 -5
  51. corekit/utils/coercion.py +118 -0
  52. corekit/utils/collections.py +115 -0
  53. corekit/utils/ids.py +61 -5
  54. corekit/utils/payload.py +100 -0
  55. corekit/utils/raise_exc.py +8 -8
  56. corekit/utils/text.py +56 -0
  57. corekit/utils/time.py +74 -21
  58. corekit/utils/validators.py +15 -15
  59. corekit/utils/void.py +8 -8
  60. {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/METADATA +105 -100
  61. {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/RECORD +64 -46
  62. {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/WHEEL +0 -0
  63. {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/licenses/LICENSE +0 -0
  64. {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Migration operations.
3
3
 
4
- Each Operation knows how to:
4
+ Each MigrationOperation knows how to:
5
5
  - render itself to SQL (for execution) OR execute Python logic
6
6
  - render a canonical string (for checksumming)
7
7
 
@@ -22,9 +22,16 @@ from abc import ABC, abstractmethod
22
22
  from dataclasses import dataclass
23
23
  from typing import TYPE_CHECKING, Any, Callable
24
24
 
25
+ from sqlalchemy.dialects import postgresql
26
+
27
+ from corekit.connections.sql.table import NamedTable
28
+
25
29
  if TYPE_CHECKING:
26
30
  from corekit.connections.sql.connection import SQLConnection
27
31
 
32
+ #: Renders a table's name the way the database expects to read it.
33
+ _PREPARER = postgresql.dialect().identifier_preparer
34
+
28
35
 
29
36
  # ---------------------------------------------------------------------------
30
37
  # Constants
@@ -151,7 +158,7 @@ class QueryBuilder:
151
158
  return self._query.strip()
152
159
 
153
160
 
154
- class Operation(ABC):
161
+ class MigrationOperation(ABC):
155
162
  """
156
163
  Base class for all operations.
157
164
  """
@@ -159,6 +166,21 @@ class Operation(ABC):
159
166
  def _builder(self) -> QueryBuilder:
160
167
  return QueryBuilder()
161
168
 
169
+ @property
170
+ def table_name(self) -> str:
171
+ """
172
+ The table this operation targets, spelled for SQL.
173
+
174
+ A model class is rendered by the dialect, which quotes a reserved word
175
+ such as ``user`` and leaves anything else bare. A plain string is used
176
+ as given, so a migration can name a table that has no model class --
177
+ one dropped or renamed since, or not yet defined.
178
+ """
179
+ table = getattr(self, "table", None)
180
+ if isinstance(table, str) or table is None:
181
+ return table
182
+ return _PREPARER.format_table(table.__table__)
183
+
162
184
  @abstractmethod
163
185
  def to_sql(self) -> str:
164
186
  """
@@ -193,7 +215,7 @@ class Operation(ABC):
193
215
 
194
216
 
195
217
  @dataclass
196
- class AddColumn(Operation):
218
+ class AddColumn(MigrationOperation):
197
219
  """
198
220
  Add a column to an existing table.
199
221
 
@@ -201,7 +223,7 @@ class AddColumn(Operation):
201
223
  not. Set it to False when targeting SQLite.
202
224
  """
203
225
 
204
- table: str
226
+ table: type[NamedTable] | str
205
227
  column: str
206
228
  dtype: str
207
229
  safe: bool = True
@@ -212,7 +234,7 @@ class AddColumn(Operation):
212
234
  """
213
235
  Example: ALTER TABLE <table_name> ADD COLUMN [IF NOT EXISTS] <column_name> <dtype>
214
236
  """
215
- builder = self._builder().alter(TABLE, self.table).add(COLUMN)
237
+ builder = self._builder().alter(TABLE, self.table_name).add(COLUMN)
216
238
  if self.safe:
217
239
  builder.if_not_exists()
218
240
 
@@ -227,29 +249,29 @@ class AddColumn(Operation):
227
249
 
228
250
  def canonical(self) -> str:
229
251
  return (
230
- f"AddColumn(table={self.table}, column={self.column}, dtype={self.dtype}, "
252
+ f"AddColumn(table={self.table_name}, column={self.column}, dtype={self.dtype}, "
231
253
  f"nullable={self.nullable}, default={self.default})"
232
254
  )
233
255
 
234
256
 
235
257
  @dataclass
236
- class DropColumn(Operation):
237
- table: str
258
+ class DropColumn(MigrationOperation):
259
+ table: type[NamedTable] | str
238
260
  column: str
239
261
 
240
262
  def to_sql(self) -> str:
241
263
  """
242
264
  Example: ALTER TABLE <table_name> DROP COLUMN [IF EXISTS] <column_name>
243
265
  """
244
- return self._builder().alter(TABLE, self.table).drop(COLUMN).if_exists().append(self.column).build()
266
+ return self._builder().alter(TABLE, self.table_name).drop(COLUMN).if_exists().append(self.column).build()
245
267
 
246
268
  def canonical(self) -> str:
247
- return f"DropColumn(table={self.table}, column={self.column})"
269
+ return f"DropColumn(table={self.table_name}, column={self.column})"
248
270
 
249
271
 
250
272
  @dataclass
251
- class AlterColumnType(Operation):
252
- table: str
273
+ class AlterColumnType(MigrationOperation):
274
+ table: type[NamedTable] | str
253
275
  column: str
254
276
  new_dtype: str
255
277
  using: str | None = None
@@ -258,21 +280,19 @@ class AlterColumnType(Operation):
258
280
  """
259
281
  Example: ALTER TABLE <table_name> ALTER COLUMN <column_name> TYPE <new_dtype> [USING <using>]
260
282
  """
261
- builder = self._builder().alter(TABLE, self.table).alter(COLUMN, self.column).type(self.new_dtype)
283
+ builder = self._builder().alter(TABLE, self.table_name).alter(COLUMN, self.column).type(self.new_dtype)
262
284
  if self.using:
263
285
  builder.using(self.using)
264
286
 
265
287
  return builder.build()
266
288
 
267
289
  def canonical(self) -> str:
268
- return (
269
- f"AlterColumnType(table={self.table}, column={self.column}, new_dtype={self.new_dtype}, using={self.using})"
270
- )
290
+ return f"AlterColumnType(table={self.table_name}, column={self.column}, new_dtype={self.new_dtype}, using={self.using})"
271
291
 
272
292
 
273
293
  @dataclass
274
- class SetColumnDefault(Operation):
275
- table: str
294
+ class SetColumnDefault(MigrationOperation):
295
+ table: type[NamedTable] | str
276
296
  column: str
277
297
  default: str
278
298
 
@@ -280,25 +300,27 @@ class SetColumnDefault(Operation):
280
300
  """
281
301
  Example: ALTER TABLE <table_name> ALTER COLUMN <column_name> SET DEFAULT <default>
282
302
  """
283
- return self._builder().alter(TABLE, self.table).alter(COLUMN, self.column).set_default(self.default).build()
303
+ return (
304
+ self._builder().alter(TABLE, self.table_name).alter(COLUMN, self.column).set_default(self.default).build()
305
+ )
284
306
 
285
307
  def canonical(self) -> str:
286
- return f"SetColumnDefault(table={self.table}, column={self.column}, default={self.default})"
308
+ return f"SetColumnDefault(table={self.table_name}, column={self.column}, default={self.default})"
287
309
 
288
310
 
289
311
  @dataclass
290
- class DropColumnDefault(Operation):
291
- table: str
312
+ class DropColumnDefault(MigrationOperation):
313
+ table: type[NamedTable] | str
292
314
  column: str
293
315
 
294
316
  def to_sql(self) -> str:
295
317
  """
296
318
  Example: ALTER TABLE <table_name> ALTER COLUMN <column_name> DROP DEFAULT
297
319
  """
298
- return self._builder().alter(TABLE, self.table).alter(COLUMN, self.column).drop_default().build()
320
+ return self._builder().alter(TABLE, self.table_name).alter(COLUMN, self.column).drop_default().build()
299
321
 
300
322
  def canonical(self) -> str:
301
- return f"DropColumnDefault(table={self.table}, column={self.column})"
323
+ return f"DropColumnDefault(table={self.table_name}, column={self.column})"
302
324
 
303
325
 
304
326
  # ---------------------------------------------------------------------------
@@ -307,23 +329,23 @@ class DropColumnDefault(Operation):
307
329
 
308
330
 
309
331
  @dataclass
310
- class CreateIndex(Operation):
332
+ class CreateIndex(MigrationOperation):
311
333
  index_name: str
312
- table: str
334
+ table: type[NamedTable] | str
313
335
  columns: list[str]
314
336
  unique: bool = False
315
337
 
316
338
  def to_sql(self) -> str:
317
339
  unique_clause = "UNIQUE " if self.unique else ""
318
340
  cols = ", ".join(self.columns)
319
- return f"CREATE {unique_clause}INDEX IF NOT EXISTS {self.index_name} ON {self.table} ({cols})"
341
+ return f"CREATE {unique_clause}INDEX IF NOT EXISTS {self.index_name} ON {self.table_name} ({cols})"
320
342
 
321
343
  def canonical(self) -> str:
322
- return f"CreateIndex(name={self.index_name}, table={self.table}, columns={self.columns}, unique={self.unique})"
344
+ return f"CreateIndex(name={self.index_name}, table={self.table_name}, columns={self.columns}, unique={self.unique})"
323
345
 
324
346
 
325
347
  @dataclass
326
- class DropIndex(Operation):
348
+ class DropIndex(MigrationOperation):
327
349
  index_name: str
328
350
 
329
351
  def to_sql(self) -> str:
@@ -339,20 +361,20 @@ class DropIndex(Operation):
339
361
 
340
362
 
341
363
  @dataclass
342
- class DropTable(Operation):
343
- table: str
364
+ class DropTable(MigrationOperation):
365
+ table: type[NamedTable] | str
344
366
  cascade: bool = False
345
367
 
346
368
  def to_sql(self) -> str:
347
369
  cascade_clause = " CASCADE" if self.cascade else ""
348
- return f"DROP TABLE IF EXISTS {self.table}{cascade_clause}"
370
+ return f"DROP TABLE IF EXISTS {self.table_name}{cascade_clause}"
349
371
 
350
372
  def canonical(self) -> str:
351
- return f"DropTable(table={self.table}, cascade={self.cascade})"
373
+ return f"DropTable(table={self.table_name}, cascade={self.cascade})"
352
374
 
353
375
 
354
376
  @dataclass
355
- class RenameTable(Operation):
377
+ class RenameTable(MigrationOperation):
356
378
  old_name: str
357
379
  new_name: str
358
380
 
@@ -364,16 +386,16 @@ class RenameTable(Operation):
364
386
 
365
387
 
366
388
  @dataclass
367
- class RenameColumn(Operation):
368
- table: str
389
+ class RenameColumn(MigrationOperation):
390
+ table: type[NamedTable] | str
369
391
  old_name: str
370
392
  new_name: str
371
393
 
372
394
  def to_sql(self) -> str:
373
- return f"ALTER TABLE {self.table} RENAME COLUMN {self.old_name} TO {self.new_name}"
395
+ return f"ALTER TABLE {self.table_name} RENAME COLUMN {self.old_name} TO {self.new_name}"
374
396
 
375
397
  def canonical(self) -> str:
376
- return f"RenameColumn(table={self.table}, old={self.old_name}, new={self.new_name})"
398
+ return f"RenameColumn(table={self.table_name}, old={self.old_name}, new={self.new_name})"
377
399
 
378
400
 
379
401
  # ---------------------------------------------------------------------------
@@ -382,7 +404,7 @@ class RenameColumn(Operation):
382
404
 
383
405
 
384
406
  @dataclass
385
- class DataOperation(Operation):
407
+ class DataMigrationOperation(MigrationOperation):
386
408
  """
387
409
  Executes a Python function for data migrations (seeding, transformations).
388
410
 
@@ -391,12 +413,12 @@ class DataOperation(Operation):
391
413
  invalidate the checksum, so choose a stable, descriptive name.
392
414
 
393
415
  Example:
394
- DataOperation(
416
+ DataMigrationOperation(
395
417
  name="seed_user_permissions_v1",
396
418
  func=seed_permissions_from_roles,
397
419
  )
398
420
 
399
- IMPORTANT: It is not appropriate to use DataOperation in place of other operations. This is
421
+ IMPORTANT: It is not appropriate to use DataMigrationOperation in place of other operations. This is
400
422
  reserved for the most complex operations that CANNOT be expressed otherwise.
401
423
  """
402
424
 
@@ -410,7 +432,9 @@ class DataOperation(Operation):
410
432
  self.func(conn)
411
433
 
412
434
  def to_sql(self) -> str:
413
- raise NotImplementedError("DataOperation does not produce SQL; use execute() instead")
435
+ raise NotImplementedError("DataMigrationOperation does not produce SQL; use execute() instead")
414
436
 
415
437
  def canonical(self) -> str:
438
+ # This spelling is part of every applied data migration's checksum, so it
439
+ # is fixed regardless of what this class is called.
416
440
  return f"DataOperation(name={self.name})"
@@ -20,7 +20,7 @@ from types import ModuleType
20
20
 
21
21
  from corekit.connections.sql.connection import SQLConnection
22
22
  from corekit.connections.sql.migration.base import Migration
23
- from corekit.connections.sql.migration.operations import DataOperation
23
+ from corekit.connections.sql.migration.operations import DataMigrationOperation
24
24
  from corekit.connections.sql.migration.table import SchemaMigration
25
25
  from corekit.observability.benchmarkable import Benchmarkable
26
26
 
@@ -120,7 +120,7 @@ class MigrationRegistry(Benchmarkable):
120
120
  self.info(f"Applying migration v{migration.version}: {migration.name}")
121
121
  for operation in migration.operations():
122
122
  self.info(f" {operation.canonical()}")
123
- if isinstance(operation, DataOperation):
123
+ if isinstance(operation, DataMigrationOperation):
124
124
  operation.execute(conn)
125
125
  else:
126
126
  conn.exec_ddl(operation.to_sql())
@@ -0,0 +1,24 @@
1
+ """
2
+ Statement primitives, from the shared base to the statements themselves.
3
+ """
4
+
5
+ from corekit.connections.sql.operations.base import (
6
+ Conditional,
7
+ DdlOperation,
8
+ DmlOperation,
9
+ DqlOperation,
10
+ Operation,
11
+ )
12
+ from corekit.connections.sql.operations.statements import Delete, Insert, Select, Update
13
+
14
+ __all__ = [
15
+ "Conditional",
16
+ "DdlOperation",
17
+ "Delete",
18
+ "DmlOperation",
19
+ "DqlOperation",
20
+ "Insert",
21
+ "Operation",
22
+ "Select",
23
+ "Update",
24
+ ]
@@ -0,0 +1,102 @@
1
+ """
2
+ Statement primitives: the shapes a database operation can take.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from corekit.connections.sql.table import NamedTable
11
+ from corekit.data.expressions import Expression
12
+
13
+ __all__ = ["Conditional", "DdlOperation", "DmlOperation", "DqlOperation", "Operation"]
14
+
15
+
16
+ class Operation(BaseModel, ABC):
17
+ """
18
+ A statement waiting to be executed.
19
+
20
+ An operation compiles itself and nothing more: it holds no session and
21
+ runs no SQL, so building one is free of side effects and a connection
22
+ stays the only thing that owns a transaction.
23
+ """
24
+
25
+ model_config = {"arbitrary_types_allowed": True}
26
+
27
+ table: type[NamedTable]
28
+
29
+ @abstractmethod
30
+ def build(self) -> Any:
31
+ """
32
+ Produce the statement to execute.
33
+ """
34
+
35
+ def __repr__(self) -> str:
36
+ return f"{type(self).__name__}({self.table.__name__})"
37
+
38
+
39
+ class Conditional(Operation, ABC):
40
+ """
41
+ An operation narrowed by conditions, e.g. everything but an insert.
42
+
43
+ Conditions accumulate and are combined with AND. Each is either a native
44
+ SQLModel clause, which names columns so an IDE checks them, or a corekit
45
+ expression, which names fields as strings and works against any backend.
46
+ """
47
+
48
+ conditions: list[Any] = Field(default_factory=list)
49
+
50
+ def where(self, condition: Any) -> "Conditional":
51
+ """
52
+ Narrow this operation. Repeated calls combine with AND.
53
+ """
54
+ self.conditions.append(condition)
55
+ return self
56
+
57
+ def _as_clause(self, condition: Any) -> Any:
58
+ """
59
+ Compile a corekit expression; pass a native SQLModel clause through.
60
+ """
61
+ if isinstance(condition, Expression):
62
+ return condition.to_sqlalchemy(self.table)
63
+ return condition
64
+
65
+ def _apply_conditions(self, statement: Any) -> Any:
66
+ """
67
+ Attach every condition to ``statement``.
68
+ """
69
+ for condition in self.conditions:
70
+ statement = statement.where(self._as_clause(condition))
71
+ return statement
72
+
73
+
74
+ class DqlOperation(Conditional, ABC):
75
+ """
76
+ A read. Returns rows and changes nothing.
77
+ """
78
+
79
+
80
+ class DmlOperation(Conditional, ABC):
81
+ """
82
+ A write against rows: insert, update or delete.
83
+ """
84
+
85
+
86
+ class DdlOperation(Operation, ABC):
87
+ """
88
+ A change to the schema itself.
89
+
90
+ Schema changes are checksummed so a migration that has already run can be
91
+ recognised, which reads and writes never need. That is what separates
92
+ this branch from the others.
93
+ """
94
+
95
+ @abstractmethod
96
+ def canonical(self) -> str:
97
+ """
98
+ A deterministic, human-readable rendering of this operation's intent.
99
+
100
+ It feeds a checksum, so it must change whenever the intent changes and
101
+ must not change for anything else -- formatting or field order included.
102
+ """
@@ -0,0 +1,150 @@
1
+ """
2
+ The statements an application runs: select, insert, update and delete.
3
+ """
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import Field
8
+ from sqlmodel import delete as _delete
9
+ from sqlmodel import desc as _desc
10
+ from sqlmodel import insert as _insert
11
+ from sqlmodel import select as _select
12
+ from sqlmodel import update as _update
13
+
14
+ from corekit.connections.sql.operations.base import DmlOperation, DqlOperation, Operation
15
+
16
+ __all__ = ["Delete", "Insert", "Select", "Update"]
17
+
18
+
19
+ class Select(DqlOperation):
20
+ """
21
+ Read rows, optionally ordered and capped.
22
+
23
+ Select(table=User).where(User.age >= 18).order_by(User.name).limit(10)
24
+
25
+ ``where`` also accepts a ``corekit.data`` expression, which is the same
26
+ predicate language ``Dataset`` filters with::
27
+
28
+ Select(table=User).where(Field("age") >= 18)
29
+ """
30
+
31
+ order_by_field: Any = None
32
+ order_by_desc: bool = False
33
+ limit_count: int = -1
34
+
35
+ def order_by(self, field: Any, desc: bool = False) -> "Select":
36
+ """
37
+ Order results by a field.
38
+ """
39
+ self.order_by_field = field
40
+ self.order_by_desc = desc
41
+ return self
42
+
43
+ def limit(self, limit: int) -> "Select":
44
+ """
45
+ Cap the number of rows returned. A non-positive value means no limit.
46
+ """
47
+ self.limit_count = limit
48
+ return self
49
+
50
+ def build(self) -> Any:
51
+ """
52
+ Produce the select statement.
53
+ """
54
+ statement = self._apply_conditions(_select(self.table))
55
+
56
+ if self.order_by_field is not None:
57
+ order_by_field = self.order_by_field
58
+ if self.order_by_desc:
59
+ order_by_field = _desc(order_by_field)
60
+ statement = statement.order_by(order_by_field)
61
+
62
+ if self.limit_count > 0:
63
+ statement = statement.limit(self.limit_count)
64
+
65
+ return statement
66
+
67
+
68
+ class Delete(DmlOperation):
69
+ """
70
+ Delete every row matching the conditions, in one statement.
71
+
72
+ Delete(table=Session).where(Session.left_at < cutoff)
73
+
74
+ Deleting without a condition would empty the table, so it is refused:
75
+ a missing ``where`` is far more often a mistake than an intent.
76
+ """
77
+
78
+ def build(self) -> Any:
79
+ """
80
+ Produce the delete statement.
81
+
82
+ Raises:
83
+ ValueError: If no condition was given.
84
+ """
85
+ if not self.conditions:
86
+ raise ValueError(f"Delete on {self.table.__name__} needs a condition; use truncate to empty a table")
87
+ return self._apply_conditions(_delete(self.table))
88
+
89
+
90
+ class Update(DmlOperation):
91
+ """
92
+ Set columns on every row matching the conditions, in one statement.
93
+
94
+ Update(table=User, values={"active": False}).where(User.age < 18)
95
+
96
+ Updating without a condition would rewrite the table, so it is refused.
97
+ """
98
+
99
+ values: dict[str, Any] = Field(default_factory=dict)
100
+
101
+ def set(self, **values: Any) -> "Update":
102
+ """
103
+ Columns to write, as keyword arguments.
104
+ """
105
+ self.values.update(values)
106
+ return self
107
+
108
+ def build(self) -> Any:
109
+ """
110
+ Produce the update statement.
111
+
112
+ Raises:
113
+ ValueError: If no condition or no value was given.
114
+ """
115
+ if not self.conditions:
116
+ raise ValueError(f"Update on {self.table.__name__} needs a condition")
117
+ if not self.values:
118
+ raise ValueError(f"Update on {self.table.__name__} needs values to set")
119
+ return self._apply_conditions(_update(self.table)).values(**self.values)
120
+
121
+
122
+ class Insert(Operation):
123
+ """
124
+ Add rows.
125
+
126
+ Insert(table=User, rows=[{"name": "Ada"}, {"name": "Bob"}])
127
+
128
+ Rows are dictionaries rather than instances, so this compiles to a single
129
+ statement. To add model instances, use ``SQLConnection.insert``.
130
+ """
131
+
132
+ rows: list[dict[str, Any]] = Field(default_factory=list)
133
+
134
+ def add(self, **values: Any) -> "Insert":
135
+ """
136
+ Append one row.
137
+ """
138
+ self.rows.append(values)
139
+ return self
140
+
141
+ def build(self) -> Any:
142
+ """
143
+ Produce the insert statement.
144
+
145
+ Raises:
146
+ ValueError: If there is nothing to insert.
147
+ """
148
+ if not self.rows:
149
+ raise ValueError(f"Insert on {self.table.__name__} needs at least one row")
150
+ return _insert(self.table).values(self.rows)
@@ -1,68 +1,10 @@
1
1
  """
2
- A chainable query builder over SQLModel's select().
2
+ The read operation, under its original name.
3
3
  """
4
4
 
5
- from typing import Any
6
-
7
- from pydantic import BaseModel, Field
8
- from sqlmodel import desc as _desc
9
- from sqlmodel import select
10
-
11
- from corekit.connections.sql.table import NamedTable
5
+ from corekit.connections.sql.operations.statements import Select
12
6
 
13
7
  __all__ = ["Query"]
14
8
 
15
-
16
- class Query(BaseModel):
17
- """
18
- Accumulates filters, ordering and a limit, then builds a select statement.
19
-
20
- Query(table=User).where(User.age >= 18).order_by(User.name).limit(10)
21
- """
22
-
23
- table: type[NamedTable]
24
- queries: list[Any] = Field(default_factory=list)
25
- order_by_field: Any = None
26
- order_by_desc: bool = False
27
- limit_count: int = -1
28
-
29
- def where(self, query: Any) -> "Query":
30
- """
31
- Add a filter clause.
32
- """
33
- self.queries.append(query)
34
- return self
35
-
36
- def order_by(self, field: Any, desc: bool = False) -> "Query":
37
- """
38
- Order results by a field.
39
- """
40
- self.order_by_field = field
41
- self.order_by_desc = desc
42
- return self
43
-
44
- def limit(self, limit: int) -> "Query":
45
- """
46
- Cap the number of rows returned. A non-positive value means no limit.
47
- """
48
- self.limit_count = limit
49
- return self
50
-
51
- def build(self) -> Any:
52
- """
53
- Produce the select statement.
54
- """
55
- statement = select(self.table)
56
- for query in self.queries:
57
- statement = statement.where(query)
58
-
59
- if self.order_by_field is not None:
60
- order_by_field = self.order_by_field
61
- if self.order_by_desc:
62
- order_by_field = _desc(order_by_field)
63
- statement = statement.order_by(order_by_field)
64
-
65
- if self.limit_count > 0:
66
- statement = statement.limit(self.limit_count)
67
-
68
- return statement
9
+ #: ``Select`` names the statement it builds; ``Query`` is its original name.
10
+ Query = Select