sqlspec 0.25.0__py3-none-any.whl → 0.27.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -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 +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -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 +153 -0
  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 +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  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 +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  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 +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  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 +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  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 +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  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 +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  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 +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  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 +102 -0
  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 +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  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 +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -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/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  from collections.abc import Sequence
4
4
  from contextlib import contextmanager
5
- from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypedDict, Union, cast
5
+ from typing import TYPE_CHECKING, Any, ClassVar, TypedDict
6
6
 
7
7
  from typing_extensions import NotRequired
8
8
 
@@ -26,7 +26,7 @@ __all__ = (
26
26
  )
27
27
 
28
28
 
29
- class DuckDBConnectionParams(TypedDict, total=False):
29
+ class DuckDBConnectionParams(TypedDict):
30
30
  """DuckDB connection parameters."""
31
31
 
32
32
  database: NotRequired[str]
@@ -65,7 +65,7 @@ class DuckDBConnectionParams(TypedDict, total=False):
65
65
  extra: NotRequired[dict[str, Any]]
66
66
 
67
67
 
68
- class DuckDBPoolParams(DuckDBConnectionParams, total=False):
68
+ class DuckDBPoolParams(DuckDBConnectionParams):
69
69
  """Complete pool configuration for DuckDB adapter.
70
70
 
71
71
  Combines standardized pool parameters with DuckDB-specific connection parameters.
@@ -77,7 +77,7 @@ class DuckDBPoolParams(DuckDBConnectionParams, total=False):
77
77
  pool_recycle_seconds: NotRequired[int]
78
78
 
79
79
 
80
- class DuckDBExtensionConfig(TypedDict, total=False):
80
+ class DuckDBExtensionConfig(TypedDict):
81
81
  """DuckDB extension configuration for auto-management."""
82
82
 
83
83
  name: str
@@ -93,7 +93,7 @@ class DuckDBExtensionConfig(TypedDict, total=False):
93
93
  """Force reinstallation of the extension."""
94
94
 
95
95
 
96
- class DuckDBSecretConfig(TypedDict, total=False):
96
+ class DuckDBSecretConfig(TypedDict):
97
97
  """DuckDB secret configuration for AI/API integrations."""
98
98
 
99
99
  secret_type: str
@@ -109,15 +109,25 @@ class DuckDBSecretConfig(TypedDict, total=False):
109
109
  """Scope of the secret (LOCAL or PERSISTENT)."""
110
110
 
111
111
 
112
- class DuckDBDriverFeatures(TypedDict, total=False):
113
- """TypedDict for DuckDB driver features configuration."""
112
+ class DuckDBDriverFeatures(TypedDict):
113
+ """TypedDict for DuckDB driver features configuration.
114
+
115
+ Attributes:
116
+ extensions: List of extensions to install/load on connection creation.
117
+ secrets: List of secrets to create for AI/API integrations.
118
+ on_connection_create: Callback executed when connection is created.
119
+ json_serializer: Custom JSON serializer for dict/list parameter conversion.
120
+ Defaults to sqlspec.utils.serializers.to_json if not provided.
121
+ enable_uuid_conversion: Enable automatic UUID string conversion.
122
+ When True (default), UUID strings are automatically converted to UUID objects.
123
+ When False, UUID strings are treated as regular strings.
124
+ """
114
125
 
115
126
  extensions: NotRequired[Sequence[DuckDBExtensionConfig]]
116
- """List of extensions to install/load on connection creation."""
117
127
  secrets: NotRequired[Sequence[DuckDBSecretConfig]]
118
- """List of secrets to create for AI/API integrations."""
119
- on_connection_create: NotRequired["Callable[[DuckDBConnection], Optional[DuckDBConnection]]"]
120
- """Callback executed when connection is created."""
128
+ on_connection_create: NotRequired["Callable[[DuckDBConnection], DuckDBConnection | None]"]
129
+ json_serializer: NotRequired["Callable[[Any], str]"]
130
+ enable_uuid_conversion: NotRequired[bool]
121
131
 
122
132
 
123
133
  class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, DuckDBDriver]):
@@ -131,26 +141,66 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
131
141
  - Auto configuration settings
132
142
  - Arrow integration
133
143
  - Direct file querying capabilities
144
+ - Configurable type handlers for JSON serialization and UUID conversion
134
145
 
135
146
  DuckDB Connection Pool Configuration:
136
147
  - Default pool size is 1-4 connections (DuckDB uses single connection by default)
137
148
  - Connection recycling is set to 24 hours by default (set to 0 to disable)
138
149
  - Shared memory databases use `:memory:shared_db` for proper concurrency
150
+
151
+ Type Handler Configuration via driver_features:
152
+ - `json_serializer`: Custom JSON serializer for dict/list parameters.
153
+ Defaults to `sqlspec.utils.serializers.to_json` if not provided.
154
+ Example: `json_serializer=msgspec.json.encode(...).decode('utf-8')`
155
+
156
+ - `enable_uuid_conversion`: Enable automatic UUID string conversion (default: True).
157
+ When True, UUID strings in query results are automatically converted to UUID objects.
158
+ When False, UUID strings are treated as regular strings.
159
+
160
+ Example:
161
+ >>> import msgspec
162
+ >>> from sqlspec.adapters.duckdb import DuckDBConfig
163
+ >>>
164
+ >>> # Custom JSON serializer
165
+ >>> def custom_json(obj):
166
+ ... return msgspec.json.encode(obj).decode("utf-8")
167
+ >>>
168
+ >>> config = DuckDBConfig(
169
+ ... pool_config={"database": ":memory:"},
170
+ ... driver_features={
171
+ ... "json_serializer": custom_json,
172
+ ... "enable_uuid_conversion": False,
173
+ ... },
174
+ ... )
139
175
  """
