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.

Files changed (197) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +62 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +52 -2
  9. sqlspec/adapters/adbc/driver.py +144 -45
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +25 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +352 -144
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +138 -43
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +292 -84
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
  77. sqlspec/adapters/psqlpy/driver.py +101 -31
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +40 -11
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +77 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +231 -60
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +95 -161
  129. sqlspec/driver/_common.py +133 -80
  130. sqlspec/driver/_sync.py +95 -162
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/base.py +200 -76
  153. sqlspec/migrations/commands.py +591 -62
  154. sqlspec/migrations/context.py +6 -9
  155. sqlspec/migrations/fix.py +199 -0
  156. sqlspec/migrations/loaders.py +47 -19
  157. sqlspec/migrations/runner.py +241 -75
  158. sqlspec/migrations/tracker.py +237 -21
  159. sqlspec/migrations/utils.py +51 -3
  160. sqlspec/migrations/validation.py +177 -0
  161. sqlspec/protocols.py +66 -36
  162. sqlspec/storage/_utils.py +98 -0
  163. sqlspec/storage/backends/fsspec.py +134 -106
  164. sqlspec/storage/backends/local.py +78 -51
  165. sqlspec/storage/backends/obstore.py +278 -162
  166. sqlspec/storage/registry.py +75 -39
  167. sqlspec/typing.py +14 -84
  168. sqlspec/utils/config_resolver.py +6 -6
  169. sqlspec/utils/correlation.py +4 -5
  170. sqlspec/utils/data_transformation.py +3 -2
  171. sqlspec/utils/deprecation.py +9 -8
  172. sqlspec/utils/fixtures.py +4 -4
  173. sqlspec/utils/logging.py +46 -6
  174. sqlspec/utils/module_loader.py +2 -2
  175. sqlspec/utils/schema.py +288 -0
  176. sqlspec/utils/serializers.py +3 -3
  177. sqlspec/utils/sync_tools.py +21 -17
  178. sqlspec/utils/text.py +1 -2
  179. sqlspec/utils/type_guards.py +111 -20
  180. sqlspec/utils/version.py +433 -0
  181. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  182. sqlspec-0.27.0.dist-info/RECORD +207 -0
  183. sqlspec/builder/mixins/__init__.py +0 -55
  184. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  185. sqlspec/builder/mixins/_delete_operations.py +0 -50
  186. sqlspec/builder/mixins/_insert_operations.py +0 -282
  187. sqlspec/builder/mixins/_merge_operations.py +0 -698
  188. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  189. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  190. sqlspec/builder/mixins/_select_operations.py +0 -930
  191. sqlspec/builder/mixins/_update_operations.py +0 -199
  192. sqlspec/builder/mixins/_where_clause.py +0 -1298
  193. sqlspec-0.26.0.dist-info/RECORD +0 -157
  194. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  195. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  197. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -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, Optional, TypeVar, cast
9
+ from typing import Any, Generic, TypeVar, cast
11
10
 
12
- from sqlspec._sql import sql
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("version_num DESC").limit(1)
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("version_num")
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, version: str, description: str, execution_time_ms: int, checksum: str, applied_by: str
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("version_num", "description", "execution_time_ms", "checksum", "applied_by")
94
- .values(version, description, execution_time_ms, checksum, applied_by)
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: "Optional[dict[str, Path]]" = None,
145
- context: "Optional[Any]" = None,
146
- extension_configs: "Optional[dict[str, dict[str, Any]]]" = None,
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: Optional[Path] = None
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) -> Optional[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
- # Handle extension-prefixed versions (e.g., "ext_litestar_0001")
173
- if filename.startswith("ext_"):
174
- # This is already a prefixed version, return as-is
175
- return filename
295
+ from pathlib import Path
296
+
297
+ stem = Path(filename).stem
176
298
 
177
- # Regular version extraction
178
- parts = filename.split("_", 1)
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=operator.itemgetter(0))
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
- if context_to_use and file_path.name.startswith("ext_"):
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) -> "Optional[list[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
- Supports both string format (extension name) and dict format
389
- (extension name with configuration).
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
- # Simple string format: just the extension name
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
- # Apply default configurations for known extensions
416
- if ext_name == "litestar" and "session_table" not in ext_options:
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-0001-up
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-0001-down
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: Zero-padded 4-digit number (0001, 0002, etc.)
571
+ - Version: Timestamp in YYYYMMDDHHmmss format (UTC)
481
572
  - Description: Brief description using underscores
482
- - Example: `0001_create_users_table.sql`
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
- This naming ensures proper sorting and avoids conflicts when loading multiple files.
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
- (migrations_dir / "__init__.py").touch()
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())