python-corekit 0.1.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/__init__.py +0 -0
- corekit/api/__init__.py +9 -0
- corekit/api/handler.py +76 -0
- corekit/api/responses.py +40 -0
- corekit/api/routers.py +115 -0
- corekit/concurrency/__init__.py +9 -0
- corekit/concurrency/decorators.py +72 -0
- corekit/concurrency/thread_local.py +99 -0
- corekit/concurrency/worker.py +65 -0
- corekit/config/__init__.py +47 -0
- corekit/config/loader.py +153 -0
- corekit/config/settings.py +161 -0
- corekit/config/sources.py +125 -0
- corekit/connections/__init__.py +31 -0
- corekit/connections/connectable.py +212 -0
- corekit/connections/decorators.py +92 -0
- corekit/connections/redis/__init__.py +7 -0
- corekit/connections/redis/connection.py +239 -0
- corekit/connections/registry.py +80 -0
- corekit/connections/sql/__init__.py +10 -0
- corekit/connections/sql/connection.py +342 -0
- corekit/connections/sql/fields/__init__.py +7 -0
- corekit/connections/sql/fields/jsonb.py +67 -0
- corekit/connections/sql/migration/__init__.py +57 -0
- corekit/connections/sql/migration/base.py +40 -0
- corekit/connections/sql/migration/operations.py +416 -0
- corekit/connections/sql/migration/registry.py +166 -0
- corekit/connections/sql/migration/table.py +27 -0
- corekit/connections/sql/query.py +68 -0
- corekit/connections/sql/table.py +96 -0
- corekit/constants.py +45 -0
- corekit/crypto/__init__.py +1 -0
- corekit/crypto/constants.py +7 -0
- corekit/crypto/enum.py +11 -0
- corekit/crypto/hasher.py +89 -0
- corekit/data/__init__.py +81 -0
- corekit/data/dataset.py +340 -0
- corekit/data/expressions/__init__.py +46 -0
- corekit/data/expressions/comparison.py +252 -0
- corekit/data/expressions/expression.py +98 -0
- corekit/data/record.py +147 -0
- corekit/data/stats.py +157 -0
- corekit/decorators/__init__.py +2 -0
- corekit/decorators/exception_handling.py +43 -0
- corekit/decorators/warnings.py +35 -0
- corekit/docker/__init__.py +7 -0
- corekit/docker/watchdog.py +222 -0
- corekit/etl/__init__.py +44 -0
- corekit/etl/connection.py +44 -0
- corekit/etl/extract/__init__.py +0 -0
- corekit/etl/extract/extractor.py +48 -0
- corekit/etl/extract/schemas.py +18 -0
- corekit/etl/load/__init__.py +0 -0
- corekit/etl/load/loader.py +53 -0
- corekit/etl/load/schemas.py +33 -0
- corekit/etl/orchestrator.py +201 -0
- corekit/etl/schemas.py +22 -0
- corekit/etl/transform/__init__.py +0 -0
- corekit/etl/transform/schemas.py +15 -0
- corekit/etl/transform/transformer.py +28 -0
- corekit/events/__init__.py +38 -0
- corekit/events/enum.py +58 -0
- corekit/events/frames.py +51 -0
- corekit/events/models.py +23 -0
- corekit/events/publisher.py +75 -0
- corekit/events/reader.py +132 -0
- corekit/events/sse.py +109 -0
- corekit/events/websocket.py +97 -0
- corekit/exceptions/__init__.py +0 -0
- corekit/exceptions/base.py +45 -0
- corekit/exceptions/custom/__init__.py +0 -0
- corekit/exceptions/http/__init__.py +0 -0
- corekit/exceptions/http/exceptions.py +37 -0
- corekit/exceptions/types.py +17 -0
- corekit/files/__init__.py +25 -0
- corekit/files/base.py +117 -0
- corekit/files/enum.py +30 -0
- corekit/files/json.py +12 -0
- corekit/files/pickle.py +12 -0
- corekit/files/toml.py +43 -0
- corekit/http/__init__.py +0 -0
- corekit/http/client.py +176 -0
- corekit/http/exponential_backoff.py +100 -0
- corekit/http/response.py +12 -0
- corekit/log_monitor/__init__.py +23 -0
- corekit/log_monitor/constants.py +8 -0
- corekit/log_monitor/models.py +150 -0
- corekit/log_monitor/service.py +418 -0
- corekit/notifications/__init__.py +8 -0
- corekit/notifications/base.py +51 -0
- corekit/notifications/models.py +34 -0
- corekit/observability/__init__.py +21 -0
- corekit/observability/benchmarkable.py +12 -0
- corekit/observability/loggable.py +29 -0
- corekit/observability/timing/__init__.py +0 -0
- corekit/observability/timing/constants.py +1 -0
- corekit/observability/timing/split.py +20 -0
- corekit/observability/timing/timer.py +30 -0
- corekit/py.typed +0 -0
- corekit/registry/__init__.py +12 -0
- corekit/registry/registry.py +134 -0
- corekit/schemas/__init__.py +0 -0
- corekit/schemas/dataclasses/__init__.py +0 -0
- corekit/schemas/enum.py +49 -0
- corekit/schemas/models/__init__.py +0 -0
- corekit/schemas/models/arbitrary.py +11 -0
- corekit/schemas/models/date_models.py +18 -0
- corekit/schemas/pydantic/__init__.py +0 -0
- corekit/schemas/pydantic/fields.py +35 -0
- corekit/schemas/types.py +40 -0
- corekit/serialization/__init__.py +0 -0
- corekit/serialization/enum.py +21 -0
- corekit/serialization/serializable.py +42 -0
- corekit/serialization/serializer.py +179 -0
- corekit/utils/__init__.py +5 -0
- corekit/utils/ids.py +5 -0
- corekit/utils/raise_exc.py +8 -0
- corekit/utils/time.py +21 -0
- corekit/utils/validators.py +15 -0
- corekit/utils/void.py +8 -0
- python_corekit-0.1.0.dist-info/METADATA +417 -0
- python_corekit-0.1.0.dist-info/RECORD +125 -0
- python_corekit-0.1.0.dist-info/WHEEL +5 -0
- python_corekit-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_corekit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Columns that store a Pydantic model as JSON.
|
|
3
|
+
|
|
4
|
+
``JSONBField`` round-trips a model through a JSON column, so a table can hold
|
|
5
|
+
structured data without a separate table and without hand-written encoding at
|
|
6
|
+
every call site::
|
|
7
|
+
|
|
8
|
+
class Profile(BaseModel):
|
|
9
|
+
theme: str = "dark"
|
|
10
|
+
|
|
11
|
+
class User(NamedTable, table=True):
|
|
12
|
+
id: str = Field(primary_key=True)
|
|
13
|
+
profile: Profile = JSONBField(Profile)
|
|
14
|
+
|
|
15
|
+
Reading ``user.profile`` gives a ``Profile``, not a dict.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel
|
|
21
|
+
from sqlalchemy import Column
|
|
22
|
+
from sqlalchemy.types import JSON, TypeDecorator
|
|
23
|
+
from sqlmodel import Field
|
|
24
|
+
|
|
25
|
+
__all__ = ["JSONBField", "PydanticJSON"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PydanticJSON(TypeDecorator):
|
|
29
|
+
"""
|
|
30
|
+
A JSON column that decodes into a Pydantic model on the way out.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
impl = JSON
|
|
34
|
+
cache_ok = True
|
|
35
|
+
|
|
36
|
+
def __init__(self, pydantic_model: type[BaseModel], *args: Any, **kwargs: Any) -> None:
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
self.pydantic_model = pydantic_model
|
|
39
|
+
|
|
40
|
+
def process_bind_param(self, value: Any, dialect: Any) -> Any:
|
|
41
|
+
"""
|
|
42
|
+
Encode a model, or an already-plain dict, for storage.
|
|
43
|
+
"""
|
|
44
|
+
if value is None:
|
|
45
|
+
return None
|
|
46
|
+
if isinstance(value, BaseModel):
|
|
47
|
+
return value.model_dump(mode="json", exclude_unset=True)
|
|
48
|
+
if isinstance(value, dict):
|
|
49
|
+
return value
|
|
50
|
+
raise TypeError(
|
|
51
|
+
f"{type(self).__name__} expected {self.pydantic_model.__name__}, dict or None, got {type(value).__name__}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def process_result_value(self, value: Any, dialect: Any) -> Any:
|
|
55
|
+
"""
|
|
56
|
+
Decode stored JSON back into the model.
|
|
57
|
+
"""
|
|
58
|
+
if value is None:
|
|
59
|
+
return None
|
|
60
|
+
return self.pydantic_model(**value)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def JSONBField(pydantic_model: type[BaseModel], **kwargs: Any) -> Any: # noqa: N802 - reads as a field constructor
|
|
64
|
+
"""
|
|
65
|
+
Declare a column holding ``pydantic_model`` as JSON.
|
|
66
|
+
"""
|
|
67
|
+
return Field(sa_column=Column(PydanticJSON(pydantic_model)), **kwargs)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schema migrations with checksummed history.
|
|
3
|
+
|
|
4
|
+
Define a migration by subclassing ``Migration`` and listing its operations::
|
|
5
|
+
|
|
6
|
+
class AddUserFlag(Migration):
|
|
7
|
+
version = 1
|
|
8
|
+
name = "user_is_active"
|
|
9
|
+
description = "Adds the is_active flag to users."
|
|
10
|
+
|
|
11
|
+
def operations(self) -> list[Operation]:
|
|
12
|
+
return [AddColumn(table="user", column="is_active", dtype="BOOLEAN", default="true")]
|
|
13
|
+
|
|
14
|
+
Collect them from a package and run them::
|
|
15
|
+
|
|
16
|
+
MigrationRegistry.discover(my_app.migrations).run(conn)
|
|
17
|
+
|
|
18
|
+
Each migration's checksum is taken over what its operations *do*, not over the
|
|
19
|
+
SQL they render, so reformatting is safe but changing an already-applied
|
|
20
|
+
migration is caught at startup.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from corekit.connections.sql.migration.base import Migration
|
|
24
|
+
from corekit.connections.sql.migration.operations import (
|
|
25
|
+
AddColumn,
|
|
26
|
+
AlterColumnType,
|
|
27
|
+
CreateIndex,
|
|
28
|
+
DataOperation,
|
|
29
|
+
DropColumn,
|
|
30
|
+
DropColumnDefault,
|
|
31
|
+
DropIndex,
|
|
32
|
+
DropTable,
|
|
33
|
+
Operation,
|
|
34
|
+
RenameColumn,
|
|
35
|
+
RenameTable,
|
|
36
|
+
SetColumnDefault,
|
|
37
|
+
)
|
|
38
|
+
from corekit.connections.sql.migration.registry import MigrationChecksumError, MigrationRegistry
|
|
39
|
+
from corekit.connections.sql.migration.table import SchemaMigration
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"AddColumn",
|
|
43
|
+
"AlterColumnType",
|
|
44
|
+
"CreateIndex",
|
|
45
|
+
"DataOperation",
|
|
46
|
+
"DropColumn",
|
|
47
|
+
"DropColumnDefault",
|
|
48
|
+
"DropIndex",
|
|
49
|
+
"DropTable",
|
|
50
|
+
"Migration",
|
|
51
|
+
"MigrationChecksumError",
|
|
52
|
+
"MigrationRegistry",
|
|
53
|
+
"Operation",
|
|
54
|
+
"RenameColumn",
|
|
55
|
+
"RenameTable",
|
|
56
|
+
"SchemaMigration",
|
|
57
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for all migrations.
|
|
3
|
+
|
|
4
|
+
A Migration is defined by:
|
|
5
|
+
- version: unique positive integer, determines execution order
|
|
6
|
+
- name: short human-readable label (used in logs and the DB table)
|
|
7
|
+
- description: longer explanation of what and why
|
|
8
|
+
- operations: ordered list of Operation instances to execute
|
|
9
|
+
|
|
10
|
+
The checksum is computed at class definition time from the canonical
|
|
11
|
+
representations of all operations. It is stored in the schema_migration
|
|
12
|
+
table when the migration is applied, and verified on every subsequent
|
|
13
|
+
startup. A mismatch means the migration was changed after being applied
|
|
14
|
+
and will cause a hard crash.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
|
|
20
|
+
from corekit.connections.sql.migration.operations import Operation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Migration(ABC):
|
|
24
|
+
version: int
|
|
25
|
+
name: str
|
|
26
|
+
description: str
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def operations(self) -> list[Operation]:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def checksum(self) -> str:
|
|
34
|
+
canonical = "\n".join(op.canonical() for op in self.operations())
|
|
35
|
+
return hashlib.sha256(canonical.encode()).hexdigest()
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
version = getattr(self, "version", None)
|
|
39
|
+
name = getattr(self, "name", None)
|
|
40
|
+
return f"{type(self).__name__}(version={version}, name={name!r})"
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration operations.
|
|
3
|
+
|
|
4
|
+
Each Operation knows how to:
|
|
5
|
+
- render itself to SQL (for execution) OR execute Python logic
|
|
6
|
+
- render a canonical string (for checksumming)
|
|
7
|
+
|
|
8
|
+
The canonical string must be deterministic and change whenever the
|
|
9
|
+
operation's intent changes — it is intentionally human-readable so that
|
|
10
|
+
diffs are obvious in code review.
|
|
11
|
+
|
|
12
|
+
Operations use keyword-only arguments to keep call sites explicit and readable.
|
|
13
|
+
|
|
14
|
+
The SQL rendered here targets Postgres. Most of it is standard and works on
|
|
15
|
+
SQLite too, but SQLite cannot drop or alter a column in place, so those
|
|
16
|
+
operations will fail there.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from corekit.connections.sql.connection import SQLConnection
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Constants
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
ALTER = "ALTER"
|
|
33
|
+
ADD = "ADD"
|
|
34
|
+
DROP = "DROP"
|
|
35
|
+
|
|
36
|
+
# Entities
|
|
37
|
+
COLUMN = "COLUMN"
|
|
38
|
+
INDEX = "INDEX"
|
|
39
|
+
TABLE = "TABLE"
|
|
40
|
+
TYPE = "TYPE"
|
|
41
|
+
USING = "USING"
|
|
42
|
+
|
|
43
|
+
DEFAULT = "DEFAULT"
|
|
44
|
+
IF_EXISTS = "IF EXISTS"
|
|
45
|
+
IF_NOT_EXISTS = "IF NOT EXISTS"
|
|
46
|
+
NOT_NULL = "NOT NULL"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Base classes
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
class QueryBuilder:
|
|
53
|
+
""" """
|
|
54
|
+
|
|
55
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
56
|
+
self._query = ""
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
return self.build()
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
return self.build()
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _build_subcommand(command: str, entity: str, name: str | None = None) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Build a subcommand for the query
|
|
68
|
+
"""
|
|
69
|
+
subcommand = f"{command} {entity} "
|
|
70
|
+
if name:
|
|
71
|
+
subcommand += f"{name} "
|
|
72
|
+
return subcommand
|
|
73
|
+
|
|
74
|
+
def append(self, part: str) -> QueryBuilder:
|
|
75
|
+
"""
|
|
76
|
+
Append a string to the query
|
|
77
|
+
"""
|
|
78
|
+
self._query += f"{part} "
|
|
79
|
+
return self
|
|
80
|
+
|
|
81
|
+
def extend(self, *parts: str) -> QueryBuilder:
|
|
82
|
+
"""
|
|
83
|
+
Append multiple strings to the query
|
|
84
|
+
"""
|
|
85
|
+
self._query += " ".join(parts) + " "
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def alter(self, entity: str, name: str | None = None) -> QueryBuilder:
|
|
89
|
+
"""
|
|
90
|
+
Append ALTER command to the query
|
|
91
|
+
"""
|
|
92
|
+
self._query += self._build_subcommand(ALTER, entity, name)
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def add(self, entity: str) -> QueryBuilder:
|
|
96
|
+
"""
|
|
97
|
+
Append ADD command to the query
|
|
98
|
+
"""
|
|
99
|
+
self._query += f"{ADD} {entity} "
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def drop(self, entity: str, name: str | None = None) -> QueryBuilder:
|
|
103
|
+
"""
|
|
104
|
+
Append DROP command to the query
|
|
105
|
+
"""
|
|
106
|
+
self._query += self._build_subcommand(DROP, entity, name)
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def not_null(self) -> QueryBuilder:
|
|
110
|
+
"""
|
|
111
|
+
Append NOT NULL to the query
|
|
112
|
+
"""
|
|
113
|
+
self._query += f"{NOT_NULL} "
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def if_not_exists(self) -> QueryBuilder:
|
|
117
|
+
"""
|
|
118
|
+
Append IF NOT EXISTS to the query
|
|
119
|
+
"""
|
|
120
|
+
self._query += f"{IF_NOT_EXISTS} "
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def if_exists(self) -> QueryBuilder:
|
|
124
|
+
"""
|
|
125
|
+
Append IF EXISTS to the query
|
|
126
|
+
"""
|
|
127
|
+
self._query += f"{IF_EXISTS} "
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def default(self, default: str) -> QueryBuilder:
|
|
131
|
+
self._query += f"{DEFAULT} {default} "
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
def set_default(self, default: str) -> QueryBuilder:
|
|
135
|
+
self._query += f"SET {DEFAULT} {default} "
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def drop_default(self) -> QueryBuilder:
|
|
139
|
+
self._query += f"DROP {DEFAULT} "
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def type(self, name: str) -> QueryBuilder:
|
|
143
|
+
self._query += f"{TYPE} {name} "
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def using(self, name: str) -> QueryBuilder:
|
|
147
|
+
self._query += f"{USING} {name} "
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def build(self) -> str:
|
|
151
|
+
return self._query.strip()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Operation(ABC):
|
|
155
|
+
"""
|
|
156
|
+
Base class for all operations.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def _builder(self) -> QueryBuilder:
|
|
160
|
+
return QueryBuilder()
|
|
161
|
+
|
|
162
|
+
@abstractmethod
|
|
163
|
+
def to_sql(self) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Render the SQL to execute against the database
|
|
166
|
+
|
|
167
|
+
Each sub-class must implement this method and provide and example in the docstring.
|
|
168
|
+
Examples must use the format:
|
|
169
|
+
|
|
170
|
+
Example: <PRE-DEFINED_COMMAND> <table_name> <PRE-DEFINED_COMMAND> [IF NOT EXISTS] <column_name> <dtype>
|
|
171
|
+
|
|
172
|
+
Where:
|
|
173
|
+
- <PRE-DEFINED_COMMAND>: (all-caps inside <>) represents a pre-defined SQL command unique to the sub-class
|
|
174
|
+
- <*>: (lowercase inside <>) represents a placeholder variable added at runtime
|
|
175
|
+
- [*]: (anything inside []) represents an optional part of the command
|
|
176
|
+
"""
|
|
177
|
+
raise NotImplementedError
|
|
178
|
+
|
|
179
|
+
@abstractmethod
|
|
180
|
+
def canonical(self) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Deterministic string representation used for checksumming
|
|
183
|
+
"""
|
|
184
|
+
raise NotImplementedError
|
|
185
|
+
|
|
186
|
+
def __repr__(self) -> str:
|
|
187
|
+
return self.canonical()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ---------------------------------------------------------------------------
|
|
191
|
+
# Column operations
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class AddColumn(Operation):
|
|
197
|
+
"""
|
|
198
|
+
Add a column to an existing table.
|
|
199
|
+
|
|
200
|
+
``safe`` emits ``IF NOT EXISTS``, which Postgres accepts and SQLite does
|
|
201
|
+
not. Set it to False when targeting SQLite.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
table: str
|
|
205
|
+
column: str
|
|
206
|
+
dtype: str
|
|
207
|
+
safe: bool = True
|
|
208
|
+
nullable: bool = True
|
|
209
|
+
default: str | None = None
|
|
210
|
+
|
|
211
|
+
def to_sql(self) -> str:
|
|
212
|
+
"""
|
|
213
|
+
Example: ALTER TABLE <table_name> ADD COLUMN [IF NOT EXISTS] <column_name> <dtype>
|
|
214
|
+
"""
|
|
215
|
+
builder = self._builder().alter(TABLE, self.table).add(COLUMN)
|
|
216
|
+
if self.safe:
|
|
217
|
+
builder.if_not_exists()
|
|
218
|
+
|
|
219
|
+
builder.append(self.column).append(self.dtype)
|
|
220
|
+
if self.default is not None:
|
|
221
|
+
builder.default(self.default)
|
|
222
|
+
|
|
223
|
+
if not self.nullable:
|
|
224
|
+
builder.not_null()
|
|
225
|
+
|
|
226
|
+
return builder.build()
|
|
227
|
+
|
|
228
|
+
def canonical(self) -> str:
|
|
229
|
+
return (
|
|
230
|
+
f"AddColumn(table={self.table}, column={self.column}, dtype={self.dtype}, "
|
|
231
|
+
f"nullable={self.nullable}, default={self.default})"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass
|
|
236
|
+
class DropColumn(Operation):
|
|
237
|
+
table: str
|
|
238
|
+
column: str
|
|
239
|
+
|
|
240
|
+
def to_sql(self) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Example: ALTER TABLE <table_name> DROP COLUMN [IF EXISTS] <column_name>
|
|
243
|
+
"""
|
|
244
|
+
return self._builder().alter(TABLE, self.table).drop(COLUMN).if_exists().append(self.column).build()
|
|
245
|
+
|
|
246
|
+
def canonical(self) -> str:
|
|
247
|
+
return f"DropColumn(table={self.table}, column={self.column})"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@dataclass
|
|
251
|
+
class AlterColumnType(Operation):
|
|
252
|
+
table: str
|
|
253
|
+
column: str
|
|
254
|
+
new_dtype: str
|
|
255
|
+
using: str | None = None
|
|
256
|
+
|
|
257
|
+
def to_sql(self) -> str:
|
|
258
|
+
"""
|
|
259
|
+
Example: ALTER TABLE <table_name> ALTER COLUMN <column_name> TYPE <new_dtype> [USING <using>]
|
|
260
|
+
"""
|
|
261
|
+
builder = self._builder().alter(TABLE, self.table).alter(COLUMN, self.column).type(self.new_dtype)
|
|
262
|
+
if self.using:
|
|
263
|
+
builder.using(self.using)
|
|
264
|
+
|
|
265
|
+
return builder.build()
|
|
266
|
+
|
|
267
|
+
def canonical(self) -> str:
|
|
268
|
+
return (
|
|
269
|
+
f"AlterColumnType(table={self.table}, column={self.column}, new_dtype={self.new_dtype}, using={self.using})"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass
|
|
274
|
+
class SetColumnDefault(Operation):
|
|
275
|
+
table: str
|
|
276
|
+
column: str
|
|
277
|
+
default: str
|
|
278
|
+
|
|
279
|
+
def to_sql(self) -> str:
|
|
280
|
+
"""
|
|
281
|
+
Example: ALTER TABLE <table_name> ALTER COLUMN <column_name> SET DEFAULT <default>
|
|
282
|
+
"""
|
|
283
|
+
return self._builder().alter(TABLE, self.table).alter(COLUMN, self.column).set_default(self.default).build()
|
|
284
|
+
|
|
285
|
+
def canonical(self) -> str:
|
|
286
|
+
return f"SetColumnDefault(table={self.table}, column={self.column}, default={self.default})"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass
|
|
290
|
+
class DropColumnDefault(Operation):
|
|
291
|
+
table: str
|
|
292
|
+
column: str
|
|
293
|
+
|
|
294
|
+
def to_sql(self) -> str:
|
|
295
|
+
"""
|
|
296
|
+
Example: ALTER TABLE <table_name> ALTER COLUMN <column_name> DROP DEFAULT
|
|
297
|
+
"""
|
|
298
|
+
return self._builder().alter(TABLE, self.table).alter(COLUMN, self.column).drop_default().build()
|
|
299
|
+
|
|
300
|
+
def canonical(self) -> str:
|
|
301
|
+
return f"DropColumnDefault(table={self.table}, column={self.column})"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Index operations
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@dataclass
|
|
310
|
+
class CreateIndex(Operation):
|
|
311
|
+
index_name: str
|
|
312
|
+
table: str
|
|
313
|
+
columns: list[str]
|
|
314
|
+
unique: bool = False
|
|
315
|
+
|
|
316
|
+
def to_sql(self) -> str:
|
|
317
|
+
unique_clause = "UNIQUE " if self.unique else ""
|
|
318
|
+
cols = ", ".join(self.columns)
|
|
319
|
+
return f"CREATE {unique_clause}INDEX IF NOT EXISTS {self.index_name} ON {self.table} ({cols})"
|
|
320
|
+
|
|
321
|
+
def canonical(self) -> str:
|
|
322
|
+
return f"CreateIndex(name={self.index_name}, table={self.table}, columns={self.columns}, unique={self.unique})"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@dataclass
|
|
326
|
+
class DropIndex(Operation):
|
|
327
|
+
index_name: str
|
|
328
|
+
|
|
329
|
+
def to_sql(self) -> str:
|
|
330
|
+
return f"DROP INDEX IF EXISTS {self.index_name}"
|
|
331
|
+
|
|
332
|
+
def canonical(self) -> str:
|
|
333
|
+
return f"DropIndex(name={self.index_name})"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
# Table operations
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@dataclass
|
|
342
|
+
class DropTable(Operation):
|
|
343
|
+
table: str
|
|
344
|
+
cascade: bool = False
|
|
345
|
+
|
|
346
|
+
def to_sql(self) -> str:
|
|
347
|
+
cascade_clause = " CASCADE" if self.cascade else ""
|
|
348
|
+
return f"DROP TABLE IF EXISTS {self.table}{cascade_clause}"
|
|
349
|
+
|
|
350
|
+
def canonical(self) -> str:
|
|
351
|
+
return f"DropTable(table={self.table}, cascade={self.cascade})"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@dataclass
|
|
355
|
+
class RenameTable(Operation):
|
|
356
|
+
old_name: str
|
|
357
|
+
new_name: str
|
|
358
|
+
|
|
359
|
+
def to_sql(self) -> str:
|
|
360
|
+
return f"ALTER TABLE {self.old_name} RENAME TO {self.new_name}"
|
|
361
|
+
|
|
362
|
+
def canonical(self) -> str:
|
|
363
|
+
return f"RenameTable(old={self.old_name}, new={self.new_name})"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@dataclass
|
|
367
|
+
class RenameColumn(Operation):
|
|
368
|
+
table: str
|
|
369
|
+
old_name: str
|
|
370
|
+
new_name: str
|
|
371
|
+
|
|
372
|
+
def to_sql(self) -> str:
|
|
373
|
+
return f"ALTER TABLE {self.table} RENAME COLUMN {self.old_name} TO {self.new_name}"
|
|
374
|
+
|
|
375
|
+
def canonical(self) -> str:
|
|
376
|
+
return f"RenameColumn(table={self.table}, old={self.old_name}, new={self.new_name})"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
# Data operations
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@dataclass
|
|
385
|
+
class DataOperation(Operation):
|
|
386
|
+
"""
|
|
387
|
+
Executes a Python function for data migrations (seeding, transformations).
|
|
388
|
+
|
|
389
|
+
The function receives a SQLConnection and should use ORM methods to
|
|
390
|
+
read/write data. The `name` is used for checksumming - changing it will
|
|
391
|
+
invalidate the checksum, so choose a stable, descriptive name.
|
|
392
|
+
|
|
393
|
+
Example:
|
|
394
|
+
DataOperation(
|
|
395
|
+
name="seed_user_permissions_v1",
|
|
396
|
+
func=seed_permissions_from_roles,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
IMPORTANT: It is not appropriate to use DataOperation in place of other operations. This is
|
|
400
|
+
reserved for the most complex operations that CANNOT be expressed otherwise.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
name: str
|
|
404
|
+
func: Callable[[SQLConnection], None]
|
|
405
|
+
|
|
406
|
+
def execute(self, conn: SQLConnection) -> None:
|
|
407
|
+
"""
|
|
408
|
+
Execute the data operation with the given connection
|
|
409
|
+
"""
|
|
410
|
+
self.func(conn)
|
|
411
|
+
|
|
412
|
+
def to_sql(self) -> str:
|
|
413
|
+
raise NotImplementedError("DataOperation does not produce SQL; use execute() instead")
|
|
414
|
+
|
|
415
|
+
def canonical(self) -> str:
|
|
416
|
+
return f"DataOperation(name={self.name})"
|