140
176
 
141
177
  driver_type: "ClassVar[type[DuckDBDriver]]" = DuckDBDriver
142
178
  connection_type: "ClassVar[type[DuckDBConnection]]" = DuckDBConnection
179
+ supports_transactional_ddl: "ClassVar[bool]" = True
143
180
 
144
181
  def __init__(
145
182
  self,
146
183
  *,
147
- pool_config: "Optional[Union[DuckDBPoolParams, dict[str, Any]]]" = None,
148
- pool_instance: "Optional[DuckDBConnectionPool]" = None,
149
- migration_config: Optional[dict[str, Any]] = None,
150
- statement_config: "Optional[StatementConfig]" = None,
151
- driver_features: "Optional[Union[DuckDBDriverFeatures, dict[str, Any]]]" = None,
184
+ pool_config: "DuckDBPoolParams | dict[str, Any] | None" = None,
185
+ pool_instance: "DuckDBConnectionPool | None" = None,
186
+ migration_config: dict[str, Any] | None = None,
187
+ statement_config: "StatementConfig | None" = None,
188
+ driver_features: "DuckDBDriverFeatures | dict[str, Any] | None" = None,
189
+ bind_key: "str | None" = None,
190
+ extension_config: "dict[str, dict[str, Any]] | None" = None,
152
191
  ) -> None:
153
- """Initialize DuckDB configuration."""
192
+ """Initialize DuckDB configuration.
193
+
194
+ Args:
195
+ pool_config: Pool configuration parameters
196
+ pool_instance: Pre-created pool instance
197
+ migration_config: Migration configuration
198
+ statement_config: Statement configuration override
199
+ driver_features: DuckDB-specific driver features including json_serializer
200
+ and enable_uuid_conversion options
201
+ bind_key: Optional unique identifier for this configuration
202
+ extension_config: Extension-specific configuration (e.g., Litestar plugin settings)
203
+ """
154
204
  if pool_config is None:
155
205
  pool_config = {}
156
206
  if "database" not in pool_config:
