sqlspec 0.25.0__py3-none-any.whl → 0.27.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.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/__init__.py +7 -15
- sqlspec/_serialization.py +256 -24
- sqlspec/_typing.py +71 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +69 -12
- sqlspec/adapters/adbc/data_dictionary.py +340 -0
- sqlspec/adapters/adbc/driver.py +266 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +88 -15
- sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
- sqlspec/adapters/aiosqlite/driver.py +143 -40
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +2 -2
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +68 -23
- sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
- sqlspec/adapters/asyncmy/driver.py +313 -58
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +450 -0
- sqlspec/adapters/asyncpg/config.py +59 -35
- sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
- sqlspec/adapters/asyncpg/driver.py +170 -25
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +576 -0
- sqlspec/adapters/bigquery/config.py +27 -10
- sqlspec/adapters/bigquery/data_dictionary.py +149 -0
- sqlspec/adapters/bigquery/driver.py +368 -142
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +80 -20
- sqlspec/adapters/duckdb/data_dictionary.py +163 -0
- sqlspec/adapters/duckdb/driver.py +167 -45
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +4 -4
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1745 -0
- sqlspec/adapters/oracledb/config.py +122 -32
- sqlspec/adapters/oracledb/data_dictionary.py +509 -0
- sqlspec/adapters/oracledb/driver.py +353 -91
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +348 -73
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +482 -0
- sqlspec/adapters/psqlpy/config.py +46 -17
- sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
- sqlspec/adapters/psqlpy/driver.py +123 -209
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +944 -0
- sqlspec/adapters/psycopg/config.py +69 -35
- sqlspec/adapters/psycopg/data_dictionary.py +331 -0
- sqlspec/adapters/psycopg/driver.py +238 -81
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +572 -0
- sqlspec/adapters/sqlite/config.py +87 -15
- sqlspec/adapters/sqlite/data_dictionary.py +149 -0
- sqlspec/adapters/sqlite/driver.py +137 -54
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +18 -9
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +162 -89
- sqlspec/builder/_column.py +62 -29
- sqlspec/builder/_ddl.py +180 -121
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +53 -94
- sqlspec/builder/_insert.py +32 -131
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +111 -17
- sqlspec/builder/_select.py +1457 -24
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +307 -194
- sqlspec/config.py +252 -67
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +17 -17
- sqlspec/core/compiler.py +62 -9
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +83 -48
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +36 -30
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +7 -6
- sqlspec/driver/_async.py +188 -151
- sqlspec/driver/_common.py +285 -80
- sqlspec/driver/_sync.py +188 -152
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +75 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +302 -39
- sqlspec/migrations/commands.py +611 -144
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +68 -23
- sqlspec/migrations/runner.py +543 -107
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +16 -84
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +50 -2
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_join_operations.py +0 -389
- sqlspec/builder/mixins/_merge_operations.py +0 -592
- sqlspec/builder/mixins/_order_limit_operations.py +0 -152
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -936
- sqlspec/builder/mixins/_update_operations.py +0 -218
- sqlspec/builder/mixins/_where_clause.py +0 -1304
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/migrations/base.py
CHANGED
|
@@ -4,18 +4,18 @@ This module provides abstract base classes for migration components.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import hashlib
|
|
7
|
-
import operator
|
|
8
7
|
from abc import ABC, abstractmethod
|
|
9
8
|
from pathlib import Path
|
|
10
|
-
from typing import Any, Generic,
|
|
9
|
+
from typing import Any, Generic, TypeVar, cast
|
|
11
10
|
|
|
12
|
-
from sqlspec.
|
|
13
|
-
from sqlspec.builder import Delete, Insert, Select
|
|
11
|
+
from sqlspec.builder import Delete, Insert, Select, Update, sql
|
|
14
12
|
from sqlspec.builder._ddl import CreateTable
|
|
15
13
|
from sqlspec.loader import SQLFileLoader
|
|
16
14
|
from sqlspec.migrations.loaders import get_migration_loader
|
|
17
15
|
from sqlspec.utils.logging import get_logger
|
|
16
|
+
from sqlspec.utils.module_loader import module_to_os_path
|
|
18
17
|
from sqlspec.utils.sync_tools import await_
|
|
18
|
+
from sqlspec.utils.version import parse_version
|
|
19
19
|
|
|
20
20
|
__all__ = ("BaseMigrationCommands", "BaseMigrationRunner", "BaseMigrationTracker")
|
|
21
21
|
|
|
@@ -42,6 +42,16 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
42
42
|
def _get_create_table_sql(self) -> CreateTable:
|
|
43
43
|
"""Get SQL builder for creating the tracking table.
|
|
44
44
|
|
|
45
|
+
Schema includes both legacy and new versioning columns:
|
|
46
|
+
- version_num: Migration version (sequential or timestamp format)
|
|
47
|
+
- version_type: Format indicator ('sequential' or 'timestamp')
|
|
48
|
+
- execution_sequence: Auto-incrementing application order
|
|
49
|
+
- description: Human-readable migration description
|
|
50
|
+
- applied_at: Timestamp when migration was applied
|
|
51
|
+
- execution_time_ms: Migration execution duration
|
|
52
|
+
- checksum: MD5 hash for content verification
|
|
53
|
+
- applied_by: User who applied the migration
|
|
54
|
+
|
|
45
55
|
Returns:
|
|
46
56
|
SQL builder object for table creation.
|
|
47
57
|
"""
|
|
@@ -49,6 +59,8 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
49
59
|
sql.create_table(self.version_table)
|
|
50
60
|
.if_not_exists()
|
|
51
61
|
.column("version_num", "VARCHAR(32)", primary_key=True)
|
|
62
|
+
.column("version_type", "VARCHAR(16)")
|
|
63
|
+
.column("execution_sequence", "INTEGER")
|
|
52
64
|
.column("description", "TEXT")
|
|
53
65
|
.column("applied_at", "TIMESTAMP", default="CURRENT_TIMESTAMP", not_null=True)
|
|
54
66
|
.column("execution_time_ms", "INTEGER")
|
|
@@ -59,26 +71,49 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
59
71
|
def _get_current_version_sql(self) -> Select:
|
|
60
72
|
"""Get SQL builder for retrieving current version.
|
|
61
73
|
|
|
74
|
+
Uses execution_sequence to get the last applied migration,
|
|
75
|
+
which may differ from version_num order due to out-of-order migrations.
|
|
76
|
+
|
|
62
77
|
Returns:
|
|
63
78
|
SQL builder object for version query.
|
|
64
79
|
"""
|
|
65
|
-
return sql.select("version_num").from_(self.version_table).order_by("
|
|
80
|
+
return sql.select("version_num").from_(self.version_table).order_by("execution_sequence DESC").limit(1)
|
|
66
81
|
|
|
67
82
|
def _get_applied_migrations_sql(self) -> Select:
|
|
68
83
|
"""Get SQL builder for retrieving all applied migrations.
|
|
69
84
|
|
|
85
|
+
Orders by execution_sequence to show migrations in application order,
|
|
86
|
+
which preserves the actual execution history for out-of-order migrations.
|
|
87
|
+
|
|
70
88
|
Returns:
|
|
71
89
|
SQL builder object for migrations query.
|
|
72
90
|
"""
|
|
73
|
-
return sql.select("*").from_(self.version_table).order_by("
|
|
91
|
+
return sql.select("*").from_(self.version_table).order_by("execution_sequence")
|
|
92
|
+
|
|
93
|
+
def _get_next_execution_sequence_sql(self) -> Select:
|
|
94
|
+
"""Get SQL builder for retrieving next execution sequence.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
SQL builder object for sequence query.
|
|
98
|
+
"""
|
|
99
|
+
return sql.select("COALESCE(MAX(execution_sequence), 0) + 1 AS next_seq").from_(self.version_table)
|
|
74
100
|
|
|
75
101
|
def _get_record_migration_sql(
|
|
76
|
-
self,
|
|
102
|
+
self,
|
|
103
|
+
version: str,
|
|
104
|
+
version_type: str,
|
|
105
|
+
execution_sequence: int,
|
|
106
|
+
description: str,
|
|
107
|
+
execution_time_ms: int,
|
|
108
|
+
checksum: str,
|
|
109
|
+
applied_by: str,
|
|
77
110
|
) -> Insert:
|
|
78
111
|
"""Get SQL builder for recording a migration.
|
|
79
112
|
|
|
80
113
|
Args:
|
|
81
114
|
version: Version number of the migration.
|
|
115
|
+
version_type: Version format type ('sequential' or 'timestamp').
|
|
116
|
+
execution_sequence: Auto-incrementing application order.
|
|
82
117
|
description: Description of the migration.
|
|
83
118
|
execution_time_ms: Execution time in milliseconds.
|
|
84
119
|
checksum: MD5 checksum of the migration content.
|
|
@@ -89,8 +124,16 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
89
124
|
"""
|
|
90
125
|
return (
|
|
91
126
|
sql.insert(self.version_table)
|
|
92
|
-
.columns(
|
|
93
|
-
|
|
127
|
+
.columns(
|
|
128
|
+
"version_num",
|
|
129
|
+
"version_type",
|
|
130
|
+
"execution_sequence",
|
|
131
|
+
"description",
|
|
132
|
+
"execution_time_ms",
|
|
133
|
+
"checksum",
|
|
134
|
+
"applied_by",
|
|
135
|
+
)
|
|
136
|
+
.values(version, version_type, execution_sequence, description, execution_time_ms, checksum, applied_by)
|
|
94
137
|
)
|
|
95
138
|
|
|
96
139
|
def _get_remove_migration_sql(self, version: str) -> Delete:
|
|
@@ -104,9 +147,90 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
104
147
|
"""
|
|
105
148
|
return sql.delete().from_(self.version_table).where(sql.version_num == version)
|
|
106
149
|
|
|
150
|
+
def _get_update_version_sql(self, old_version: str, new_version: str, new_version_type: str) -> Update:
|
|
151
|
+
"""Get SQL builder for updating version record.
|
|
152
|
+
|
|
153
|
+
Updates version_num and version_type while preserving execution_sequence,
|
|
154
|
+
applied_at, and other metadata. Used during fix command to convert
|
|
155
|
+
timestamp versions to sequential format.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
old_version: Current version string.
|
|
159
|
+
new_version: New version string.
|
|
160
|
+
new_version_type: New version type ('sequential' or 'timestamp').
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
SQL builder object for update.
|
|
164
|
+
"""
|
|
165
|
+
return (
|
|
166
|
+
sql.update(self.version_table)
|
|
167
|
+
.set("version_num", new_version)
|
|
168
|
+
.set("version_type", new_version_type)
|
|
169
|
+
.where(sql.version_num == old_version)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _get_check_column_exists_sql(self) -> Select:
|
|
173
|
+
"""Get SQL to check what columns exist in the tracking table.
|
|
174
|
+
|
|
175
|
+
Returns a query that will fail gracefully if the table doesn't exist,
|
|
176
|
+
and returns column names if it does.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
SQL builder object for column check query.
|
|
180
|
+
"""
|
|
181
|
+
return sql.select("*").from_(self.version_table).limit(0)
|
|
182
|
+
|
|
183
|
+
def _get_add_missing_columns_sql(self, missing_columns: "set[str]") -> "list[str]":
|
|
184
|
+
"""Generate ALTER TABLE statements to add missing columns.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
missing_columns: Set of column names that need to be added.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
List of SQL statements to execute.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
statements = []
|
|
194
|
+
target_create = self._get_create_table_sql()
|
|
195
|
+
|
|
196
|
+
column_definitions = {col.name.lower(): col for col in target_create.columns}
|
|
197
|
+
|
|
198
|
+
for col_name in sorted(missing_columns):
|
|
199
|
+
if col_name in column_definitions:
|
|
200
|
+
col_def = column_definitions[col_name]
|
|
201
|
+
alter = sql.alter_table(self.version_table).add_column(
|
|
202
|
+
name=col_def.name,
|
|
203
|
+
dtype=col_def.dtype,
|
|
204
|
+
default=col_def.default,
|
|
205
|
+
not_null=col_def.not_null,
|
|
206
|
+
unique=col_def.unique,
|
|
207
|
+
comment=col_def.comment,
|
|
208
|
+
)
|
|
209
|
+
statements.append(str(alter))
|
|
210
|
+
|
|
211
|
+
return statements
|
|
212
|
+
|
|
213
|
+
def _detect_missing_columns(self, existing_columns: "set[str]") -> "set[str]":
|
|
214
|
+
"""Detect which columns are missing from the current schema.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
existing_columns: Set of existing column names (may be uppercase/lowercase).
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Set of missing column names (lowercase).
|
|
221
|
+
"""
|
|
222
|
+
target_create = self._get_create_table_sql()
|
|
223
|
+
target_columns = {col.name.lower() for col in target_create.columns}
|
|
224
|
+
existing_lower = {col.lower() for col in existing_columns}
|
|
225
|
+
return target_columns - existing_lower
|
|
226
|
+
|
|
107
227
|
@abstractmethod
|
|
108
228
|
def ensure_tracking_table(self, driver: DriverT) -> Any:
|
|
109
|
-
"""Create the migration tracking table if it doesn't exist.
|
|
229
|
+
"""Create the migration tracking table if it doesn't exist.
|
|
230
|
+
|
|
231
|
+
Implementations should also check for and add any missing columns
|
|
232
|
+
to support schema migrations from older versions.
|
|
233
|
+
"""
|
|
110
234
|
...
|
|
111
235
|
|
|
112
236
|
@abstractmethod
|
|
@@ -135,17 +259,31 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
135
259
|
class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
136
260
|
"""Base class for migration execution."""
|
|
137
261
|
|
|
138
|
-
|
|
262
|
+
extension_configs: "dict[str, dict[str, Any]]"
|
|
263
|
+
|
|
264
|
+
def __init__(
|
|
265
|
+
self,
|
|
266
|
+
migrations_path: Path,
|
|
267
|
+
extension_migrations: "dict[str, Path] | None" = None,
|
|
268
|
+
context: "Any | None" = None,
|
|
269
|
+
extension_configs: "dict[str, dict[str, Any]] | None" = None,
|
|
270
|
+
) -> None:
|
|
139
271
|
"""Initialize the migration runner.
|
|
140
272
|
|
|
141
273
|
Args:
|
|
142
274
|
migrations_path: Path to the directory containing migration files.
|
|
275
|
+
extension_migrations: Optional mapping of extension names to their migration paths.
|
|
276
|
+
context: Optional migration context for Python migrations.
|
|
277
|
+
extension_configs: Optional mapping of extension names to their configurations.
|
|
143
278
|
"""
|
|
144
279
|
self.migrations_path = migrations_path
|
|
280
|
+
self.extension_migrations = extension_migrations or {}
|
|
145
281
|
self.loader = SQLFileLoader()
|
|
146
|
-
self.project_root:
|
|
282
|
+
self.project_root: Path | None = None
|
|
283
|
+
self.context = context
|
|
284
|
+
self.extension_configs = extension_configs or {}
|
|
147
285
|
|
|
148
|
-
def _extract_version(self, filename: str) ->
|
|
286
|
+
def _extract_version(self, filename: str) -> str | None:
|
|
149
287
|
"""Extract version from filename.
|
|
150
288
|
|
|
151
289
|
Args:
|
|
@@ -154,7 +292,14 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
154
292
|
Returns:
|
|
155
293
|
The extracted version string or None.
|
|
156
294
|
"""
|
|
157
|
-
|
|
295
|
+
from pathlib import Path
|
|
296
|
+
|
|
297
|
+
stem = Path(filename).stem
|
|
298
|
+
|
|
299
|
+
if stem.startswith("ext_"):
|
|
300
|
+
return stem
|
|
301
|
+
|
|
302
|
+
parts = stem.split("_", 1)
|
|
158
303
|
return parts[0].zfill(4) if parts and parts[0].isdigit() else None
|
|
159
304
|
|
|
160
305
|
def _calculate_checksum(self, content: str) -> str:
|
|
@@ -172,38 +317,73 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
172
317
|
def _get_migration_files_sync(self) -> "list[tuple[str, Path]]":
|
|
173
318
|
"""Get all migration files sorted by version.
|
|
174
319
|
|
|
320
|
+
Uses version-aware sorting that handles both sequential and timestamp
|
|
321
|
+
formats correctly, with extension migrations sorted by extension name.
|
|
322
|
+
|
|
175
323
|
Returns:
|
|
176
324
|
List of tuples containing (version, file_path).
|
|
177
325
|
"""
|
|
178
|
-
if not self.migrations_path.exists():
|
|
179
|
-
return []
|
|
180
|
-
|
|
181
326
|
migrations = []
|
|
182
|
-
for pattern in ["*.sql", "*.py"]:
|
|
183
|
-
for file_path in self.migrations_path.glob(pattern):
|
|
184
|
-
if file_path.name.startswith("."):
|
|
185
|
-
continue
|
|
186
|
-
version = self._extract_version(file_path.name)
|
|
187
|
-
if version:
|
|
188
|
-
migrations.append((version, file_path))
|
|
189
|
-
|
|
190
|
-
return sorted(migrations, key=operator.itemgetter(0))
|
|
191
327
|
|
|
192
|
-
|
|
328
|
+
# Scan primary migration path
|
|
329
|
+
if self.migrations_path.exists():
|
|
330
|
+
for pattern in ("*.sql", "*.py"):
|
|
331
|
+
for file_path in self.migrations_path.glob(pattern):
|
|
332
|
+
if file_path.name.startswith("."):
|
|
333
|
+
continue
|
|
334
|
+
version = self._extract_version(file_path.name)
|
|
335
|
+
if version:
|
|
336
|
+
migrations.append((version, file_path))
|
|
337
|
+
|
|
338
|
+
# Scan extension migration paths
|
|
339
|
+
for ext_name, ext_path in self.extension_migrations.items():
|
|
340
|
+
if ext_path.exists():
|
|
341
|
+
for pattern in ("*.sql", "*.py"):
|
|
342
|
+
for file_path in ext_path.glob(pattern):
|
|
343
|
+
if file_path.name.startswith("."):
|
|
344
|
+
continue
|
|
345
|
+
# Prefix extension migrations to avoid version conflicts
|
|
346
|
+
version = self._extract_version(file_path.name)
|
|
347
|
+
if version:
|
|
348
|
+
# Use ext_ prefix to distinguish extension migrations
|
|
349
|
+
prefixed_version = f"ext_{ext_name}_{version}"
|
|
350
|
+
migrations.append((prefixed_version, file_path))
|
|
351
|
+
|
|
352
|
+
return sorted(migrations, key=lambda m: parse_version(m[0]))
|
|
353
|
+
|
|
354
|
+
def _load_migration_metadata(self, file_path: Path, version: "str | None" = None) -> "dict[str, Any]":
|
|
193
355
|
"""Load migration metadata from file.
|
|
194
356
|
|
|
195
357
|
Args:
|
|
196
358
|
file_path: Path to the migration file.
|
|
359
|
+
version: Optional pre-extracted version (preserves prefixes like ext_adk_0001).
|
|
197
360
|
|
|
198
361
|
Returns:
|
|
199
362
|
Migration metadata dictionary.
|
|
200
363
|
"""
|
|
201
|
-
|
|
202
|
-
|
|
364
|
+
if version is None:
|
|
365
|
+
version = self._extract_version(file_path.name)
|
|
366
|
+
|
|
367
|
+
context_to_use = self.context
|
|
368
|
+
|
|
369
|
+
for ext_name, ext_path in self.extension_migrations.items():
|
|
370
|
+
if file_path.parent == ext_path:
|
|
371
|
+
if ext_name in self.extension_configs and self.context:
|
|
372
|
+
from sqlspec.migrations.context import MigrationContext
|
|
373
|
+
|
|
374
|
+
context_to_use = MigrationContext(
|
|
375
|
+
dialect=self.context.dialect,
|
|
376
|
+
config=self.context.config,
|
|
377
|
+
driver=self.context.driver,
|
|
378
|
+
metadata=self.context.metadata.copy() if self.context.metadata else {},
|
|
379
|
+
extension_config=self.extension_configs[ext_name],
|
|
380
|
+
)
|
|
381
|
+
break
|
|
382
|
+
|
|
383
|
+
loader = get_migration_loader(file_path, self.migrations_path, self.project_root, context_to_use)
|
|
203
384
|
loader.validate_migration_file(file_path)
|
|
204
385
|
content = file_path.read_text(encoding="utf-8")
|
|
205
386
|
checksum = self._calculate_checksum(content)
|
|
206
|
-
version = self._extract_version(file_path.name)
|
|
207
387
|
description = file_path.stem.split("_", 1)[1] if "_" in file_path.stem else ""
|
|
208
388
|
|
|
209
389
|
has_upgrade, has_downgrade = True, False
|
|
@@ -229,7 +409,7 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
229
409
|
"loader": loader,
|
|
230
410
|
}
|
|
231
411
|
|
|
232
|
-
def _get_migration_sql(self, migration: "dict[str, Any]", direction: str) -> "
|
|
412
|
+
def _get_migration_sql(self, migration: "dict[str, Any]", direction: str) -> "list[str] | None":
|
|
233
413
|
"""Get migration SQL for given direction.
|
|
234
414
|
|
|
235
415
|
Args:
|
|
@@ -292,6 +472,8 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
292
472
|
class BaseMigrationCommands(ABC, Generic[ConfigT, DriverT]):
|
|
293
473
|
"""Base class for migration commands."""
|
|
294
474
|
|
|
475
|
+
extension_configs: "dict[str, dict[str, Any]]"
|
|
476
|
+
|
|
295
477
|
def __init__(self, config: ConfigT) -> None:
|
|
296
478
|
"""Initialize migration commands.
|
|
297
479
|
|
|
@@ -304,6 +486,56 @@ class BaseMigrationCommands(ABC, Generic[ConfigT, DriverT]):
|
|
|
304
486
|
self.version_table = migration_config.get("version_table_name", "ddl_migrations")
|
|
305
487
|
self.migrations_path = Path(migration_config.get("script_location", "migrations"))
|
|
306
488
|
self.project_root = Path(migration_config["project_root"]) if "project_root" in migration_config else None
|
|
489
|
+
self.include_extensions = migration_config.get("include_extensions", [])
|
|
490
|
+
self.extension_configs = self._parse_extension_configs()
|
|
491
|
+
|
|
492
|
+
def _parse_extension_configs(self) -> "dict[str, dict[str, Any]]":
|
|
493
|
+
"""Parse extension configurations from include_extensions.
|
|
494
|
+
|
|
495
|
+
Reads extension configuration from config.extension_config for each
|
|
496
|
+
extension listed in include_extensions.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Dictionary mapping extension names to their configurations.
|
|
500
|
+
"""
|
|
501
|
+
configs = {}
|
|
502
|
+
|
|
503
|
+
for ext_config in self.include_extensions:
|
|
504
|
+
if not isinstance(ext_config, str):
|
|
505
|
+
logger.warning("Extension must be a string name, got: %s", ext_config)
|
|
506
|
+
continue
|
|
507
|
+
|
|
508
|
+
ext_name = ext_config
|
|
509
|
+
ext_options = getattr(self.config, "extension_config", {}).get(ext_name, {})
|
|
510
|
+
configs[ext_name] = ext_options
|
|
511
|
+
|
|
512
|
+
return configs
|
|
513
|
+
|
|
514
|
+
def _discover_extension_migrations(self) -> "dict[str, Path]":
|
|
515
|
+
"""Discover migration paths for configured extensions.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
Dictionary mapping extension names to their migration paths.
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
extension_migrations = {}
|
|
522
|
+
|
|
523
|
+
for ext_name in self.extension_configs:
|
|
524
|
+
module_name = "sqlspec.extensions.litestar" if ext_name == "litestar" else f"sqlspec.extensions.{ext_name}"
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
module_path = module_to_os_path(module_name)
|
|
528
|
+
migrations_dir = module_path / "migrations"
|
|
529
|
+
|
|
530
|
+
if migrations_dir.exists():
|
|
531
|
+
extension_migrations[ext_name] = migrations_dir
|
|
532
|
+
logger.debug("Found migrations for extension %s at %s", ext_name, migrations_dir)
|
|
533
|
+
else:
|
|
534
|
+
logger.warning("No migrations directory found for extension %s", ext_name)
|
|
535
|
+
except TypeError:
|
|
536
|
+
logger.warning("Extension %s not found", ext_name)
|
|
537
|
+
|
|
538
|
+
return extension_migrations
|
|
307
539
|
|
|
308
540
|
def _get_init_readme_content(self) -> str:
|
|
309
541
|
"""Get README content for migration directory initialization.
|
|
@@ -320,13 +552,13 @@ This directory contains database migration files.
|
|
|
320
552
|
Migration files use SQLFileLoader's named query syntax with versioned names:
|
|
321
553
|
|
|
322
554
|
```sql
|
|
323
|
-
-- name: migrate-
|
|
555
|
+
-- name: migrate-20251011120000-up
|
|
324
556
|
CREATE TABLE example (
|
|
325
557
|
id INTEGER PRIMARY KEY,
|
|
326
558
|
name TEXT NOT NULL
|
|
327
559
|
);
|
|
328
560
|
|
|
329
|
-
-- name: migrate-
|
|
561
|
+
-- name: migrate-20251011120000-down
|
|
330
562
|
DROP TABLE example;
|
|
331
563
|
```
|
|
332
564
|
|
|
@@ -336,16 +568,48 @@ DROP TABLE example;
|
|
|
336
568
|
|
|
337
569
|
Format: `{version}_{description}.sql`
|
|
338
570
|
|
|
339
|
-
- Version:
|
|
571
|
+
- Version: Timestamp in YYYYMMDDHHmmss format (UTC)
|
|
340
572
|
- Description: Brief description using underscores
|
|
341
|
-
- Example: `
|
|
573
|
+
- Example: `20251011120000_create_users_table.sql`
|
|
342
574
|
|
|
343
575
|
### Query Names
|
|
344
576
|
|
|
345
577
|
- Upgrade: `migrate-{version}-up`
|
|
346
578
|
- Downgrade: `migrate-{version}-down`
|
|
347
579
|
|
|
348
|
-
|
|
580
|
+
## Version Format
|
|
581
|
+
|
|
582
|
+
Migrations use **timestamp-based versioning** (YYYYMMDDHHmmss):
|
|
583
|
+
|
|
584
|
+
- **Format**: 14-digit UTC timestamp
|
|
585
|
+
- **Example**: `20251011120000` (October 11, 2025 at 12:00:00 UTC)
|
|
586
|
+
- **Benefits**: Eliminates merge conflicts when multiple developers create migrations concurrently
|
|
587
|
+
|
|
588
|
+
### Creating Migrations
|
|
589
|
+
|
|
590
|
+
Use the CLI to generate timestamped migrations:
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
sqlspec create-migration "add user table"
|
|
594
|
+
# Creates: 20251011120000_add_user_table.sql
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
The timestamp is automatically generated in UTC timezone.
|
|
598
|
+
|
|
599
|
+
## Migration Execution
|
|
600
|
+
|
|
601
|
+
Migrations are applied in chronological order based on their timestamps.
|
|
602
|
+
The database tracks both version and execution order separately to handle
|
|
603
|
+
out-of-order migrations gracefully (e.g., from late-merging branches).
|
|
604
|
+
"""
|
|
605
|
+
|
|
606
|
+
def _get_init_init_content(self) -> str:
|
|
607
|
+
"""Get __init__.py content for migration directory initialization.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
Python module docstring content for the __init__.py file.
|
|
611
|
+
"""
|
|
612
|
+
return """Migrations.
|
|
349
613
|
"""
|
|
350
614
|
|
|
351
615
|
def init_directory(self, directory: str, package: bool = True) -> None:
|
|
@@ -363,13 +627,12 @@ This naming ensures proper sorting and avoids conflicts when loading multiple fi
|
|
|
363
627
|
migrations_dir.mkdir(parents=True, exist_ok=True)
|
|
364
628
|
|
|
365
629
|
if package:
|
|
366
|
-
|
|
630
|
+
init = migrations_dir / "__init__.py"
|
|
631
|
+
init.write_text(self._get_init_init_content())
|
|
367
632
|
|
|
368
633
|
readme = migrations_dir / "README.md"
|
|
369
634
|
readme.write_text(self._get_init_readme_content())
|
|
370
635
|
|
|
371
|
-
(migrations_dir / ".gitkeep").touch()
|
|
372
|
-
|
|
373
636
|
console.print(f"[green]Initialized migrations in {directory}[/]")
|
|
374
637
|
|
|
375
638
|
@abstractmethod
|