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.
- corekit/api/__init__.py +18 -3
- corekit/api/application.py +237 -0
- corekit/api/lifespan.py +210 -0
- corekit/api/middleware.py +93 -0
- corekit/api/routers.py +109 -1
- corekit/concurrency/worker.py +65 -65
- corekit/config/settings.py +3 -3
- corekit/connections/sql/__init__.py +31 -3
- corekit/connections/sql/connection.py +19 -0
- corekit/connections/sql/migration/__init__.py +5 -5
- corekit/connections/sql/migration/base.py +3 -3
- corekit/connections/sql/migration/operations.py +66 -42
- corekit/connections/sql/migration/registry.py +2 -2
- corekit/connections/sql/operations/__init__.py +24 -0
- corekit/connections/sql/operations/base.py +102 -0
- corekit/connections/sql/operations/statements.py +150 -0
- corekit/connections/sql/query.py +4 -62
- corekit/connections/sql/table.py +30 -4
- corekit/constants.py +45 -45
- corekit/crypto/constants.py +4 -4
- corekit/data/__init__.py +8 -0
- corekit/data/expressions/__init__.py +10 -2
- corekit/data/expressions/comparison.py +184 -104
- corekit/data/expressions/expression.py +103 -98
- corekit/data/expressions/operator.py +54 -0
- corekit/data/expressions/target.py +21 -0
- corekit/data/record.py +147 -147
- corekit/data/stats.py +159 -157
- corekit/decorators/__init__.py +2 -2
- corekit/decorators/exception_handling.py +2 -1
- corekit/etl/connection.py +44 -44
- corekit/events/websocket.py +3 -2
- corekit/exceptions/__init__.py +18 -0
- corekit/http/__init__.py +13 -0
- corekit/jobs/__init__.py +26 -0
- corekit/jobs/registry.py +87 -0
- corekit/jobs/runner.py +69 -0
- corekit/jobs/task.py +152 -0
- corekit/observability/__init__.py +5 -3
- corekit/observability/request_context.py +135 -0
- corekit/registry/__init__.py +11 -6
- corekit/registry/ordered.py +86 -0
- corekit/schemas/__init__.py +10 -0
- corekit/schemas/enum.py +49 -49
- corekit/schemas/models/arbitrary.py +11 -11
- corekit/schemas/pydantic/fields.py +35 -35
- corekit/schemas/types.py +40 -40
- corekit/serialization/__init__.py +22 -0
- corekit/serialization/serializer.py +1 -1
- corekit/utils/__init__.py +59 -5
- corekit/utils/coercion.py +118 -0
- corekit/utils/collections.py +115 -0
- corekit/utils/ids.py +61 -5
- corekit/utils/payload.py +100 -0
- corekit/utils/raise_exc.py +8 -8
- corekit/utils/text.py +56 -0
- corekit/utils/time.py +74 -21
- corekit/utils/validators.py +15 -15
- corekit/utils/void.py +8 -8
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/METADATA +105 -100
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/RECORD +64 -46
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/WHEEL +0 -0
- {python_corekit-0.1.0.dist-info → python_corekit-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
|
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(
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
269
|
+
return f"DropColumn(table={self.table_name}, column={self.column})"
|
|
248
270
|
|
|
249
271
|
|
|
250
272
|
@dataclass
|
|
251
|
-
class AlterColumnType(
|
|
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.
|
|
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(
|
|
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
|
|
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.
|
|
308
|
+
return f"SetColumnDefault(table={self.table_name}, column={self.column}, default={self.default})"
|
|
287
309
|
|
|
288
310
|
|
|
289
311
|
@dataclass
|
|
290
|
-
class DropColumnDefault(
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
373
|
+
return f"DropTable(table={self.table_name}, cascade={self.cascade})"
|
|
352
374
|
|
|
353
375
|
|
|
354
376
|
@dataclass
|
|
355
|
-
class RenameTable(
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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("
|
|
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
|
|
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,
|
|
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)
|
corekit/connections/sql/query.py
CHANGED
|
@@ -1,68 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
The read operation, under its original name.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from
|
|
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
|
-
|
|
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
|