@@ -159,12 +209,18 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
159
209
  if pool_config.get("database") in {":memory:", ""}:
160
210
  pool_config["database"] = ":memory:shared_db"
161
211
 
212
+ processed_features = dict(driver_features) if driver_features else {}
213
+ if "enable_uuid_conversion" not in processed_features:
214
+ processed_features["enable_uuid_conversion"] = True
215
+
162
216
  super().__init__(
217
+ bind_key=bind_key,
163
218
  pool_config=dict(pool_config),
164
219
  pool_instance=pool_instance,
165
220
  migration_config=migration_config,
166
221
  statement_config=statement_config or duckdb_statement_config,
167
- driver_features=cast("dict[str, Any]", driver_features),
222
+ driver_features=processed_features,
223
+ extension_config=extension_config,
168
224
  )
169
225
 
170
226
  def _get_connection_config_dict(self) -> "dict[str, Any]":
@@ -245,7 +301,7 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
245
301
 
246
302
  @contextmanager
247
303
  def provide_session(
248
- self, *args: Any, statement_config: "Optional[StatementConfig]" = None, **kwargs: Any
304
+ self, *args: Any, statement_config: "StatementConfig | None" = None, **kwargs: Any
249
305
  ) -> "Generator[DuckDBDriver, None, None]":
250
306
  """Provide a DuckDB driver session context manager.
251
307
 
@@ -258,7 +314,11 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
258
314
  A context manager that yields a DuckDBDriver instance.
259
315
  """
260
316
  with self.provide_connection(*args, **kwargs) as connection:
261
- driver = self.driver_type(connection=connection, statement_config=statement_config or self.statement_config)
317
+ driver = self.driver_type(
318
+ connection=connection,
319
+ statement_config=statement_config or self.statement_config,
320
+ driver_features=self.driver_features,
321
+ )
262
322
  yield driver
263
323
 
264
324
  def get_signature_namespace(self) -> "dict[str, type[Any]]":
