sqlspec 0.26.0__py3-none-any.whl → 0.28.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 (212) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +155 -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 +880 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +74 -2
  9. sqlspec/adapters/adbc/driver.py +226 -58
  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 +536 -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 +503 -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 +460 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +48 -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 +585 -0
  44. sqlspec/adapters/bigquery/config.py +36 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +489 -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 +563 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +225 -44
  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 +1628 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +475 -86
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +765 -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 +483 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +48 -2
  77. sqlspec/adapters/psqlpy/driver.py +108 -41
  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 +962 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +91 -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 +582 -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 +331 -62
  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 +55 -47
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +234 -47
  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 +183 -160
  129. sqlspec/driver/_common.py +197 -109
  130. sqlspec/driver/_sync.py +189 -161
  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 +69 -61
  142. sqlspec/extensions/fastapi/__init__.py +21 -0
  143. sqlspec/extensions/fastapi/extension.py +331 -0
  144. sqlspec/extensions/fastapi/providers.py +543 -0
  145. sqlspec/extensions/flask/__init__.py +36 -0
  146. sqlspec/extensions/flask/_state.py +71 -0
  147. sqlspec/extensions/flask/_utils.py +40 -0
  148. sqlspec/extensions/flask/extension.py +389 -0
  149. sqlspec/extensions/litestar/__init__.py +21 -4
  150. sqlspec/extensions/litestar/cli.py +54 -10
  151. sqlspec/extensions/litestar/config.py +56 -266
  152. sqlspec/extensions/litestar/handlers.py +46 -17
  153. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  154. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  155. sqlspec/extensions/litestar/plugin.py +349 -224
  156. sqlspec/extensions/litestar/providers.py +25 -25
  157. sqlspec/extensions/litestar/store.py +265 -0
  158. sqlspec/extensions/starlette/__init__.py +10 -0
  159. sqlspec/extensions/starlette/_state.py +25 -0
  160. sqlspec/extensions/starlette/_utils.py +52 -0
  161. sqlspec/extensions/starlette/extension.py +254 -0
  162. sqlspec/extensions/starlette/middleware.py +154 -0
  163. sqlspec/loader.py +30 -49
  164. sqlspec/migrations/base.py +200 -76
  165. sqlspec/migrations/commands.py +591 -62
  166. sqlspec/migrations/context.py +6 -9
  167. sqlspec/migrations/fix.py +199 -0
  168. sqlspec/migrations/loaders.py +47 -19
  169. sqlspec/migrations/runner.py +241 -75
  170. sqlspec/migrations/tracker.py +237 -21
  171. sqlspec/migrations/utils.py +51 -3
  172. sqlspec/migrations/validation.py +177 -0
  173. sqlspec/protocols.py +106 -36
  174. sqlspec/storage/_utils.py +85 -0
  175. sqlspec/storage/backends/fsspec.py +133 -107
  176. sqlspec/storage/backends/local.py +78 -51
  177. sqlspec/storage/backends/obstore.py +276 -168
  178. sqlspec/storage/registry.py +75 -39
  179. sqlspec/typing.py +30 -84
  180. sqlspec/utils/__init__.py +25 -4
  181. sqlspec/utils/arrow_helpers.py +81 -0
  182. sqlspec/utils/config_resolver.py +6 -6
  183. sqlspec/utils/correlation.py +4 -5
  184. sqlspec/utils/data_transformation.py +3 -2
  185. sqlspec/utils/deprecation.py +9 -8
  186. sqlspec/utils/fixtures.py +4 -4
  187. sqlspec/utils/logging.py +46 -6
  188. sqlspec/utils/module_loader.py +205 -5
  189. sqlspec/utils/portal.py +311 -0
  190. sqlspec/utils/schema.py +288 -0
  191. sqlspec/utils/serializers.py +113 -4
  192. sqlspec/utils/sync_tools.py +36 -22
  193. sqlspec/utils/text.py +1 -2
  194. sqlspec/utils/type_guards.py +136 -20
  195. sqlspec/utils/version.py +433 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/METADATA +41 -22
  197. sqlspec-0.28.0.dist-info/RECORD +221 -0
  198. sqlspec/builder/mixins/__init__.py +0 -55
  199. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  200. sqlspec/builder/mixins/_delete_operations.py +0 -50
  201. sqlspec/builder/mixins/_insert_operations.py +0 -282
  202. sqlspec/builder/mixins/_merge_operations.py +0 -698
  203. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  204. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  205. sqlspec/builder/mixins/_select_operations.py +0 -930
  206. sqlspec/builder/mixins/_update_operations.py +0 -199
  207. sqlspec/builder/mixins/_where_clause.py +0 -1298
  208. sqlspec-0.26.0.dist-info/RECORD +0 -157
  209. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  210. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/WHEEL +0 -0
  211. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/entry_points.txt +0 -0
  212. {sqlspec-0.26.0.dist-info → sqlspec-0.28.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,433 @@
1
+ """Migration version parsing and comparison utilities.
2
+
3
+ Provides structured parsing of migration versions supporting both legacy sequential
4
+ (0001) and timestamp-based (20251011120000) formats with type-safe comparison.
5
+ """
6
+
7
+ import logging
8
+ import re
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timezone
11
+ from enum import Enum
12
+ from typing import Any
13
+
14
+ __all__ = (
15
+ "MigrationVersion",
16
+ "VersionType",
17
+ "convert_to_sequential_version",
18
+ "generate_conversion_map",
19
+ "generate_timestamp_version",
20
+ "get_next_sequential_number",
21
+ "is_sequential_version",
22
+ "is_timestamp_version",
23
+ "parse_version",
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Regex patterns for version detection
29
+ SEQUENTIAL_PATTERN = re.compile(r"^(?!\d{14}$)(\d+)$")
30
+ TIMESTAMP_PATTERN = re.compile(r"^(\d{14})$")
31
+ EXTENSION_PATTERN = re.compile(r"^ext_(\w+)_(.+)$")
32
+
33
+
34
+ class VersionType(Enum):
35
+ """Migration version format type."""
36
+
37
+ SEQUENTIAL = "sequential"
38
+ TIMESTAMP = "timestamp"
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class MigrationVersion:
43
+ """Parsed migration version with structured comparison support.
44
+
45
+ Attributes:
46
+ raw: Original version string (e.g., "0001", "20251011120000", "ext_litestar_0001").
47
+ type: Version format type (sequential or timestamp).
48
+ sequence: Numeric value for sequential versions (e.g., 1, 2, 42).
49
+ timestamp: Parsed datetime for timestamp versions (UTC).
50
+ extension: Extension name for extension-prefixed versions (e.g., "litestar").
51
+ """
52
+
53
+ raw: str
54
+ type: VersionType
55
+ sequence: "int | None"
56
+ timestamp: "datetime | None"
57
+ extension: "str | None"
58
+
59
+ def __lt__(self, other: "MigrationVersion") -> bool:
60
+ """Compare versions supporting mixed formats.
61
+
62
+ Comparison Rules:
63
+ 1. Extension migrations sort by extension name first, then version
64
+ 2. Sequential < Timestamp (legacy migrations first)
65
+ 3. Sequential vs Sequential: numeric comparison
66
+ 4. Timestamp vs Timestamp: chronological comparison
67
+
68
+ Args:
69
+ other: Version to compare against.
70
+
71
+ Returns:
72
+ True if this version sorts before other.
73
+
74
+ Raises:
75
+ TypeError: If comparing against non-MigrationVersion.
76
+ """
77
+ if not isinstance(other, MigrationVersion):
78
+ return NotImplemented
79
+
80
+ if self.extension != other.extension:
81
+ if self.extension is None:
82
+ return True
83
+ if other.extension is None:
84
+ return False
85
+ return self.extension < other.extension
86
+
87
+ if self.type == other.type:
88
+ if self.type == VersionType.SEQUENTIAL:
89
+ return (self.sequence or 0) < (other.sequence or 0)
90
+ return (self.timestamp or datetime.min.replace(tzinfo=timezone.utc)) < (
91
+ other.timestamp or datetime.min.replace(tzinfo=timezone.utc)
92
+ )
93
+
94
+ return self.type == VersionType.SEQUENTIAL
95
+
96
+ def __le__(self, other: "MigrationVersion") -> bool:
97
+ """Check if version is less than or equal to another.
98
+
99
+ Args:
100
+ other: Version to compare against.
101
+
102
+ Returns:
103
+ True if this version is less than or equal to other.
104
+ """
105
+ return self == other or self < other
106
+
107
+ def __eq__(self, other: object) -> bool:
108
+ """Check version equality.
109
+
110
+ Args:
111
+ other: Version to compare against.
112
+
113
+ Returns:
114
+ True if versions are equal.
115
+ """
116
+ if not isinstance(other, MigrationVersion):
117
+ return NotImplemented
118
+ return self.raw == other.raw
119
+
120
+ def __hash__(self) -> int:
121
+ """Hash version for use in sets and dicts.
122
+
123
+ Returns:
124
+ Hash value based on raw version string.
125
+ """
126
+ return hash(self.raw)
127
+
128
+ def __repr__(self) -> str:
129
+ """Get string representation for debugging.
130
+
131
+ Returns:
132
+ String representation with type and value.
133
+ """
134
+ if self.extension:
135
+ return f"MigrationVersion(ext={self.extension}, {self.type.value}={self.raw})"
136
+ return f"MigrationVersion({self.type.value}={self.raw})"
137
+
138
+
139
+ def is_sequential_version(version_str: str) -> bool:
140
+ """Check if version string is sequential format.
141
+
142
+ Sequential format: Any sequence of digits (0001, 42, 9999, 10000+).
143
+
144
+ Args:
145
+ version_str: Version string to check.
146
+
147
+ Returns:
148
+ True if sequential format.
149
+
150
+ Examples:
151
+ >>> is_sequential_version("0001")
152
+ True
153
+ >>> is_sequential_version("42")
154
+ True
155
+ >>> is_sequential_version("10000")
156
+ True
157
+ >>> is_sequential_version("20251011120000")
158
+ False
159
+ """
160
+ return bool(SEQUENTIAL_PATTERN.match(version_str))
161
+
162
+
163
+ def is_timestamp_version(version_str: str) -> bool:
164
+ """Check if version string is timestamp format.
165
+
166
+ Timestamp format: 14-digit YYYYMMDDHHmmss (20251011120000).
167
+
168
+ Args:
169
+ version_str: Version string to check.
170
+
171
+ Returns:
172
+ True if timestamp format.
173
+
174
+ Examples:
175
+ >>> is_timestamp_version("20251011120000")
176
+ True
177
+ >>> is_timestamp_version("0001")
178
+ False
179
+ """
180
+ if not TIMESTAMP_PATTERN.match(version_str):
181
+ return False
182
+
183
+ try:
184
+ datetime.strptime(version_str, "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
185
+ except ValueError:
186
+ return False
187
+ else:
188
+ return True
189
+
190
+
191
+ def parse_version(version_str: str) -> MigrationVersion:
192
+ """Parse version string into structured format.
193
+
194
+ Supports:
195
+ - Sequential: "0001", "42", "9999"
196
+ - Timestamp: "20251011120000"
197
+ - Extension: "ext_litestar_0001", "ext_litestar_20251011120000"
198
+
199
+ Args:
200
+ version_str: Version string to parse.
201
+
202
+ Returns:
203
+ Parsed migration version.
204
+
205
+ Raises:
206
+ ValueError: If version format is invalid.
207
+
208
+ Examples:
209
+ >>> v = parse_version("0001")
210
+ >>> v.type == VersionType.SEQUENTIAL
211
+ True
212
+ >>> v.sequence
213
+ 1
214
+
215
+ >>> v = parse_version("20251011120000")
216
+ >>> v.type == VersionType.TIMESTAMP
217
+ True
218
+
219
+ >>> v = parse_version("ext_litestar_0001")
220
+ >>> v.extension
221
+ 'litestar'
222
+ """
223
+ extension_match = EXTENSION_PATTERN.match(version_str)
224
+ if extension_match:
225
+ extension_name = extension_match.group(1)
226
+ base_version = extension_match.group(2)
227
+ parsed = parse_version(base_version)
228
+
229
+ return MigrationVersion(
230
+ raw=version_str,
231
+ type=parsed.type,
232
+ sequence=parsed.sequence,
233
+ timestamp=parsed.timestamp,
234
+ extension=extension_name,
235
+ )
236
+
237
+ if is_timestamp_version(version_str):
238
+ dt = datetime.strptime(version_str, "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
239
+ return MigrationVersion(
240
+ raw=version_str, type=VersionType.TIMESTAMP, sequence=None, timestamp=dt, extension=None
241
+ )
242
+
243
+ if is_sequential_version(version_str):
244
+ return MigrationVersion(
245
+ raw=version_str, type=VersionType.SEQUENTIAL, sequence=int(version_str), timestamp=None, extension=None
246
+ )
247
+
248
+ msg = f"Invalid migration version format: {version_str}. Expected sequential (0001) or timestamp (YYYYMMDDHHmmss)."
249
+ raise ValueError(msg)
250
+
251
+
252
+ def generate_timestamp_version() -> str:
253
+ """Generate new timestamp version in UTC.
254
+
255
+ Format: YYYYMMDDHHmmss (14 digits).
256
+
257
+ Returns:
258
+ Timestamp version string.
259
+
260
+ Examples:
261
+ >>> version = generate_timestamp_version()
262
+ >>> len(version)
263
+ 14
264
+ >>> is_timestamp_version(version)
265
+ True
266
+ """
267
+ return datetime.now(tz=timezone.utc).strftime("%Y%m%d%H%M%S")
268
+
269
+
270
+ def get_next_sequential_number(migrations: "list[MigrationVersion]", extension: "str | None" = None) -> int:
271
+ """Find highest sequential number and return next available.
272
+
273
+ Scans migrations for sequential versions and returns the next number in sequence.
274
+ When extension is specified, only that extension's migrations are considered.
275
+ When extension is None, only core (non-extension) migrations are considered.
276
+
277
+ Args:
278
+ migrations: List of parsed migration versions.
279
+ extension: Optional extension name to filter by (e.g., "litestar", "adk").
280
+ None means core migrations only.
281
+
282
+ Returns:
283
+ Next available sequential number (1 if no sequential migrations exist).
284
+
285
+ Examples:
286
+ >>> v1 = parse_version("0001")
287
+ >>> v2 = parse_version("0002")
288
+ >>> get_next_sequential_number([v1, v2])
289
+ 3
290
+
291
+ >>> get_next_sequential_number([])
292
+ 1
293
+
294
+ >>> ext = parse_version("ext_litestar_0001")
295
+ >>> core = parse_version("0001")
296
+ >>> get_next_sequential_number([ext, core])
297
+ 2
298
+
299
+ >>> ext1 = parse_version("ext_litestar_0001")
300
+ >>> get_next_sequential_number([ext1], extension="litestar")
301
+ 2
302
+ """
303
+ sequential = [
304
+ m.sequence for m in migrations if m.type == VersionType.SEQUENTIAL and m.extension == extension and m.sequence
305
+ ]
306
+
307
+ if not sequential:
308
+ return 1
309
+
310
+ return max(sequential) + 1
311
+
312
+
313
+ def convert_to_sequential_version(timestamp_version: MigrationVersion, sequence_number: int) -> str:
314
+ """Convert timestamp MigrationVersion to sequential string format.
315
+
316
+ Preserves extension prefixes during conversion. Format uses zero-padded
317
+ 4-digit numbers (0001, 0002, etc.).
318
+
319
+ Args:
320
+ timestamp_version: Parsed timestamp version to convert.
321
+ sequence_number: Sequential number to assign.
322
+
323
+ Returns:
324
+ Sequential version string with extension prefix if applicable.
325
+
326
+ Raises:
327
+ ValueError: If input is not a timestamp version.
328
+
329
+ Examples:
330
+ >>> v = parse_version("20251011120000")
331
+ >>> convert_to_sequential_version(v, 3)
332
+ '0003'
333
+
334
+ >>> v = parse_version("ext_litestar_20251011120000")
335
+ >>> convert_to_sequential_version(v, 1)
336
+ 'ext_litestar_0001'
337
+
338
+ >>> v = parse_version("0001")
339
+ >>> convert_to_sequential_version(v, 2)
340
+ Traceback (most recent call last):
341
+ ...
342
+ ValueError: Can only convert timestamp versions to sequential
343
+ """
344
+ if timestamp_version.type != VersionType.TIMESTAMP:
345
+ msg = "Can only convert timestamp versions to sequential"
346
+ raise ValueError(msg)
347
+
348
+ seq_str = str(sequence_number).zfill(4)
349
+
350
+ if timestamp_version.extension:
351
+ return f"ext_{timestamp_version.extension}_{seq_str}"
352
+
353
+ return seq_str
354
+
355
+
356
+ def generate_conversion_map(migrations: "list[tuple[str, Any]]") -> "dict[str, str]":
357
+ """Generate mapping from timestamp versions to sequential versions.
358
+
359
+ Separates timestamp migrations from sequential, sorts timestamps chronologically,
360
+ and assigns sequential numbers starting after the highest existing sequential
361
+ number. Extension migrations maintain separate numbering within their namespace.
362
+
363
+ Args:
364
+ migrations: List of tuples (version_string, migration_path).
365
+
366
+ Returns:
367
+ Dictionary mapping old timestamp versions to new sequential versions.
368
+
369
+ Examples:
370
+ >>> migrations = [
371
+ ... ("0001", Path("0001_init.sql")),
372
+ ... ("0002", Path("0002_users.sql")),
373
+ ... ("20251011120000", Path("20251011120000_products.sql")),
374
+ ... ("20251012130000", Path("20251012130000_orders.sql")),
375
+ ... ]
376
+ >>> result = generate_conversion_map(migrations)
377
+ >>> result
378
+ {'20251011120000': '0003', '20251012130000': '0004'}
379
+
380
+ >>> migrations = [
381
+ ... ("20251011120000", Path("20251011120000_first.sql")),
382
+ ... ("20251010090000", Path("20251010090000_earlier.sql")),
383
+ ... ]
384
+ >>> result = generate_conversion_map(migrations)
385
+ >>> result
386
+ {'20251010090000': '0001', '20251011120000': '0002'}
387
+
388
+ >>> migrations = []
389
+ >>> generate_conversion_map(migrations)
390
+ {}
391
+ """
392
+ if not migrations:
393
+ return {}
394
+
395
+ def _try_parse_version(version_str: str) -> "MigrationVersion | None":
396
+ """Parse version string, returning None for invalid versions."""
397
+ try:
398
+ return parse_version(version_str)
399
+ except ValueError:
400
+ logger.warning("Skipping invalid migration version: %s", version_str)
401
+ return None
402
+
403
+ parsed_versions = [v for version_str, _path in migrations if (v := _try_parse_version(version_str)) is not None]
404
+
405
+ timestamp_migrations = sorted([v for v in parsed_versions if v.type == VersionType.TIMESTAMP])
406
+
407
+ if not timestamp_migrations:
408
+ return {}
409
+
410
+ core_timestamps = [m for m in timestamp_migrations if m.extension is None]
411
+ ext_timestamps_by_name: dict[str, list[MigrationVersion]] = {}
412
+ for m in timestamp_migrations:
413
+ if m.extension:
414
+ ext_timestamps_by_name.setdefault(m.extension, []).append(m)
415
+
416
+ conversion_map: dict[str, str] = {}
417
+
418
+ if core_timestamps:
419
+ next_seq = get_next_sequential_number(parsed_versions)
420
+ for timestamp_version in core_timestamps:
421
+ sequential_version = convert_to_sequential_version(timestamp_version, next_seq)
422
+ conversion_map[timestamp_version.raw] = sequential_version
423
+ next_seq += 1
424
+
425
+ for ext_name, ext_migrations in ext_timestamps_by_name.items():
426
+ ext_parsed = [v for v in parsed_versions if v.extension == ext_name]
427
+ next_seq = get_next_sequential_number(ext_parsed, extension=ext_name)
428
+ for timestamp_version in ext_migrations:
429
+ sequential_version = convert_to_sequential_version(timestamp_version, next_seq)
430
+ conversion_map[timestamp_version.raw] = sequential_version
431
+ next_seq += 1
432
+
433
+ return conversion_map
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlspec
3
- Version: 0.26.0
3
+ Version: 0.28.0
4
4
  Summary: SQL Experiments in Python
5
5
  Project-URL: Discord, https://discord.gg/litestar
6
6
  Project-URL: Issue, https://github.com/litestar-org/sqlspec/issues/
@@ -9,9 +9,7 @@ Author-email: Cody Fincher <cody@litestar.dev>
9
9
  Maintainer-email: Litestar Developers <hello@litestar.dev>
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
12
- License-File: NOTICE
13
- Requires-Python: <4.0,>=3.9
14
- Requires-Dist: eval-type-backport; python_version < '3.10'
12
+ Requires-Python: <4.0,>=3.10
15
13
  Requires-Dist: mypy-extensions
16
14
  Requires-Dist: rich-click
17
15
  Requires-Dist: sqlglot>=19.9.0
@@ -19,6 +17,8 @@ Requires-Dist: typing-extensions
19
17
  Provides-Extra: adbc
20
18
  Requires-Dist: adbc-driver-manager; extra == 'adbc'
21
19
  Requires-Dist: pyarrow; extra == 'adbc'
20
+ Provides-Extra: adk
21
+ Requires-Dist: google-adk; extra == 'adk'
22
22
  Provides-Extra: aioodbc
23
23
  Requires-Dist: aioodbc; extra == 'aioodbc'
24
24
  Provides-Extra: aiosql
@@ -103,7 +103,7 @@ SQLSpec is an experimental Python library designed to streamline and modernize y
103
103
  - **Emphasis on RAW SQL and Minimal Abstractions**: SQLSpec is a library for working with SQL in Python. Its goals are to offer minimal abstractions between the user and the database. It does not aim to be an ORM library.
104
104
  - **Type-Safe Queries**: Quickly map SQL queries to typed objects using libraries such as Pydantic, Msgspec, Attrs, etc.
105
105
  - **Extensible Design**: Easily add support for new database dialects or extend existing functionality to meet your specific needs. Easily add support for async and sync database drivers.
106
- - **Minimal Dependencies**: SQLSpec is designed to be lightweight and can run on its own or with other libraries such as `litestar`, `fastapi`, `flask` and more. (Contributions welcome!)
106
+ - **Framework Extensions**: First-class integrations for Litestar, Starlette, and FastAPI with automatic transaction handling and lifecycle management
107
107
  - **Support for Async and Sync Database Drivers**: SQLSpec supports both async and sync database drivers, allowing you to choose the style that best fits your application.
108
108
 
109
109
  ### Experimental Features (API will change rapidly)
@@ -392,27 +392,51 @@ SQLSpec includes a built-in migration system for managing schema changes. After
392
392
 
393
393
  ```bash
394
394
  # Initialize migration directory
395
- sqlspec db init migrations
395
+ sqlspec --config myapp.config init
396
396
 
397
397
  # Generate new migration file
398
- sqlspec db make-migrations "Add user table"
398
+ sqlspec --config myapp.config create-migration -m "Add user table"
399
399
 
400
400
  # Apply all pending migrations
401
- sqlspec db upgrade
401
+ sqlspec --config myapp.config upgrade
402
402
 
403
403
  # Show current migration status
404
- sqlspec db show-current-revision
404
+ sqlspec --config myapp.config show-current-revision
405
405
  ```
406
406
 
407
407
  For Litestar applications, replace `sqlspec` with your application command:
408
408
 
409
409
  ```bash
410
410
  # Using Litestar CLI integration
411
- litestar db make-migrations "Add user table"
412
- litestar db upgrade
413
- litestar db show-current-revision
411
+ litestar database create-migration -m "Add user table"
412
+ litestar database upgrade
413
+ litestar database show-current-revision
414
414
  ```
415
415
 
416
+ ### Shell Completion
417
+
418
+ SQLSpec CLI supports tab completion for bash, zsh, and fish shells. Enable it with:
419
+
420
+ ```bash
421
+ # Bash - add to ~/.bashrc
422
+ eval "$(_SQLSPEC_COMPLETE=bash_source sqlspec)"
423
+
424
+ # Zsh - add to ~/.zshrc
425
+ eval "$(_SQLSPEC_COMPLETE=zsh_source sqlspec)"
426
+
427
+ # Fish - add to ~/.config/fish/completions/sqlspec.fish
428
+ eval (env _SQLSPEC_COMPLETE=fish_source sqlspec)
429
+ ```
430
+
431
+ After setup, you can tab-complete commands and options:
432
+
433
+ ```bash
434
+ sqlspec <TAB> # Shows: create-migration, downgrade, init, ...
435
+ sqlspec upgrade --<TAB> # Shows: --bind-key, --help, --no-prompt, ...
436
+ ```
437
+
438
+ See the [CLI documentation](https://sqlspec.litestar.dev/usage/cli.html) for complete setup instructions.
439
+
416
440
  ### Basic Litestar Integration
417
441
 
418
442
  In this example we demonstrate how to create a basic configuration that integrates into Litestar:
@@ -426,23 +450,18 @@ In this example we demonstrate how to create a basic configuration that integrat
426
450
  # ///
427
451
 
428
452
  from litestar import Litestar, get
429
-
453
+ from sqlspec import SQLSpec
430
454
  from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
431
- from sqlspec.extensions.litestar import DatabaseConfig, SQLSpec
432
-
455
+ from sqlspec.extensions.litestar import SQLSpecPlugin
433
456
 
434
457
  @get("/")
435
458
  async def simple_sqlite(db_session: AiosqliteDriver) -> dict[str, str]:
436
459
  return await db_session.select_one("SELECT 'Hello, world!' AS greeting")
437
460
 
438
461
 
439
- sqlspec = SQLSpec(
440
- config=DatabaseConfig(
441
- config=AiosqliteConfig(pool_config={"database": ":memory:"}), # built in local pooling
442
- commit_mode="autocommit"
443
- )
444
- )
445
- app = Litestar(route_handlers=[simple_sqlite], plugins=[sqlspec])
462
+ sqlspec = SQLSpec()
463
+ sqlspec.add_config(AiosqliteConfig(pool_config={"database": ":memory:"}))
464
+ app = Litestar(route_handlers=[simple_sqlite], plugins=[SQLSpecPlugin(sqlspec)])
446
465
  ```
447
466
 
448
467
  ## Inspiration and Future Direction