sqlspec 0.26.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 +55 -25
- sqlspec/_typing.py +62 -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 +62 -12
- sqlspec/adapters/adbc/data_dictionary.py +52 -2
- sqlspec/adapters/adbc/driver.py +144 -45
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +44 -50
- 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 +86 -16
- sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
- sqlspec/adapters/aiosqlite/driver.py +127 -38
- 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 +1 -1
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +59 -17
- sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
- sqlspec/adapters/asyncmy/driver.py +293 -62
- 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 +57 -36
- sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
- sqlspec/adapters/asyncpg/driver.py +153 -23
- 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 +25 -11
- sqlspec/adapters/bigquery/data_dictionary.py +42 -2
- sqlspec/adapters/bigquery/driver.py +352 -144
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +55 -23
- sqlspec/adapters/duckdb/_types.py +2 -2
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +79 -21
- sqlspec/adapters/duckdb/data_dictionary.py +41 -2
- sqlspec/adapters/duckdb/driver.py +138 -43
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +5 -5
- sqlspec/adapters/duckdb/type_converter.py +51 -21
- 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 +120 -36
- sqlspec/adapters/oracledb/data_dictionary.py +87 -20
- sqlspec/adapters/oracledb/driver.py +292 -84
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +316 -25
- sqlspec/adapters/oracledb/type_converter.py +91 -16
- 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 +45 -19
- sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
- sqlspec/adapters/psqlpy/driver.py +101 -31
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +40 -11
- 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 +65 -37
- sqlspec/adapters/psycopg/data_dictionary.py +77 -3
- sqlspec/adapters/psycopg/driver.py +200 -78
- 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 +85 -16
- sqlspec/adapters/sqlite/data_dictionary.py +34 -2
- sqlspec/adapters/sqlite/driver.py +120 -52
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +5 -5
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +91 -58
- sqlspec/builder/_column.py +5 -5
- sqlspec/builder/_ddl.py +98 -89
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +41 -44
- sqlspec/builder/_insert.py +5 -82
- sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +9 -11
- sqlspec/builder/_select.py +1313 -25
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +76 -69
- sqlspec/config.py +231 -60
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +18 -18
- sqlspec/core/compiler.py +6 -8
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +76 -45
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +32 -31
- sqlspec/core/type_conversion.py +3 -2
- sqlspec/driver/__init__.py +1 -3
- sqlspec/driver/_async.py +95 -161
- sqlspec/driver/_common.py +133 -80
- sqlspec/driver/_sync.py +95 -162
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +70 -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/base.py +200 -76
- sqlspec/migrations/commands.py +591 -62
- sqlspec/migrations/context.py +6 -9
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +47 -19
- sqlspec/migrations/runner.py +241 -75
- 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 +14 -84
- sqlspec/utils/config_resolver.py +6 -6
- 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 +3 -3
- 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.26.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 -253
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_merge_operations.py +0 -698
- sqlspec/builder/mixins/_order_limit_operations.py +0 -145
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -930
- sqlspec/builder/mixins/_update_operations.py +0 -199
- sqlspec/builder/mixins/_where_clause.py +0 -1298
- sqlspec-0.26.0.dist-info/RECORD +0 -157
- sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
sqlspec/migrations/base.py
CHANGED
|
@@ -4,19 +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
|
|
18
16
|
from sqlspec.utils.module_loader import module_to_os_path
|
|
19
17
|
from sqlspec.utils.sync_tools import await_
|
|
18
|
+
from sqlspec.utils.version import parse_version
|
|
20
19
|
|
|
21
20
|
__all__ = ("BaseMigrationCommands", "BaseMigrationRunner", "BaseMigrationTracker")
|
|
22
21
|
|
|
@@ -43,6 +42,16 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
43
42
|
def _get_create_table_sql(self) -> CreateTable:
|
|
44
43
|
"""Get SQL builder for creating the tracking table.
|
|
45
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
|
+
|
|
46
55
|
Returns:
|
|
47
56
|
SQL builder object for table creation.
|
|
48
57
|
"""
|
|
@@ -50,6 +59,8 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
50
59
|
sql.create_table(self.version_table)
|
|
51
60
|
.if_not_exists()
|
|
52
61
|
.column("version_num", "VARCHAR(32)", primary_key=True)
|
|
62
|
+
.column("version_type", "VARCHAR(16)")
|
|
63
|
+
.column("execution_sequence", "INTEGER")
|
|
53
64
|
.column("description", "TEXT")
|
|
54
65
|
.column("applied_at", "TIMESTAMP", default="CURRENT_TIMESTAMP", not_null=True)
|
|
55
66
|
.column("execution_time_ms", "INTEGER")
|
|
@@ -60,26 +71,49 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
60
71
|
def _get_current_version_sql(self) -> Select:
|
|
61
72
|
"""Get SQL builder for retrieving current version.
|
|
62
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
|
+
|
|
63
77
|
Returns:
|
|
64
78
|
SQL builder object for version query.
|
|
65
79
|
"""
|
|
66
|
-
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)
|
|
67
81
|
|
|
68
82
|
def _get_applied_migrations_sql(self) -> Select:
|
|
69
83
|
"""Get SQL builder for retrieving all applied migrations.
|
|
70
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
|
+
|
|
71
88
|
Returns:
|
|
72
89
|
SQL builder object for migrations query.
|
|
73
90
|
"""
|
|
74
|
-
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)
|
|
75
100
|
|
|
76
101
|
def _get_record_migration_sql(
|
|
77
|
-
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,
|
|
78
110
|
) -> Insert:
|
|
79
111
|
"""Get SQL builder for recording a migration.
|
|
80
112
|
|
|
81
113
|
Args:
|
|
82
114
|
version: Version number of the migration.
|
|
115
|
+
version_type: Version format type ('sequential' or 'timestamp').
|
|
116
|
+
execution_sequence: Auto-incrementing application order.
|
|
83
117
|
description: Description of the migration.
|
|
84
118
|
execution_time_ms: Execution time in milliseconds.
|
|
85
119
|
checksum: MD5 checksum of the migration content.
|
|
@@ -90,8 +124,16 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
90
124
|
"""
|
|
91
125
|
return (
|
|
92
126
|
sql.insert(self.version_table)
|
|
93
|
-
.columns(
|
|
94
|
-
|
|
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)
|
|
95
137
|
)
|
|
96
138
|
|
|
97
139
|
def _get_remove_migration_sql(self, version: str) -> Delete:
|
|
@@ -105,9 +147,90 @@ class BaseMigrationTracker(ABC, Generic[DriverT]):
|
|
|
105
147
|
"""
|
|
106
148
|
return sql.delete().from_(self.version_table).where(sql.version_num == version)
|
|
107
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
|
+
|
|
108
227
|
@abstractmethod
|
|
109
228
|
def ensure_tracking_table(self, driver: DriverT) -> Any:
|
|
110
|
-
"""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
|
+
"""
|
|
111
234
|
...
|
|
112
235
|
|
|
113
236
|
@abstractmethod
|
|
@@ -141,9 +264,9 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
141
264
|
def __init__(
|
|
142
265
|
self,
|
|
143
266
|
migrations_path: Path,
|
|
144
|
-
extension_migrations: "
|
|
145
|
-
context: "
|
|
146
|
-
extension_configs: "
|
|
267
|
+
extension_migrations: "dict[str, Path] | None" = None,
|
|
268
|
+
context: "Any | None" = None,
|
|
269
|
+
extension_configs: "dict[str, dict[str, Any]] | None" = None,
|
|
147
270
|
) -> None:
|
|
148
271
|
"""Initialize the migration runner.
|
|
149
272
|
|
|
@@ -156,11 +279,11 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
156
279
|
self.migrations_path = migrations_path
|
|
157
280
|
self.extension_migrations = extension_migrations or {}
|
|
158
281
|
self.loader = SQLFileLoader()
|
|
159
|
-
self.project_root:
|
|
282
|
+
self.project_root: Path | None = None
|
|
160
283
|
self.context = context
|
|
161
284
|
self.extension_configs = extension_configs or {}
|
|
162
285
|
|
|
163
|
-
def _extract_version(self, filename: str) ->
|
|
286
|
+
def _extract_version(self, filename: str) -> str | None:
|
|
164
287
|
"""Extract version from filename.
|
|
165
288
|
|
|
166
289
|
Args:
|
|
@@ -169,13 +292,14 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
169
292
|
Returns:
|
|
170
293
|
The extracted version string or None.
|
|
171
294
|
"""
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return filename
|
|
295
|
+
from pathlib import Path
|
|
296
|
+
|
|
297
|
+
stem = Path(filename).stem
|
|
176
298
|
|
|
177
|
-
|
|
178
|
-
|
|
299
|
+
if stem.startswith("ext_"):
|
|
300
|
+
return stem
|
|
301
|
+
|
|
302
|
+
parts = stem.split("_", 1)
|
|
179
303
|
return parts[0].zfill(4) if parts and parts[0].isdigit() else None
|
|
180
304
|
|
|
181
305
|
def _calculate_checksum(self, content: str) -> str:
|
|
@@ -193,6 +317,9 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
193
317
|
def _get_migration_files_sync(self) -> "list[tuple[str, Path]]":
|
|
194
318
|
"""Get all migration files sorted by version.
|
|
195
319
|
|
|
320
|
+
Uses version-aware sorting that handles both sequential and timestamp
|
|
321
|
+
formats correctly, with extension migrations sorted by extension name.
|
|
322
|
+
|
|
196
323
|
Returns:
|
|
197
324
|
List of tuples containing (version, file_path).
|
|
198
325
|
"""
|
|
@@ -222,42 +349,23 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
222
349
|
prefixed_version = f"ext_{ext_name}_{version}"
|
|
223
350
|
migrations.append((prefixed_version, file_path))
|
|
224
351
|
|
|
225
|
-
return sorted(migrations, key=
|
|
352
|
+
return sorted(migrations, key=lambda m: parse_version(m[0]))
|
|
226
353
|
|
|
227
|
-
def _load_migration_metadata(self, file_path: Path) -> "dict[str, Any]":
|
|
354
|
+
def _load_migration_metadata(self, file_path: Path, version: "str | None" = None) -> "dict[str, Any]":
|
|
228
355
|
"""Load migration metadata from file.
|
|
229
356
|
|
|
230
357
|
Args:
|
|
231
358
|
file_path: Path to the migration file.
|
|
359
|
+
version: Optional pre-extracted version (preserves prefixes like ext_adk_0001).
|
|
232
360
|
|
|
233
361
|
Returns:
|
|
234
362
|
Migration metadata dictionary.
|
|
235
363
|
"""
|
|
364
|
+
if version is None:
|
|
365
|
+
version = self._extract_version(file_path.name)
|
|
236
366
|
|
|
237
|
-
# Check if this is an extension migration and update context accordingly
|
|
238
367
|
context_to_use = self.context
|
|
239
|
-
|
|
240
|
-
# Try to extract extension name from the version
|
|
241
|
-
version = self._extract_version(file_path.name)
|
|
242
|
-
if version and version.startswith("ext_"):
|
|
243
|
-
# Parse extension name from version like "ext_litestar_0001"
|
|
244
|
-
min_extension_version_parts = 3
|
|
245
|
-
parts = version.split("_", 2)
|
|
246
|
-
if len(parts) >= min_extension_version_parts:
|
|
247
|
-
ext_name = parts[1]
|
|
248
|
-
if ext_name in self.extension_configs:
|
|
249
|
-
# Create a new context with the extension config
|
|
250
|
-
from sqlspec.migrations.context import MigrationContext
|
|
251
|
-
|
|
252
|
-
context_to_use = MigrationContext(
|
|
253
|
-
dialect=self.context.dialect if self.context else None,
|
|
254
|
-
config=self.context.config if self.context else None,
|
|
255
|
-
driver=self.context.driver if self.context else None,
|
|
256
|
-
metadata=self.context.metadata.copy() if self.context and self.context.metadata else {},
|
|
257
|
-
extension_config=self.extension_configs[ext_name],
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
# For extension migrations, check by path
|
|
368
|
+
|
|
261
369
|
for ext_name, ext_path in self.extension_migrations.items():
|
|
262
370
|
if file_path.parent == ext_path:
|
|
263
371
|
if ext_name in self.extension_configs and self.context:
|
|
@@ -276,7 +384,6 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
276
384
|
loader.validate_migration_file(file_path)
|
|
277
385
|
content = file_path.read_text(encoding="utf-8")
|
|
278
386
|
checksum = self._calculate_checksum(content)
|
|
279
|
-
version = self._extract_version(file_path.name)
|
|
280
387
|
description = file_path.stem.split("_", 1)[1] if "_" in file_path.stem else ""
|
|
281
388
|
|
|
282
389
|
has_upgrade, has_downgrade = True, False
|
|
@@ -302,7 +409,7 @@ class BaseMigrationRunner(ABC, Generic[DriverT]):
|
|
|
302
409
|
"loader": loader,
|
|
303
410
|
}
|
|
304
411
|
|
|
305
|
-
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":
|
|
306
413
|
"""Get migration SQL for given direction.
|
|
307
414
|
|
|
308
415
|
Args:
|
|
@@ -385,8 +492,8 @@ class BaseMigrationCommands(ABC, Generic[ConfigT, DriverT]):
|
|
|
385
492
|
def _parse_extension_configs(self) -> "dict[str, dict[str, Any]]":
|
|
386
493
|
"""Parse extension configurations from include_extensions.
|
|
387
494
|
|
|
388
|
-
|
|
389
|
-
|
|
495
|
+
Reads extension configuration from config.extension_config for each
|
|
496
|
+
extension listed in include_extensions.
|
|
390
497
|
|
|
391
498
|
Returns:
|
|
392
499
|
Dictionary mapping extension names to their configurations.
|
|
@@ -394,28 +501,12 @@ class BaseMigrationCommands(ABC, Generic[ConfigT, DriverT]):
|
|
|
394
501
|
configs = {}
|
|
395
502
|
|
|
396
503
|
for ext_config in self.include_extensions:
|
|
397
|
-
if isinstance(ext_config, str):
|
|
398
|
-
|
|
399
|
-
ext_name = ext_config
|
|
400
|
-
ext_options = {}
|
|
401
|
-
elif isinstance(ext_config, dict):
|
|
402
|
-
# Dict format: {"name": "litestar", "session_table": "custom_sessions"}
|
|
403
|
-
ext_name_raw = ext_config.get("name")
|
|
404
|
-
if not ext_name_raw:
|
|
405
|
-
logger.warning("Extension configuration missing 'name' field: %s", ext_config)
|
|
406
|
-
continue
|
|
407
|
-
# Assert for type narrowing: ext_name_raw is guaranteed to be str here
|
|
408
|
-
assert isinstance(ext_name_raw, str)
|
|
409
|
-
ext_name = ext_name_raw
|
|
410
|
-
ext_options = {k: v for k, v in ext_config.items() if k != "name"}
|
|
411
|
-
else:
|
|
412
|
-
logger.warning("Invalid extension configuration format: %s", ext_config)
|
|
504
|
+
if not isinstance(ext_config, str):
|
|
505
|
+
logger.warning("Extension must be a string name, got: %s", ext_config)
|
|
413
506
|
continue
|
|
414
507
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
ext_options["session_table"] = "litestar_sessions"
|
|
418
|
-
|
|
508
|
+
ext_name = ext_config
|
|
509
|
+
ext_options = getattr(self.config, "extension_config", {}).get(ext_name, {})
|
|
419
510
|
configs[ext_name] = ext_options
|
|
420
511
|
|
|
421
512
|
return configs
|
|
@@ -461,13 +552,13 @@ This directory contains database migration files.
|
|
|
461
552
|
Migration files use SQLFileLoader's named query syntax with versioned names:
|
|
462
553
|
|
|
463
554
|
```sql
|
|
464
|
-
-- name: migrate-
|
|
555
|
+
-- name: migrate-20251011120000-up
|
|
465
556
|
CREATE TABLE example (
|
|
466
557
|
id INTEGER PRIMARY KEY,
|
|
467
558
|
name TEXT NOT NULL
|
|
468
559
|
);
|
|
469
560
|
|
|
470
|
-
-- name: migrate-
|
|
561
|
+
-- name: migrate-20251011120000-down
|
|
471
562
|
DROP TABLE example;
|
|
472
563
|
```
|
|
473
564
|
|
|
@@ -477,16 +568,48 @@ DROP TABLE example;
|
|
|
477
568
|
|
|
478
569
|
Format: `{version}_{description}.sql`
|
|
479
570
|
|
|
480
|
-
- Version:
|
|
571
|
+
- Version: Timestamp in YYYYMMDDHHmmss format (UTC)
|
|
481
572
|
- Description: Brief description using underscores
|
|
482
|
-
- Example: `
|
|
573
|
+
- Example: `20251011120000_create_users_table.sql`
|
|
483
574
|
|
|
484
575
|
### Query Names
|
|
485
576
|
|
|
486
577
|
- Upgrade: `migrate-{version}-up`
|
|
487
578
|
- Downgrade: `migrate-{version}-down`
|
|
488
579
|
|
|
489
|
-
|
|
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.
|
|
490
613
|
"""
|
|
491
614
|
|
|
492
615
|
def init_directory(self, directory: str, package: bool = True) -> None:
|
|
@@ -504,7 +627,8 @@ This naming ensures proper sorting and avoids conflicts when loading multiple fi
|
|
|
504
627
|
migrations_dir.mkdir(parents=True, exist_ok=True)
|
|
505
628
|
|
|
506
629
|
if package:
|
|
507
|
-
|
|
630
|
+
init = migrations_dir / "__init__.py"
|
|
631
|
+
init.write_text(self._get_init_init_content())
|
|
508
632
|
|
|
509
633
|
readme = migrations_dir / "README.md"
|
|
510
634
|
readme.write_text(self._get_init_readme_content())
|