@@ -0,0 +1,163 @@
1
+ """DuckDB-specific data dictionary for metadata queries."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Any, cast
5
+
6
+ from sqlspec.driver import SyncDataDictionaryBase, SyncDriverAdapterBase, VersionInfo
7
+ from sqlspec.utils.logging import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Callable
11
+
12
+ from sqlspec.adapters.duckdb.driver import DuckDBDriver
13
+
14
+ logger = get_logger("adapters.duckdb.data_dictionary")
15
+
16
+ # Compiled regex patterns
17
+ DUCKDB_VERSION_PATTERN = re.compile(r"v?(\d+)\.(\d+)\.(\d+)")
18
+
19
+ __all__ = ("DuckDBSyncDataDictionary",)
20
+
21
+
22
+ class DuckDBSyncDataDictionary(SyncDataDictionaryBase):
23
+ """DuckDB-specific sync data dictionary."""
24
+
25
+ def get_version(self, driver: SyncDriverAdapterBase) -> "VersionInfo | None":
26
+ """Get DuckDB database version information.
27
+
28
+ Args:
29
+ driver: DuckDB driver instance
30
+
31
+ Returns:
32
+ DuckDB version information or None if detection fails
33
+ """
34
+ version_str = cast("DuckDBDriver", driver).select_value("SELECT version()")
35
+ if not version_str:
36
+ logger.warning("No DuckDB version information found")
37
+ return None
38
+
39
+ # Parse version like "v0.9.2" or "0.9.2"
40
+ version_match = DUCKDB_VERSION_PATTERN.search(str(version_str))
41
+ if not version_match:
42
+ logger.warning("Could not parse DuckDB version: %s", version_str)
43
+ return None
44
+
45
+ major, minor, patch = map(int, version_match.groups())
46
+ version_info = VersionInfo(major, minor, patch)
47
+ logger.debug("Detected DuckDB version: %s", version_info)
48
+ return version_info
49
+
50
+ def get_feature_flag(self, driver: SyncDriverAdapterBase, feature: str) -> bool:
51
+ """Check if DuckDB database supports a specific feature.
52
+
53
+ Args:
54
+ driver: DuckDB driver instance
55
+ feature: Feature name to check
56
+
57
+ Returns:
58
+ True if feature is supported, False otherwise
59
+ """
60
+ version_info = self.get_version(driver)
61
+ if not version_info:
62
+ return False
63
+
64
+ feature_checks: dict[str, Callable[..., bool]] = {
65
+ "supports_json": lambda _: True, # DuckDB has excellent JSON support
66
+ "supports_arrays": lambda _: True, # LIST type
67
+ "supports_maps": lambda _: True, # MAP type
68
+ "supports_structs": lambda _: True, # STRUCT type
69
+ "supports_returning": lambda v: v >= VersionInfo(0, 8, 0),
70
+ "supports_upsert": lambda v: v >= VersionInfo(0, 8, 0),
71
+ "supports_window_functions": lambda _: True,
72
+ "supports_cte": lambda _: True,
73
+ "supports_transactions": lambda _: True,
74
+ "supports_prepared_statements": lambda _: True,
75
+ "supports_schemas": lambda _: True,
76
+ "supports_uuid": lambda _: True,
77
+ }
78
+
79
+ if feature in feature_checks:
80
+ return bool(feature_checks[feature](version_info))
81
+
82
+ return False
83
+
84
+ def get_optimal_type(self, driver: SyncDriverAdapterBase, type_category: str) -> str: # pyright: ignore
85
+ """Get optimal DuckDB type for a category.
86
+
87
+ Args:
88
+ driver: DuckDB driver instance
89
+ type_category: Type category
90
+
91
+ Returns:
92
+ DuckDB-specific type name
93
+ """
94
+ type_map = {
95
+ "json": "JSON",
96
+ "uuid": "UUID",
97
+ "boolean": "BOOLEAN",
98
+ "timestamp": "TIMESTAMP",
99
+ "text": "TEXT",
100
+ "blob": "BLOB",
101
+ "array": "LIST",
102
+ "map": "MAP",
103
+ "struct": "STRUCT",
104
+ }
105
+ return type_map.get(type_category, "VARCHAR")
106
+
107
+ def get_columns(
108
+ self, driver: SyncDriverAdapterBase, table: str, schema: "str | None" = None
109
+ ) -> "list[dict[str, Any]]":
110
+ """Get column information for a table using information_schema.
111
+
112
+ Args:
113
+ driver: DuckDB driver instance
114
+ table: Table name to query columns for
115
+ schema: Schema name (None for default)
116
+
117
+ Returns:
118
+ List of column metadata dictionaries with keys:
119
+ - column_name: Name of the column
120
+ - data_type: DuckDB data type
121
+ - nullable: Whether column allows NULL (YES/NO)
122
+ - column_default: Default value if any
123
+ """
124
+ duckdb_driver = cast("DuckDBDriver", driver)
125
+
126
+ if schema:
127
+ sql = f"""
128
+ SELECT column_name, data_type, is_nullable, column_default
129
+ FROM information_schema.columns
130
+ WHERE table_name = '{table}' AND table_schema = '{schema}'
131
+ ORDER BY ordinal_position
132
+ """
133
+ else:
134
+ sql = f"""
135
+ SELECT column_name, data_type, is_nullable, column_default
136
+ FROM information_schema.columns
137
+ WHERE table_name = '{table}'
138
+ ORDER BY ordinal_position
139
+ """
140
+
141
+ result = duckdb_driver.execute(sql)
142
+ return result.data or []
143
+
144
+ def list_available_features(self) -> "list[str]":
145
+ """List available DuckDB feature flags.
146
+
147
+ Returns:
148
+ List of supported feature names
149
+ """
150
+ return [
151
+ "supports_json",
152
+ "supports_arrays",
153
+ "supports_maps",
154
+ "supports_structs",
155
+ "supports_returning",
156
+ "supports_upsert",
157
+ "supports_window_functions",
158
+ "supports_cte",
159
+ "supports_transactions",
160
+ "supports_prepared_statements",
161
+ "supports_schemas",
162
+ "supports_uuid",
163
+ ]
@@ -1,16 +1,32 @@
1
1
  """DuckDB driver implementation."""
2
2
 
3
- from typing import TYPE_CHECKING, Any, Final, Optional
3
+ import datetime
4
+ from decimal import Decimal
5
+ from typing import TYPE_CHECKING, Any, Final
4
6
 
5
7
  import duckdb
6
8
  from sqlglot import exp
7
9
 
10
+ from sqlspec.adapters.duckdb.data_dictionary import DuckDBSyncDataDictionary
11
+ from sqlspec.adapters.duckdb.type_converter import DuckDBTypeConverter
8
12
  from sqlspec.core.cache import get_cache_config
9
13
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
10
14
  from sqlspec.core.statement import SQL, StatementConfig
11
15
  from sqlspec.driver import SyncDriverAdapterBase
12
- from sqlspec.exceptions import SQLParsingError, SQLSpecError
16
+ from sqlspec.exceptions import (
17
+ CheckViolationError,
18
+ DataError,
19
+ ForeignKeyViolationError,
20
+ IntegrityError,
21
+ NotFoundError,
22
+ NotNullViolationError,
23
+ OperationalError,
24
+ SQLParsingError,
25
+ SQLSpecError,
26
+ UniqueViolationError,
27
+ )
13
28
  from sqlspec.utils.logging import get_logger
29
+ from sqlspec.utils.serializers import to_json
14
30
 
15
31
  if TYPE_CHECKING:
16
32
  from contextlib import AbstractContextManager
@@ -18,11 +34,14 @@ if TYPE_CHECKING:
18
34
  from sqlspec.adapters.duckdb._types import DuckDBConnection
19
35
  from sqlspec.core.result import SQLResult
20
36
  from sqlspec.driver import ExecutionResult
37
+ from sqlspec.driver._sync import SyncDataDictionaryBase
21
38
 
22
39
  __all__ = ("DuckDBCursor", "DuckDBDriver", "DuckDBExceptionHandler", "duckdb_statement_config")
23
40
 
24
41
  logger = get_logger("adapters.duckdb")
25
42
 
43
+ _type_converter = DuckDBTypeConverter()
44
+
26
45
 
27
46
  duckdb_statement_config = StatementConfig(
28
47
  dialect="duckdb",
@@ -31,7 +50,15 @@ duckdb_statement_config = StatementConfig(
31
50
  supported_parameter_styles={ParameterStyle.QMARK, ParameterStyle.NUMERIC, ParameterStyle.NAMED_DOLLAR},
32
51
  default_execution_parameter_style=ParameterStyle.QMARK,
33
52
  supported_execution_parameter_styles={ParameterStyle.QMARK, ParameterStyle.NUMERIC},
34
- type_coercion_map={},
53
+ type_coercion_map={
54
+ bool: int,
55
+ datetime.datetime: lambda v: v.isoformat(),
56
+ datetime.date: lambda v: v.isoformat(),
57
+ Decimal: str,
58
+ dict: to_json,
59
+ list: to_json,
60
+ str: _type_converter.convert_if_detected,
61
+ },
35
62
  has_native_list_expansion=True,
36
63
  needs_static_script_compilation=False,
37
64
  preserve_parameter_format=True,
@@ -54,14 +81,13 @@ class DuckDBCursor:
54
81
 
55
82
  def __init__(self, connection: "DuckDBConnection") -> None:
56
83
  self.connection = connection
57
- self.cursor: Optional[Any] = None
84
+ self.cursor: Any | None = None
58
85
 
59
86
  def __enter__(self) -> Any:
60
87
  self.cursor = self.connection.cursor()
61
88
  return self.cursor
62
89
 
63
- def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
64
- _ = (exc_type, exc_val, exc_tb)
90
+ def __exit__(self, *_: Any) -> None:
65
91
  if self.cursor is not None:
66
92
  self.cursor.close()
67
93
 
@@ -69,8 +95,8 @@ class DuckDBCursor:
69
95
  class DuckDBExceptionHandler:
70
96
  """Context manager for handling DuckDB database exceptions.
