sqlspec 0.32.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.
Files changed (262) hide show
  1. sqlspec/__init__.py +104 -0
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +14 -0
  4. sqlspec/_serialization.py +312 -0
  5. sqlspec/_typing.py +784 -0
  6. sqlspec/adapters/__init__.py +0 -0
  7. sqlspec/adapters/adbc/__init__.py +5 -0
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  10. sqlspec/adapters/adbc/adk/store.py +880 -0
  11. sqlspec/adapters/adbc/config.py +436 -0
  12. sqlspec/adapters/adbc/data_dictionary.py +537 -0
  13. sqlspec/adapters/adbc/driver.py +841 -0
  14. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  15. sqlspec/adapters/adbc/litestar/store.py +504 -0
  16. sqlspec/adapters/adbc/type_converter.py +153 -0
  17. sqlspec/adapters/aiosqlite/__init__.py +29 -0
  18. sqlspec/adapters/aiosqlite/_types.py +13 -0
  19. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/adk/store.py +536 -0
  21. sqlspec/adapters/aiosqlite/config.py +310 -0
  22. sqlspec/adapters/aiosqlite/data_dictionary.py +260 -0
  23. sqlspec/adapters/aiosqlite/driver.py +463 -0
  24. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  25. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  26. sqlspec/adapters/aiosqlite/pool.py +500 -0
  27. sqlspec/adapters/asyncmy/__init__.py +25 -0
  28. sqlspec/adapters/asyncmy/_types.py +12 -0
  29. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/adk/store.py +503 -0
  31. sqlspec/adapters/asyncmy/config.py +246 -0
  32. sqlspec/adapters/asyncmy/data_dictionary.py +241 -0
  33. sqlspec/adapters/asyncmy/driver.py +632 -0
  34. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  35. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  36. sqlspec/adapters/asyncpg/__init__.py +23 -0
  37. sqlspec/adapters/asyncpg/_type_handlers.py +76 -0
  38. sqlspec/adapters/asyncpg/_types.py +23 -0
  39. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/adk/store.py +460 -0
  41. sqlspec/adapters/asyncpg/config.py +464 -0
  42. sqlspec/adapters/asyncpg/data_dictionary.py +321 -0
  43. sqlspec/adapters/asyncpg/driver.py +720 -0
  44. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  45. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  46. sqlspec/adapters/bigquery/__init__.py +18 -0
  47. sqlspec/adapters/bigquery/_types.py +12 -0
  48. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  49. sqlspec/adapters/bigquery/adk/store.py +585 -0
  50. sqlspec/adapters/bigquery/config.py +298 -0
  51. sqlspec/adapters/bigquery/data_dictionary.py +256 -0
  52. sqlspec/adapters/bigquery/driver.py +1073 -0
  53. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  54. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  55. sqlspec/adapters/bigquery/type_converter.py +125 -0
  56. sqlspec/adapters/duckdb/__init__.py +24 -0
  57. sqlspec/adapters/duckdb/_types.py +12 -0
  58. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  59. sqlspec/adapters/duckdb/adk/store.py +563 -0
  60. sqlspec/adapters/duckdb/config.py +396 -0
  61. sqlspec/adapters/duckdb/data_dictionary.py +264 -0
  62. sqlspec/adapters/duckdb/driver.py +604 -0
  63. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  64. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  65. sqlspec/adapters/duckdb/pool.py +273 -0
  66. sqlspec/adapters/duckdb/type_converter.py +133 -0
  67. sqlspec/adapters/oracledb/__init__.py +32 -0
  68. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  69. sqlspec/adapters/oracledb/_types.py +39 -0
  70. sqlspec/adapters/oracledb/_uuid_handlers.py +130 -0
  71. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  72. sqlspec/adapters/oracledb/adk/store.py +1632 -0
  73. sqlspec/adapters/oracledb/config.py +469 -0
  74. sqlspec/adapters/oracledb/data_dictionary.py +717 -0
  75. sqlspec/adapters/oracledb/driver.py +1493 -0
  76. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  77. sqlspec/adapters/oracledb/litestar/store.py +765 -0
  78. sqlspec/adapters/oracledb/migrations.py +532 -0
  79. sqlspec/adapters/oracledb/type_converter.py +207 -0
  80. sqlspec/adapters/psqlpy/__init__.py +16 -0
  81. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  82. sqlspec/adapters/psqlpy/_types.py +12 -0
  83. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  84. sqlspec/adapters/psqlpy/adk/store.py +483 -0
  85. sqlspec/adapters/psqlpy/config.py +271 -0
  86. sqlspec/adapters/psqlpy/data_dictionary.py +179 -0
  87. sqlspec/adapters/psqlpy/driver.py +892 -0
  88. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  90. sqlspec/adapters/psqlpy/type_converter.py +102 -0
  91. sqlspec/adapters/psycopg/__init__.py +32 -0
  92. sqlspec/adapters/psycopg/_type_handlers.py +90 -0
  93. sqlspec/adapters/psycopg/_types.py +18 -0
  94. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  95. sqlspec/adapters/psycopg/adk/store.py +962 -0
  96. sqlspec/adapters/psycopg/config.py +487 -0
  97. sqlspec/adapters/psycopg/data_dictionary.py +630 -0
  98. sqlspec/adapters/psycopg/driver.py +1336 -0
  99. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  100. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  101. sqlspec/adapters/spanner/__init__.py +38 -0
  102. sqlspec/adapters/spanner/_type_handlers.py +186 -0
  103. sqlspec/adapters/spanner/_types.py +12 -0
  104. sqlspec/adapters/spanner/adk/__init__.py +5 -0
  105. sqlspec/adapters/spanner/adk/store.py +435 -0
  106. sqlspec/adapters/spanner/config.py +241 -0
  107. sqlspec/adapters/spanner/data_dictionary.py +95 -0
  108. sqlspec/adapters/spanner/dialect/__init__.py +6 -0
  109. sqlspec/adapters/spanner/dialect/_spangres.py +52 -0
  110. sqlspec/adapters/spanner/dialect/_spanner.py +123 -0
  111. sqlspec/adapters/spanner/driver.py +366 -0
  112. sqlspec/adapters/spanner/litestar/__init__.py +5 -0
  113. sqlspec/adapters/spanner/litestar/store.py +266 -0
  114. sqlspec/adapters/spanner/type_converter.py +46 -0
  115. sqlspec/adapters/sqlite/__init__.py +18 -0
  116. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  117. sqlspec/adapters/sqlite/_types.py +11 -0
  118. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  119. sqlspec/adapters/sqlite/adk/store.py +582 -0
  120. sqlspec/adapters/sqlite/config.py +221 -0
  121. sqlspec/adapters/sqlite/data_dictionary.py +256 -0
  122. sqlspec/adapters/sqlite/driver.py +527 -0
  123. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  124. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  125. sqlspec/adapters/sqlite/pool.py +140 -0
  126. sqlspec/base.py +811 -0
  127. sqlspec/builder/__init__.py +146 -0
  128. sqlspec/builder/_base.py +900 -0
  129. sqlspec/builder/_column.py +517 -0
  130. sqlspec/builder/_ddl.py +1642 -0
  131. sqlspec/builder/_delete.py +84 -0
  132. sqlspec/builder/_dml.py +381 -0
  133. sqlspec/builder/_expression_wrappers.py +46 -0
  134. sqlspec/builder/_factory.py +1537 -0
  135. sqlspec/builder/_insert.py +315 -0
  136. sqlspec/builder/_join.py +375 -0
  137. sqlspec/builder/_merge.py +848 -0
  138. sqlspec/builder/_parsing_utils.py +297 -0
  139. sqlspec/builder/_select.py +1615 -0
  140. sqlspec/builder/_update.py +161 -0
  141. sqlspec/builder/_vector_expressions.py +259 -0
  142. sqlspec/cli.py +764 -0
  143. sqlspec/config.py +1540 -0
  144. sqlspec/core/__init__.py +305 -0
  145. sqlspec/core/cache.py +785 -0
  146. sqlspec/core/compiler.py +603 -0
  147. sqlspec/core/filters.py +872 -0
  148. sqlspec/core/hashing.py +274 -0
  149. sqlspec/core/metrics.py +83 -0
  150. sqlspec/core/parameters/__init__.py +64 -0
  151. sqlspec/core/parameters/_alignment.py +266 -0
  152. sqlspec/core/parameters/_converter.py +413 -0
  153. sqlspec/core/parameters/_processor.py +341 -0
  154. sqlspec/core/parameters/_registry.py +201 -0
  155. sqlspec/core/parameters/_transformers.py +226 -0
  156. sqlspec/core/parameters/_types.py +430 -0
  157. sqlspec/core/parameters/_validator.py +123 -0
  158. sqlspec/core/pipeline.py +187 -0
  159. sqlspec/core/result.py +1124 -0
  160. sqlspec/core/splitter.py +940 -0
  161. sqlspec/core/stack.py +163 -0
  162. sqlspec/core/statement.py +835 -0
  163. sqlspec/core/type_conversion.py +235 -0
  164. sqlspec/driver/__init__.py +36 -0
  165. sqlspec/driver/_async.py +1027 -0
  166. sqlspec/driver/_common.py +1236 -0
  167. sqlspec/driver/_sync.py +1025 -0
  168. sqlspec/driver/mixins/__init__.py +7 -0
  169. sqlspec/driver/mixins/_result_tools.py +61 -0
  170. sqlspec/driver/mixins/_sql_translator.py +122 -0
  171. sqlspec/driver/mixins/_storage.py +311 -0
  172. sqlspec/exceptions.py +321 -0
  173. sqlspec/extensions/__init__.py +0 -0
  174. sqlspec/extensions/adk/__init__.py +53 -0
  175. sqlspec/extensions/adk/_types.py +51 -0
  176. sqlspec/extensions/adk/converters.py +172 -0
  177. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  178. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  179. sqlspec/extensions/adk/service.py +181 -0
  180. sqlspec/extensions/adk/store.py +536 -0
  181. sqlspec/extensions/aiosql/__init__.py +10 -0
  182. sqlspec/extensions/aiosql/adapter.py +471 -0
  183. sqlspec/extensions/fastapi/__init__.py +19 -0
  184. sqlspec/extensions/fastapi/extension.py +341 -0
  185. sqlspec/extensions/fastapi/providers.py +543 -0
  186. sqlspec/extensions/flask/__init__.py +36 -0
  187. sqlspec/extensions/flask/_state.py +72 -0
  188. sqlspec/extensions/flask/_utils.py +40 -0
  189. sqlspec/extensions/flask/extension.py +402 -0
  190. sqlspec/extensions/litestar/__init__.py +23 -0
  191. sqlspec/extensions/litestar/_utils.py +52 -0
  192. sqlspec/extensions/litestar/cli.py +92 -0
  193. sqlspec/extensions/litestar/config.py +90 -0
  194. sqlspec/extensions/litestar/handlers.py +316 -0
  195. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  196. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  197. sqlspec/extensions/litestar/plugin.py +638 -0
  198. sqlspec/extensions/litestar/providers.py +454 -0
  199. sqlspec/extensions/litestar/store.py +265 -0
  200. sqlspec/extensions/otel/__init__.py +58 -0
  201. sqlspec/extensions/prometheus/__init__.py +107 -0
  202. sqlspec/extensions/starlette/__init__.py +10 -0
  203. sqlspec/extensions/starlette/_state.py +26 -0
  204. sqlspec/extensions/starlette/_utils.py +52 -0
  205. sqlspec/extensions/starlette/extension.py +257 -0
  206. sqlspec/extensions/starlette/middleware.py +154 -0
  207. sqlspec/loader.py +716 -0
  208. sqlspec/migrations/__init__.py +36 -0
  209. sqlspec/migrations/base.py +728 -0
  210. sqlspec/migrations/commands.py +1140 -0
  211. sqlspec/migrations/context.py +142 -0
  212. sqlspec/migrations/fix.py +203 -0
  213. sqlspec/migrations/loaders.py +450 -0
  214. sqlspec/migrations/runner.py +1024 -0
  215. sqlspec/migrations/templates.py +234 -0
  216. sqlspec/migrations/tracker.py +403 -0
  217. sqlspec/migrations/utils.py +256 -0
  218. sqlspec/migrations/validation.py +203 -0
  219. sqlspec/observability/__init__.py +22 -0
  220. sqlspec/observability/_config.py +228 -0
  221. sqlspec/observability/_diagnostics.py +67 -0
  222. sqlspec/observability/_dispatcher.py +151 -0
  223. sqlspec/observability/_observer.py +180 -0
  224. sqlspec/observability/_runtime.py +381 -0
  225. sqlspec/observability/_spans.py +158 -0
  226. sqlspec/protocols.py +530 -0
  227. sqlspec/py.typed +0 -0
  228. sqlspec/storage/__init__.py +46 -0
  229. sqlspec/storage/_utils.py +104 -0
  230. sqlspec/storage/backends/__init__.py +1 -0
  231. sqlspec/storage/backends/base.py +163 -0
  232. sqlspec/storage/backends/fsspec.py +398 -0
  233. sqlspec/storage/backends/local.py +377 -0
  234. sqlspec/storage/backends/obstore.py +580 -0
  235. sqlspec/storage/errors.py +104 -0
  236. sqlspec/storage/pipeline.py +604 -0
  237. sqlspec/storage/registry.py +289 -0
  238. sqlspec/typing.py +219 -0
  239. sqlspec/utils/__init__.py +31 -0
  240. sqlspec/utils/arrow_helpers.py +95 -0
  241. sqlspec/utils/config_resolver.py +153 -0
  242. sqlspec/utils/correlation.py +132 -0
  243. sqlspec/utils/data_transformation.py +114 -0
  244. sqlspec/utils/dependencies.py +79 -0
  245. sqlspec/utils/deprecation.py +113 -0
  246. sqlspec/utils/fixtures.py +250 -0
  247. sqlspec/utils/logging.py +172 -0
  248. sqlspec/utils/module_loader.py +273 -0
  249. sqlspec/utils/portal.py +325 -0
  250. sqlspec/utils/schema.py +288 -0
  251. sqlspec/utils/serializers.py +396 -0
  252. sqlspec/utils/singleton.py +41 -0
  253. sqlspec/utils/sync_tools.py +277 -0
  254. sqlspec/utils/text.py +108 -0
  255. sqlspec/utils/type_converters.py +99 -0
  256. sqlspec/utils/type_guards.py +1324 -0
  257. sqlspec/utils/version.py +444 -0
  258. sqlspec-0.32.0.dist-info/METADATA +202 -0
  259. sqlspec-0.32.0.dist-info/RECORD +262 -0
  260. sqlspec-0.32.0.dist-info/WHEEL +4 -0
  261. sqlspec-0.32.0.dist-info/entry_points.txt +2 -0
  262. sqlspec-0.32.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,444 @@
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
+ SEQUENTIAL_PATTERN = re.compile(r"^(?!\d{14}$)\d+$")
29
+ TIMESTAMP_PATTERN = re.compile(r"^(\d{14})$")
30
+ EXTENSION_PATTERN = re.compile(r"^ext_(\w+)_(.+)$")
31
+
32
+
33
+ class VersionType(Enum):
34
+ """Migration version format type."""
35
+
36
+ SEQUENTIAL = "sequential"
37
+ TIMESTAMP = "timestamp"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class MigrationVersion:
42
+ """Parsed migration version with structured comparison support.
43
+
44
+ Attributes:
45
+ raw: Original version string (e.g., "0001", "20251011120000", "ext_litestar_0001").
46
+ type: Version format type (sequential or timestamp).
47
+ sequence: Numeric value for sequential versions (e.g., 1, 2, 42).
48
+ timestamp: Parsed datetime for timestamp versions (UTC).
49
+ extension: Extension name for extension-prefixed versions (e.g., "litestar").
50
+ """
51
+
52
+ raw: str
53
+ type: VersionType
54
+ sequence: "int | None"
55
+ timestamp: "datetime | None"
56
+ extension: "str | None"
57
+
58
+ def __lt__(self, other: "MigrationVersion") -> bool:
59
+ """Compare versions supporting mixed formats.
60
+
61
+ Comparison Rules:
62
+ 1. Extension migrations sort by extension name first, then version
63
+ 2. Sequential < Timestamp (legacy migrations first)
64
+ 3. Sequential vs Sequential: numeric comparison
65
+ 4. Timestamp vs Timestamp: chronological comparison
66
+
67
+ Args:
68
+ other: Version to compare against.
69
+
70
+ Returns:
71
+ True if this version sorts before other.
72
+
73
+ Raises:
74
+ TypeError: If comparing against non-MigrationVersion.
75
+ """
76
+ if not isinstance(other, MigrationVersion):
77
+ return NotImplemented
78
+
79
+ if self.extension != other.extension:
80
+ if self.extension is None:
81
+ return True
82
+ if other.extension is None:
83
+ return False
84
+ return self.extension < other.extension
85
+
86
+ if self.type == other.type:
87
+ if self.type == VersionType.SEQUENTIAL:
88
+ return (self.sequence or 0) < (other.sequence or 0)
89
+ return (self.timestamp or datetime.min.replace(tzinfo=timezone.utc)) < (
90
+ other.timestamp or datetime.min.replace(tzinfo=timezone.utc)
91
+ )
92
+
93
+ return self.type == VersionType.SEQUENTIAL
94
+
95
+ def __le__(self, other: "MigrationVersion") -> bool:
96
+ """Check if version is less than or equal to another.
97
+
98
+ Args:
99
+ other: Version to compare against.
100
+
101
+ Returns:
102
+ True if this version is less than or equal to other.
103
+ """
104
+ return self == other or self < other
105
+
106
+ def __eq__(self, other: object) -> bool:
107
+ """Check version equality.
108
+
109
+ Args:
110
+ other: Version to compare against.
111
+
112
+ Returns:
113
+ True if versions are equal.
114
+ """
115
+ if not isinstance(other, MigrationVersion):
116
+ return NotImplemented
117
+ return self.raw == other.raw
118
+
119
+ def __hash__(self) -> int:
120
+ """Hash version for use in sets and dicts.
121
+
122
+ Returns:
123
+ Hash value based on raw version string.
124
+ """
125
+ return hash(self.raw)
126
+
127
+ def __repr__(self) -> str:
128
+ """Get string representation for debugging.
129
+
130
+ Returns:
131
+ String representation with type and value.
132
+ """
133
+ if self.extension:
134
+ return f"MigrationVersion(ext={self.extension}, {self.type.value}={self.raw})"
135
+ return f"MigrationVersion({self.type.value}={self.raw})"
136
+
137
+
138
+ def is_sequential_version(version_str: "str | None") -> bool:
139
+ """Check if version string is sequential format.
140
+
141
+ Sequential format: Any sequence of digits (0001, 42, 9999, 10000+).
142
+
143
+ Args:
144
+ version_str: Version string to check.
145
+
146
+ Returns:
147
+ True if sequential format, False if None or whitespace.
148
+
149
+ Examples:
150
+ >>> is_sequential_version("0001")
151
+ True
152
+ >>> is_sequential_version("42")
153
+ True
154
+ >>> is_sequential_version("10000")
155
+ True
156
+ >>> is_sequential_version("20251011120000")
157
+ False
158
+ >>> is_sequential_version(None)
159
+ False
160
+ """
161
+ if version_str is None or not version_str.strip():
162
+ return False
163
+ return bool(SEQUENTIAL_PATTERN.match(version_str))
164
+
165
+
166
+ def is_timestamp_version(version_str: "str | None") -> bool:
167
+ """Check if version string is timestamp format.
168
+
169
+ Timestamp format: 14-digit YYYYMMDDHHmmss (20251011120000).
170
+
171
+ Args:
172
+ version_str: Version string to check.
173
+
174
+ Returns:
175
+ True if timestamp format, False if None or whitespace.
176
+
177
+ Examples:
178
+ >>> is_timestamp_version("20251011120000")
179
+ True
180
+ >>> is_timestamp_version("0001")
181
+ False
182
+ >>> is_timestamp_version(None)
183
+ False
184
+ """
185
+ if version_str is None or not version_str.strip():
186
+ return False
187
+ if not TIMESTAMP_PATTERN.match(version_str):
188
+ return False
189
+
190
+ try:
191
+ datetime.strptime(version_str, "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
192
+ except ValueError:
193
+ return False
194
+ else:
195
+ return True
196
+
197
+
198
+ def parse_version(version_str: "str | None") -> MigrationVersion:
199
+ """Parse version string into structured format.
200
+
201
+ Supports:
202
+ - Sequential: "0001", "42", "9999"
203
+ - Timestamp: "20251011120000"
204
+ - Extension: "ext_litestar_0001", "ext_litestar_20251011120000"
205
+
206
+ Args:
207
+ version_str: Version string to parse.
208
+
209
+ Returns:
210
+ Parsed migration version.
211
+
212
+ Raises:
213
+ ValueError: If version format is invalid, None, or whitespace-only.
214
+
215
+ Examples:
216
+ >>> v = parse_version("0001")
217
+ >>> v.type == VersionType.SEQUENTIAL
218
+ True
219
+ >>> v.sequence
220
+ 1
221
+
222
+ >>> v = parse_version("20251011120000")
223
+ >>> v.type == VersionType.TIMESTAMP
224
+ True
225
+
226
+ >>> v = parse_version("ext_litestar_0001")
227
+ >>> v.extension
228
+ 'litestar'
229
+ """
230
+ if version_str is None or not version_str.strip():
231
+ msg = "Invalid migration version: version string is None or empty"
232
+ raise ValueError(msg)
233
+
234
+ extension_match = EXTENSION_PATTERN.match(version_str)
235
+ if extension_match:
236
+ extension_name = extension_match.group(1)
237
+ base_version = extension_match.group(2)
238
+ parsed = parse_version(base_version)
239
+
240
+ return MigrationVersion(
241
+ raw=version_str,
242
+ type=parsed.type,
243
+ sequence=parsed.sequence,
244
+ timestamp=parsed.timestamp,
245
+ extension=extension_name,
246
+ )
247
+
248
+ if is_timestamp_version(version_str):
249
+ dt = datetime.strptime(version_str, "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc)
250
+ return MigrationVersion(
251
+ raw=version_str, type=VersionType.TIMESTAMP, sequence=None, timestamp=dt, extension=None
252
+ )
253
+
254
+ if is_sequential_version(version_str):
255
+ return MigrationVersion(
256
+ raw=version_str, type=VersionType.SEQUENTIAL, sequence=int(version_str), timestamp=None, extension=None
257
+ )
258
+
259
+ msg = f"Invalid migration version format: {version_str}. Expected sequential (0001) or timestamp (YYYYMMDDHHmmss)."
260
+ raise ValueError(msg)
261
+
262
+
263
+ def generate_timestamp_version() -> str:
264
+ """Generate new timestamp version in UTC.
265
+
266
+ Format: YYYYMMDDHHmmss (14 digits).
267
+
268
+ Returns:
269
+ Timestamp version string.
270
+
271
+ Examples:
272
+ >>> version = generate_timestamp_version()
273
+ >>> len(version)
274
+ 14
275
+ >>> is_timestamp_version(version)
276
+ True
277
+ """
278
+ return datetime.now(tz=timezone.utc).strftime("%Y%m%d%H%M%S")
279
+
280
+
281
+ def get_next_sequential_number(migrations: "list[MigrationVersion]", extension: "str | None" = None) -> int:
282
+ """Find highest sequential number and return next available.
283
+
284
+ Scans migrations for sequential versions and returns the next number in sequence.
285
+ When extension is specified, only that extension's migrations are considered.
286
+ When extension is None, only core (non-extension) migrations are considered.
287
+
288
+ Args:
289
+ migrations: List of parsed migration versions.
290
+ extension: Optional extension name to filter by (e.g., "litestar", "adk").
291
+ None means core migrations only.
292
+
293
+ Returns:
294
+ Next available sequential number (1 if no sequential migrations exist).
295
+
296
+ Examples:
297
+ >>> v1 = parse_version("0001")
298
+ >>> v2 = parse_version("0002")
299
+ >>> get_next_sequential_number([v1, v2])
300
+ 3
301
+
302
+ >>> get_next_sequential_number([])
303
+ 1
304
+
305
+ >>> ext = parse_version("ext_litestar_0001")
306
+ >>> core = parse_version("0001")
307
+ >>> get_next_sequential_number([ext, core])
308
+ 2
309
+
310
+ >>> ext1 = parse_version("ext_litestar_0001")
311
+ >>> get_next_sequential_number([ext1], extension="litestar")
312
+ 2
313
+ """
314
+ sequential = [
315
+ m.sequence for m in migrations if m.type == VersionType.SEQUENTIAL and m.extension == extension and m.sequence
316
+ ]
317
+
318
+ if not sequential:
319
+ return 1
320
+
321
+ return max(sequential) + 1
322
+
323
+
324
+ def convert_to_sequential_version(timestamp_version: MigrationVersion, sequence_number: int) -> str:
325
+ """Convert timestamp MigrationVersion to sequential string format.
326
+
327
+ Preserves extension prefixes during conversion. Format uses zero-padded
328
+ 4-digit numbers (0001, 0002, etc.).
329
+
330
+ Args:
331
+ timestamp_version: Parsed timestamp version to convert.
332
+ sequence_number: Sequential number to assign.
333
+
334
+ Returns:
335
+ Sequential version string with extension prefix if applicable.
336
+
337
+ Raises:
338
+ ValueError: If input is not a timestamp version.
339
+
340
+ Examples:
341
+ >>> v = parse_version("20251011120000")
342
+ >>> convert_to_sequential_version(v, 3)
343
+ '0003'
344
+
345
+ >>> v = parse_version("ext_litestar_20251011120000")
346
+ >>> convert_to_sequential_version(v, 1)
347
+ 'ext_litestar_0001'
348
+
349
+ >>> v = parse_version("0001")
350
+ >>> convert_to_sequential_version(v, 2)
351
+ Traceback (most recent call last):
352
+ ...
353
+ ValueError: Can only convert timestamp versions to sequential
354
+ """
355
+ if timestamp_version.type != VersionType.TIMESTAMP:
356
+ msg = "Can only convert timestamp versions to sequential"
357
+ raise ValueError(msg)
358
+
359
+ seq_str = str(sequence_number).zfill(4)
360
+
361
+ if timestamp_version.extension:
362
+ return f"ext_{timestamp_version.extension}_{seq_str}"
363
+
364
+ return seq_str
365
+
366
+
367
+ def generate_conversion_map(migrations: "list[tuple[str, Any]]") -> "dict[str, str]":
368
+ """Generate mapping from timestamp versions to sequential versions.
369
+
370
+ Separates timestamp migrations from sequential, sorts timestamps chronologically,
371
+ and assigns sequential numbers starting after the highest existing sequential
372
+ number. Extension migrations maintain separate numbering within their namespace.
373
+
374
+ Args:
375
+ migrations: List of tuples (version_string, migration_path).
376
+
377
+ Returns:
378
+ Dictionary mapping old timestamp versions to new sequential versions.
379
+
380
+ Examples:
381
+ >>> migrations = [
382
+ ... ("0001", Path("0001_init.sql")),
383
+ ... ("0002", Path("0002_users.sql")),
384
+ ... ("20251011120000", Path("20251011120000_products.sql")),
385
+ ... ("20251012130000", Path("20251012130000_orders.sql")),
386
+ ... ]
387
+ >>> result = generate_conversion_map(migrations)
388
+ >>> result
389
+ {'20251011120000': '0003', '20251012130000': '0004'}
390
+
391
+ >>> migrations = [
392
+ ... ("20251011120000", Path("20251011120000_first.sql")),
393
+ ... ("20251010090000", Path("20251010090000_earlier.sql")),
394
+ ... ]
395
+ >>> result = generate_conversion_map(migrations)
396
+ >>> result
397
+ {'20251010090000': '0001', '20251011120000': '0002'}
398
+
399
+ >>> migrations = []
400
+ >>> generate_conversion_map(migrations)
401
+ {}
402
+ """
403
+ if not migrations:
404
+ return {}
405
+
406
+ def _try_parse_version(version_str: str) -> "MigrationVersion | None":
407
+ """Parse version string, returning None for invalid versions."""
408
+ try:
409
+ return parse_version(version_str)
410
+ except ValueError:
411
+ logger.warning("Skipping invalid migration version: %s", version_str)
412
+ return None
413
+
414
+ parsed_versions = [v for version_str, _path in migrations if (v := _try_parse_version(version_str)) is not None]
415
+
416
+ timestamp_migrations = sorted([v for v in parsed_versions if v.type == VersionType.TIMESTAMP])
417
+
418
+ if not timestamp_migrations:
419
+ return {}
420
+
421
+ core_timestamps = [m for m in timestamp_migrations if m.extension is None]
422
+ ext_timestamps_by_name: dict[str, list[MigrationVersion]] = {}
423
+ for m in timestamp_migrations:
424
+ if m.extension:
425
+ ext_timestamps_by_name.setdefault(m.extension, []).append(m)
426
+
427
+ conversion_map: dict[str, str] = {}
428
+
429
+ if core_timestamps:
430
+ next_seq = get_next_sequential_number(parsed_versions)
431
+ for timestamp_version in core_timestamps:
432
+ sequential_version = convert_to_sequential_version(timestamp_version, next_seq)
433
+ conversion_map[timestamp_version.raw] = sequential_version
434
+ next_seq += 1
435
+
436
+ for ext_name, ext_migrations in ext_timestamps_by_name.items():
437
+ ext_parsed = [v for v in parsed_versions if v.extension == ext_name]
438
+ next_seq = get_next_sequential_number(ext_parsed, extension=ext_name)
439
+ for timestamp_version in ext_migrations:
440
+ sequential_version = convert_to_sequential_version(timestamp_version, next_seq)
441
+ conversion_map[timestamp_version.raw] = sequential_version
442
+ next_seq += 1
443
+
444
+ return conversion_map
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlspec
3
+ Version: 0.32.0
4
+ Summary: SQL Experiments in Python
5
+ Project-URL: Discord, https://discord.gg/litestar
6
+ Project-URL: Issue, https://github.com/litestar-org/sqlspec/issues/
7
+ Project-URL: Source, https://github.com/litestar-org/sqlspec
8
+ Author-email: Cody Fincher <cody@litestar.dev>
9
+ Maintainer-email: Litestar Developers <hello@litestar.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Requires-Python: <4.0,>=3.10
13
+ Requires-Dist: mypy-extensions
14
+ Requires-Dist: rich-click
15
+ Requires-Dist: sqlglot>=19.9.0
16
+ Requires-Dist: typing-extensions
17
+ Provides-Extra: adbc
18
+ Requires-Dist: adbc-driver-manager; extra == 'adbc'
19
+ Requires-Dist: pyarrow; extra == 'adbc'
20
+ Provides-Extra: adk
21
+ Requires-Dist: google-adk; extra == 'adk'
22
+ Provides-Extra: aioodbc
23
+ Requires-Dist: aioodbc; extra == 'aioodbc'
24
+ Provides-Extra: aiosql
25
+ Requires-Dist: aiosql; extra == 'aiosql'
26
+ Provides-Extra: aiosqlite
27
+ Requires-Dist: aiosqlite; extra == 'aiosqlite'
28
+ Provides-Extra: alloydb
29
+ Requires-Dist: google-cloud-alloydb-connector; extra == 'alloydb'
30
+ Provides-Extra: asyncmy
31
+ Requires-Dist: asyncmy; extra == 'asyncmy'
32
+ Provides-Extra: asyncpg
33
+ Requires-Dist: asyncpg; extra == 'asyncpg'
34
+ Provides-Extra: attrs
35
+ Requires-Dist: attrs; extra == 'attrs'
36
+ Requires-Dist: cattrs; extra == 'attrs'
37
+ Provides-Extra: bigquery
38
+ Requires-Dist: google-cloud-bigquery; extra == 'bigquery'
39
+ Requires-Dist: google-cloud-storage; extra == 'bigquery'
40
+ Provides-Extra: cli
41
+ Requires-Dist: rich-click; extra == 'cli'
42
+ Provides-Extra: cloud-sql
43
+ Requires-Dist: cloud-sql-python-connector; extra == 'cloud-sql'
44
+ Provides-Extra: duckdb
45
+ Requires-Dist: duckdb; extra == 'duckdb'
46
+ Provides-Extra: fastapi
47
+ Requires-Dist: fastapi; extra == 'fastapi'
48
+ Provides-Extra: flask
49
+ Requires-Dist: flask; extra == 'flask'
50
+ Provides-Extra: fsspec
51
+ Requires-Dist: fsspec; extra == 'fsspec'
52
+ Provides-Extra: litestar
53
+ Requires-Dist: litestar; extra == 'litestar'
54
+ Provides-Extra: msgspec
55
+ Requires-Dist: msgspec; extra == 'msgspec'
56
+ Provides-Extra: mypyc
57
+ Provides-Extra: nanoid
58
+ Requires-Dist: fastnanoid>=0.4.1; extra == 'nanoid'
59
+ Provides-Extra: obstore
60
+ Requires-Dist: obstore; extra == 'obstore'
61
+ Provides-Extra: opentelemetry
62
+ Requires-Dist: opentelemetry-instrumentation; extra == 'opentelemetry'
63
+ Provides-Extra: oracledb
64
+ Requires-Dist: oracledb; extra == 'oracledb'
65
+ Provides-Extra: orjson
66
+ Requires-Dist: orjson; extra == 'orjson'
67
+ Provides-Extra: pandas
68
+ Requires-Dist: pandas; extra == 'pandas'
69
+ Requires-Dist: pyarrow; extra == 'pandas'
70
+ Provides-Extra: performance
71
+ Requires-Dist: msgspec; extra == 'performance'
72
+ Requires-Dist: sqlglot[rs]; extra == 'performance'
73
+ Provides-Extra: polars
74
+ Requires-Dist: polars; extra == 'polars'
75
+ Requires-Dist: pyarrow; extra == 'polars'
76
+ Provides-Extra: prometheus
77
+ Requires-Dist: prometheus-client; extra == 'prometheus'
78
+ Provides-Extra: psqlpy
79
+ Requires-Dist: psqlpy; extra == 'psqlpy'
80
+ Provides-Extra: psycopg
81
+ Requires-Dist: psycopg[binary,pool]; extra == 'psycopg'
82
+ Provides-Extra: pydantic
83
+ Requires-Dist: pydantic; extra == 'pydantic'
84
+ Requires-Dist: pydantic-extra-types; extra == 'pydantic'
85
+ Provides-Extra: pymssql
86
+ Requires-Dist: pymssql; extra == 'pymssql'
87
+ Provides-Extra: pymysql
88
+ Requires-Dist: pymysql; extra == 'pymysql'
89
+ Provides-Extra: spanner
90
+ Requires-Dist: google-cloud-spanner; extra == 'spanner'
91
+ Provides-Extra: uuid
92
+ Requires-Dist: uuid-utils; extra == 'uuid'
93
+ Description-Content-Type: text/markdown
94
+
95
+ # SQLSpec
96
+
97
+ **Type-safe SQL execution layer for Python.**
98
+
99
+ SQLSpec handles database connectivity and result mapping so you can focus on SQL. Write raw queries when you need precision, use the builder API when you need composability, or load SQL from files when you need organization. Every statement passes through a [sqlglot](https://github.com/tobymao/sqlglot)-powered AST pipeline for validation, dialect conversion, and optimization before execution. Export results as Python objects, Arrow tables, Polars or pandas DataFrames.
100
+
101
+ It's not an ORM. It's the connectivity and processing layer between your application and your database that provides the right abstraction for each situation without dictating how you write SQL.
102
+
103
+ ## Status
104
+
105
+ SQLSpec is currently in active development. The public API may change. Follow the [docs](https://sqlspec.dev/) and changelog for updates.
106
+
107
+ ## What You Get
108
+
109
+ **Connection Management**
110
+
111
+ - Connection pooling with configurable size, timeout, and lifecycle hooks
112
+ - Sync and async support with a unified API surface
113
+ - Adapters for PostgreSQL (psycopg, asyncpg, psqlpy), SQLite (sqlite3, aiosqlite), DuckDB, MySQL (asyncmy), Oracle, BigQuery, and ADBC-compatible databases
114
+
115
+ **Query Execution**
116
+
117
+ - Raw SQL strings with automatic parameter binding and dialect translation
118
+ - SQL AST parsing via sqlglot for validation, optimization, and dialect conversion
119
+ - Builder API for programmatic query construction without string concatenation
120
+ - SQL file loading to keep queries organized alongside your code (aiosql-style)
121
+ - Statement stacks for batching multiple operations with transaction control
122
+
123
+ **Result Handling**
124
+
125
+ - Type-safe result mapping to Pydantic, msgspec, attrs, or dataclasses
126
+ - Apache Arrow export for zero-copy integration with pandas, Polars, and analytical tools
127
+ - Result iteration, single-row fetch, or bulk retrieval based on your use case
128
+
129
+ **Framework Integration**
130
+
131
+ - Litestar plugin with dependency injection for connections, sessions, and pools
132
+ - Starlette/FastAPI middleware for automatic transaction management
133
+ - Flask extension with sync/async portal support
134
+
135
+ **Production Features**
136
+
137
+ - SQL validation and caching via sqlglot AST parsing
138
+ - OpenTelemetry and Prometheus instrumentation hooks
139
+ - Structured logging with correlation ID support
140
+ - Migration CLI for schema versioning
141
+
142
+ ## Quick Start
143
+
144
+ ### Install
145
+
146
+ ```bash
147
+ pip install "sqlspec"
148
+ ```
149
+
150
+ ### Run your first query
151
+
152
+ ```python
153
+ from pydantic import BaseModel
154
+ from sqlspec import SQLSpec
155
+ from sqlspec.adapters.sqlite import SqliteConfig
156
+
157
+ class Greeting(BaseModel):
158
+ message: str
159
+
160
+ spec = SQLSpec()
161
+ db = sql.add_config(SqliteConfig(pool_config={"database": ":memory:"}))
162
+
163
+ with spec.provide_session(db) as session:
164
+ greeting = session.select_one(
165
+ "SELECT 'Hello, SQLSpec!' AS message",
166
+ schema_type=Greeting,
167
+ )
168
+ print(greeting.message) # Output: Hello, SQLSpec!
169
+ ```
170
+
171
+ That's it. Write SQL, define a schema, get typed objects back. Connection pooling, parameter binding, and result mapping are handled automatically.
172
+
173
+ See the [Getting Started guide](https://sqlspec.dev/getting_started/) for installation variants, adapter selection, and advanced result mapping options.
174
+
175
+ ## Documentation
176
+
177
+ - [Getting Started](https://sqlspec.dev/getting_started/)
178
+ - [Usage Guides](https://sqlspec.dev/usage/)
179
+ - [Examples Gallery](https://sqlspec.dev/examples/)
180
+ - [API Reference](https://sqlspec.dev/reference/)
181
+ - [CLI Reference](https://sqlspec.dev/usage/cli.html)
182
+
183
+ ## Reference Applications
184
+
185
+ - **[PostgreSQL + Vertex AI Demo](https://github.com/cofin/postgres-vertexai-demo)** - Vector search with pgvector and real-time chat using Litestar and Google ADK. Shows connection pooling, migrations, type-safe result mapping, vector embeddings, and response caching.
186
+ - **[Oracle + Vertex AI Demo](https://github.com/cofin/oracledb-vertexai-demo)** - Oracle 23ai vector search with semantic similarity using HNSW indexes. Demonstrates NumPy array conversion, large object (CLOB) handling, and real-time performance metrics.
187
+
188
+ See the [usage docs](https://sqlspec.dev/usage/) for detailed guides on adapters, configuration patterns, and features like the [SQL file loader](https://sqlspec.dev/usage/loader.html).
189
+
190
+ ## Built With
191
+
192
+ - **[sqlglot](https://github.com/tobymao/sqlglot)** - SQL parser, transpiler, and optimizer powering SQLSpec's AST pipeline
193
+
194
+ ## Contributing
195
+
196
+ Contributions, issue reports, and adapter ideas are welcome. Review the
197
+ [contributor guide](https://sqlspec.dev/contributing/) and follow the project
198
+ coding standards before opening a pull request.
199
+
200
+ ## License
201
+
202
+ SQLSpec is distributed under the MIT License.