71
97
 
72
- Catches DuckDB-specific exceptions and converts them to appropriate
73
- SQLSpec exception types for consistent error handling.
98
+ Uses exception type and message-based detection to map DuckDB errors
99
+ to specific SQLSpec exceptions for better error handling.
74
100
  """
75
101
 
76
102
  __slots__ = ()
@@ -79,41 +105,93 @@ class DuckDBExceptionHandler:
79
105
  return None
80
106
 
81
107
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
108
+ _ = exc_tb
82
109
  if exc_type is None:
83
110
  return
111
+ self._map_duckdb_exception(exc_type, exc_val)
84
112
 
85
- if issubclass(exc_type, duckdb.IntegrityError):
86
- e = exc_val
87
- msg = f"DuckDB integrity constraint violation: {e}"
88
- raise SQLSpecError(msg) from e
89
- if issubclass(exc_type, duckdb.OperationalError):
90
- e = exc_val
91
- error_msg = str(e).lower()
92
- if "syntax" in error_msg or "parse" in error_msg:
93
- msg = f"DuckDB SQL syntax error: {e}"
94
- raise SQLParsingError(msg) from e
95
- msg = f"DuckDB operational error: {e}"
96
- raise SQLSpecError(msg) from e
97
- if issubclass(exc_type, duckdb.ProgrammingError):
98
- e = exc_val
99
- error_msg = str(e).lower()
100
- if "syntax" in error_msg or "parse" in error_msg:
101
- msg = f"DuckDB SQL syntax error: {e}"
102
- raise SQLParsingError(msg) from e
103
- msg = f"DuckDB programming error: {e}"
104
- raise SQLSpecError(msg) from e
105
- if issubclass(exc_type, duckdb.Error):
106
- e = exc_val
107
- msg = f"DuckDB error: {e}"
108
- raise SQLSpecError(msg) from e
109
- if issubclass(exc_type, Exception):
110
- e = exc_val
111
- error_msg = str(e).lower()
112
- if "parse" in error_msg or "syntax" in error_msg:
113
- msg = f"SQL parsing failed: {e}"
114
- raise SQLParsingError(msg) from e
115
- msg = f"Unexpected database operation error: {e}"
116
- raise SQLSpecError(msg) from e
113
+ def _map_duckdb_exception(self, exc_type: Any, e: Any) -> None:
114
+ """Map DuckDB exception to SQLSpec exception.
115
+
116
+ Uses exception type and message-based detection.
117
+
118
+ Args:
119
+ exc_type: Exception type
120
+ e: Exception instance
121
+ """
122
+ error_msg = str(e).lower()
123
+ exc_name = exc_type.__name__ if hasattr(exc_type, "__name__") else str(exc_type)
124
+
125
+ if "constraintexception" in exc_name.lower():
126
+ self._handle_constraint_exception(e, error_msg)
127
+ elif "catalogexception" in exc_name.lower():
128
+ self._raise_not_found_error(e)
129
+ elif "parserexception" in exc_name.lower() or "binderexception" in exc_name.lower():
130
+ self._raise_parsing_error(e)
131
+ elif "ioexception" in exc_name.lower():
132
+ self._raise_operational_error(e)
133
+ elif "conversionexception" in exc_name.lower() or "type mismatch" in error_msg:
134
+ self._raise_data_error(e)
135
+ else:
136
+ self._raise_generic_error(e)
137
+
138
+ def _handle_constraint_exception(self, e: Any, error_msg: str) -> None:
139
+ """Handle constraint exceptions using message-based detection.
140
+
141
+ Args:
142
+ e: Exception instance
143
+ error_msg: Lowercase error message
144
+ """
145
+ if "unique" in error_msg or "duplicate" in error_msg:
146
+ self._raise_unique_violation(e)
147
+ elif "foreign key" in error_msg or "violates foreign key" in error_msg:
148
+ self._raise_foreign_key_violation(e)
149
+ elif "not null" in error_msg or "null value" in error_msg:
150
+ self._raise_not_null_violation(e)
151
+ elif "check constraint" in error_msg or "check condition" in error_msg:
152
+ self._raise_check_violation(e)
153
+ else:
154
+ self._raise_integrity_error(e)
155
+
156
+ def _raise_unique_violation(self, e: Any) -> None:
157
+ msg = f"DuckDB unique constraint violation: {e}"
158
+ raise UniqueViolationError(msg) from e
159
+
160
+ def _raise_foreign_key_violation(self, e: Any) -> None:
161
+ msg = f"DuckDB foreign key constraint violation: {e}"
162
+ raise ForeignKeyViolationError(msg) from e
163
+
164
+ def _raise_not_null_violation(self, e: Any) -> None:
165
+ msg = f"DuckDB not-null constraint violation: {e}"
166
+ raise NotNullViolationError(msg) from e
167
+
168
+ def _raise_check_violation(self, e: Any) -> None:
169
+ msg = f"DuckDB check constraint violation: {e}"
170
+ raise CheckViolationError(msg) from e
171
+
172
+ def _raise_integrity_error(self, e: Any) -> None:
173
+ msg = f"DuckDB integrity constraint violation: {e}"
174
+ raise IntegrityError(msg) from e
175
+
176
+ def _raise_not_found_error(self, e: Any) -> None:
177
+ msg = f"DuckDB catalog error: {e}"
178
+ raise NotFoundError(msg) from e
179
+
180
+ def _raise_parsing_error(self, e: Any) -> None:
181
+ msg = f"DuckDB SQL parsing error: {e}"
182
+ raise SQLParsingError(msg) from e
183
+
184
+ def _raise_operational_error(self, e: Any) -> None:
185
+ msg = f"DuckDB operational error: {e}"
186
+ raise OperationalError(msg) from e
187
+
188
+ def _raise_data_error(self, e: Any) -> None:
189
+ msg = f"DuckDB data error: {e}"
190
+ raise DataError(msg) from e
191
+
192
+ def _raise_generic_error(self, e: Any) -> None:
193
+ msg = f"DuckDB database error: {e}"
194
+ raise SQLSpecError(msg) from e
117
195
 
118
196
 
119
197
  class DuckDBDriver(SyncDriverAdapterBase):
@@ -127,14 +205,14 @@ class DuckDBDriver(SyncDriverAdapterBase):
127
205
  the sqlspec.core modules for statement processing and caching.
128
206
  """
129
207
 
130
- __slots__ = ()
208
+ __slots__ = ("_data_dictionary",)
131
209
  dialect = "duckdb"
132
210
 
133
211
  def __init__(
134
212
  self,
135
213
  connection: "DuckDBConnection",
136
- statement_config: "Optional[StatementConfig]" = None,
137
- driver_features: "Optional[dict[str, Any]]" = None,
214
+ statement_config: "StatementConfig | None" = None,
215
+ driver_features: "dict[str, Any] | None" = None,
138
216
  ) -> None:
139
217
  if statement_config is None:
140
218
  cache_config = get_cache_config()
@@ -146,7 +224,40 @@ class DuckDBDriver(SyncDriverAdapterBase):
146
224
  )
147
225
  statement_config = updated_config
148
226
 
227
+ if driver_features:
228
+ json_serializer = driver_features.get("json_serializer")
229
+ enable_uuid_conversion = driver_features.get("enable_uuid_conversion", True)
230
+
231
+ if json_serializer or not enable_uuid_conversion:
232
+ type_converter = DuckDBTypeConverter(enable_uuid_conversion=enable_uuid_conversion)
233
+ type_coercion_map = dict(statement_config.parameter_config.type_coercion_map)
234
+
235
+ if json_serializer:
236
+ type_coercion_map[dict] = json_serializer
237
+ type_coercion_map[list] = json_serializer
238
+
239
+ if not enable_uuid_conversion:
240
+ type_coercion_map[str] = type_converter.convert_if_detected
241
+
242
+ param_config = statement_config.parameter_config
243
+ updated_param_config = ParameterStyleConfig(
244
+ default_parameter_style=param_config.default_parameter_style,
245
+ supported_parameter_styles=param_config.supported_parameter_styles,
246
+ supported_execution_parameter_styles=param_config.supported_execution_parameter_styles,
247
+ default_execution_parameter_style=param_config.default_execution_parameter_style,
248
+ type_coercion_map=type_coercion_map,
249
+ has_native_list_expansion=param_config.has_native_list_expansion,
250
+ needs_static_script_compilation=param_config.needs_static_script_compilation,
251
+ allow_mixed_parameter_styles=param_config.allow_mixed_parameter_styles,
252
+ preserve_parameter_format=param_config.preserve_parameter_format,
253
+ preserve_original_params_for_many=param_config.preserve_original_params_for_many,
254
+ output_transformer=param_config.output_transformer,
255
+ ast_transformer=param_config.ast_transformer,
256
+ )
257
+ statement_config = statement_config.replace(parameter_config=updated_param_config)
258
+
149
259
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
260
+ self._data_dictionary: SyncDataDictionaryBase | None = None
150
261
 
151
262
  def with_cursor(self, connection: "DuckDBConnection") -> "DuckDBCursor":
152
263
  """Create context manager for DuckDB cursor.
@@ -167,7 +278,7 @@ class DuckDBDriver(SyncDriverAdapterBase):
167
278
  """
168
279
  return DuckDBExceptionHandler()
169
280
 
170
- def _try_special_handling(self, cursor: Any, statement: SQL) -> "Optional[SQLResult]":
281
+ def _try_special_handling(self, cursor: Any, statement: SQL) -> "SQLResult | None":
171
282
  """Handle DuckDB-specific special operations.
172
283
 
173
284
  DuckDB does not require special operation handling, so this method
@@ -282,7 +393,7 @@ class DuckDBDriver(SyncDriverAdapterBase):
282
393
  column_names = [col[0] for col in cursor.description or []]
283
394
 
284
395
  if fetched_data and isinstance(fetched_data[0], tuple):
285
- dict_data = [dict(zip(column_names, row)) for row in fetched_data]
396
+ dict_data = [dict(zip(column_names, row, strict=False)) for row in fetched_data]
286
397
  else:
287
398
  dict_data = fetched_data
288
399
 
@@ -325,3 +436,14 @@ class DuckDBDriver(SyncDriverAdapterBase):
325
436
  except duckdb.Error as e:
326
437
  msg = f"Failed to commit DuckDB transaction: {e}"
327
438
  raise SQLSpecError(msg) from e
439
+
440
+ @property
441
+ def data_dictionary(self) -> "SyncDataDictionaryBase":
442
+ """Get the data dictionary for this driver.
443
+
444
+ Returns:
445
+ Data dictionary instance for metadata queries
446
+ """
447
+ if self._data_dictionary is None:
448
+ self._data_dictionary = DuckDBSyncDataDictionary()
449
+ return self._data_dictionary
@@ -0,0 +1,5 @@
1
+ """Litestar integration for DuckDB adapter."""
2
+
3
+ from sqlspec.adapters.duckdb.litestar.store import DuckdbStore
4
+
5
+ __all__ = ("DuckdbStore",)