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

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

Potentially problematic release.


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

Files changed (197) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +62 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +52 -2
  9. sqlspec/adapters/adbc/driver.py +144 -45
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +25 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +352 -144
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +138 -43
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +292 -84
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
  77. sqlspec/adapters/psqlpy/driver.py +101 -31
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +40 -11
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +77 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +231 -60
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +95 -161
  129. sqlspec/driver/_common.py +133 -80
  130. sqlspec/driver/_sync.py +95 -162
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/base.py +200 -76
  153. sqlspec/migrations/commands.py +591 -62
  154. sqlspec/migrations/context.py +6 -9
  155. sqlspec/migrations/fix.py +199 -0
  156. sqlspec/migrations/loaders.py +47 -19
  157. sqlspec/migrations/runner.py +241 -75
  158. sqlspec/migrations/tracker.py +237 -21
  159. sqlspec/migrations/utils.py +51 -3
  160. sqlspec/migrations/validation.py +177 -0
  161. sqlspec/protocols.py +66 -36
  162. sqlspec/storage/_utils.py +98 -0
  163. sqlspec/storage/backends/fsspec.py +134 -106
  164. sqlspec/storage/backends/local.py +78 -51
  165. sqlspec/storage/backends/obstore.py +278 -162
  166. sqlspec/storage/registry.py +75 -39
  167. sqlspec/typing.py +14 -84
  168. sqlspec/utils/config_resolver.py +6 -6
  169. sqlspec/utils/correlation.py +4 -5
  170. sqlspec/utils/data_transformation.py +3 -2
  171. sqlspec/utils/deprecation.py +9 -8
  172. sqlspec/utils/fixtures.py +4 -4
  173. sqlspec/utils/logging.py +46 -6
  174. sqlspec/utils/module_loader.py +2 -2
  175. sqlspec/utils/schema.py +288 -0
  176. sqlspec/utils/serializers.py +3 -3
  177. sqlspec/utils/sync_tools.py +21 -17
  178. sqlspec/utils/text.py +1 -2
  179. sqlspec/utils/type_guards.py +111 -20
  180. sqlspec/utils/version.py +433 -0
  181. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  182. sqlspec-0.27.0.dist-info/RECORD +207 -0
  183. sqlspec/builder/mixins/__init__.py +0 -55
  184. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  185. sqlspec/builder/mixins/_delete_operations.py +0 -50
  186. sqlspec/builder/mixins/_insert_operations.py +0 -282
  187. sqlspec/builder/mixins/_merge_operations.py +0 -698
  188. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  189. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  190. sqlspec/builder/mixins/_select_operations.py +0 -930
  191. sqlspec/builder/mixins/_update_operations.py +0 -199
  192. sqlspec/builder/mixins/_where_clause.py +0 -1298
  193. sqlspec-0.26.0.dist-info/RECORD +0 -157
  194. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  195. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  197. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,27 +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,
152
- bind_key: "Optional[str]" = 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,
153
191
  ) -> None:
154
- """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
+ """
155
204
  if pool_config is None:
156
205
  pool_config = {}
157
206
  if "database" not in pool_config:
@@ -160,13 +209,18 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
160
209
  if pool_config.get("database") in {":memory:", ""}:
161
210
  pool_config["database"] = ":memory:shared_db"
162
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
+
163
216
  super().__init__(
164
217
  bind_key=bind_key,
165
218
  pool_config=dict(pool_config),
166
219
  pool_instance=pool_instance,
167
220
  migration_config=migration_config,
168
221
  statement_config=statement_config or duckdb_statement_config,
169
- driver_features=cast("dict[str, Any]", driver_features),
222
+ driver_features=processed_features,
223
+ extension_config=extension_config,
170
224
  )
171
225
 
172
226
  def _get_connection_config_dict(self) -> "dict[str, Any]":
@@ -247,7 +301,7 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
247
301
 
248
302
  @contextmanager
249
303
  def provide_session(
250
- self, *args: Any, statement_config: "Optional[StatementConfig]" = None, **kwargs: Any
304
+ self, *args: Any, statement_config: "StatementConfig | None" = None, **kwargs: Any
251
305
  ) -> "Generator[DuckDBDriver, None, None]":
252
306
  """Provide a DuckDB driver session context manager.
253
307
 
@@ -260,7 +314,11 @@ class DuckDBConfig(SyncDatabaseConfig[DuckDBConnection, DuckDBConnectionPool, Du
260
314
  A context manager that yields a DuckDBDriver instance.
261
315
  """
262
316
  with self.provide_connection(*args, **kwargs) as connection:
263
- 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
+ )
264
322
  yield driver
265
323
 
266
324
  def get_signature_namespace(self) -> "dict[str, type[Any]]":
@@ -1,12 +1,14 @@
1
1
  """DuckDB-specific data dictionary for metadata queries."""
2
2
 
3
3
  import re
4
- from typing import TYPE_CHECKING, Callable, Optional, cast
4
+ from typing import TYPE_CHECKING, Any, cast
5
5
 
6
6
  from sqlspec.driver import SyncDataDictionaryBase, SyncDriverAdapterBase, VersionInfo
7
7
  from sqlspec.utils.logging import get_logger
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from collections.abc import Callable
11
+
10
12
  from sqlspec.adapters.duckdb.driver import DuckDBDriver
11
13
 
12
14
  logger = get_logger("adapters.duckdb.data_dictionary")
@@ -20,7 +22,7 @@ __all__ = ("DuckDBSyncDataDictionary",)
20
22
  class DuckDBSyncDataDictionary(SyncDataDictionaryBase):
21
23
  """DuckDB-specific sync data dictionary."""
22
24
 
23
- def get_version(self, driver: SyncDriverAdapterBase) -> "Optional[VersionInfo]":
25
+ def get_version(self, driver: SyncDriverAdapterBase) -> "VersionInfo | None":
24
26
  """Get DuckDB database version information.
25
27
 
26
28
  Args:
@@ -102,6 +104,43 @@ class DuckDBSyncDataDictionary(SyncDataDictionaryBase):
102
104
  }
103
105
  return type_map.get(type_category, "VARCHAR")
104
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
+
105
144
  def list_available_features(self) -> "list[str]":
106
145
  """List available DuckDB feature flags.
107
146
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  import datetime
4
4
  from decimal import Decimal
5
- from typing import TYPE_CHECKING, Any, Final, Optional
5
+ from typing import TYPE_CHECKING, Any, Final
6
6
 
7
- import duckdb # type: ignore[import-untyped]
7
+ import duckdb
8
8
  from sqlglot import exp
9
9
 
10
10
  from sqlspec.adapters.duckdb.data_dictionary import DuckDBSyncDataDictionary
@@ -13,7 +13,18 @@ from sqlspec.core.cache import get_cache_config
13
13
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
14
14
  from sqlspec.core.statement import SQL, StatementConfig
15
15
  from sqlspec.driver import SyncDriverAdapterBase
16
- 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
+ )
17
28
  from sqlspec.utils.logging import get_logger
18
29
  from sqlspec.utils.serializers import to_json
19
30
 
@@ -70,7 +81,7 @@ class DuckDBCursor:
70
81
 
71
82
  def __init__(self, connection: "DuckDBConnection") -> None:
72
83
  self.connection = connection
73
- self.cursor: Optional[Any] = None
84
+ self.cursor: Any | None = None
74
85
 
75
86
  def __enter__(self) -> Any:
76
87
  self.cursor = self.connection.cursor()
@@ -84,8 +95,8 @@ class DuckDBCursor:
84
95
  class DuckDBExceptionHandler:
85
96
  """Context manager for handling DuckDB database exceptions.
86
97
 
87
- Catches DuckDB-specific exceptions and converts them to appropriate
88
- 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.
89
100
  """
90
101
 
91
102
  __slots__ = ()
@@ -94,41 +105,93 @@ class DuckDBExceptionHandler:
94
105
  return None
95
106
 
96
107
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
108
+ _ = exc_tb
97
109
  if exc_type is None:
98
110
  return
111
+ self._map_duckdb_exception(exc_type, exc_val)
99
112
 
100
- if issubclass(exc_type, duckdb.IntegrityError):
101
- e = exc_val
102
- msg = f"DuckDB integrity constraint violation: {e}"
103
- raise SQLSpecError(msg) from e
104
- if issubclass(exc_type, duckdb.OperationalError):
105
- e = exc_val
106
- error_msg = str(e).lower()
107
- if "syntax" in error_msg or "parse" in error_msg:
108
- msg = f"DuckDB SQL syntax error: {e}"
109
- raise SQLParsingError(msg) from e
110
- msg = f"DuckDB operational error: {e}"
111
- raise SQLSpecError(msg) from e
112
- if issubclass(exc_type, duckdb.ProgrammingError):
113
- e = exc_val
114
- error_msg = str(e).lower()
115
- if "syntax" in error_msg or "parse" in error_msg:
116
- msg = f"DuckDB SQL syntax error: {e}"
117
- raise SQLParsingError(msg) from e
118
- msg = f"DuckDB programming error: {e}"
119
- raise SQLSpecError(msg) from e
120
- if issubclass(exc_type, duckdb.Error):
121
- e = exc_val
122
- msg = f"DuckDB error: {e}"
123
- raise SQLSpecError(msg) from e
124
- if issubclass(exc_type, Exception):
125
- e = exc_val
126
- error_msg = str(e).lower()
127
- if "parse" in error_msg or "syntax" in error_msg:
128
- msg = f"SQL parsing failed: {e}"
129
- raise SQLParsingError(msg) from e
130
- msg = f"Unexpected database operation error: {e}"
131
- 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
132
195
 
133
196
 
134
197
  class DuckDBDriver(SyncDriverAdapterBase):
@@ -148,8 +211,8 @@ class DuckDBDriver(SyncDriverAdapterBase):
148
211
  def __init__(
149
212
  self,
150
213
  connection: "DuckDBConnection",
151
- statement_config: "Optional[StatementConfig]" = None,
152
- driver_features: "Optional[dict[str, Any]]" = None,
214
+ statement_config: "StatementConfig | None" = None,
215
+ driver_features: "dict[str, Any] | None" = None,
153
216
  ) -> None:
154
217
  if statement_config is None:
155
218
  cache_config = get_cache_config()
@@ -161,8 +224,40 @@ class DuckDBDriver(SyncDriverAdapterBase):
161
224
  )
162
225
  statement_config = updated_config
163
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
+
164
259
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
165
- self._data_dictionary: Optional[SyncDataDictionaryBase] = None
260
+ self._data_dictionary: SyncDataDictionaryBase | None = None
166
261
 
167
262
  def with_cursor(self, connection: "DuckDBConnection") -> "DuckDBCursor":
168
263
  """Create context manager for DuckDB cursor.
@@ -183,7 +278,7 @@ class DuckDBDriver(SyncDriverAdapterBase):
183
278
  """
184
279
  return DuckDBExceptionHandler()
185
280
 
186
- def _try_special_handling(self, cursor: Any, statement: SQL) -> "Optional[SQLResult]":
281
+ def _try_special_handling(self, cursor: Any, statement: SQL) -> "SQLResult | None":
187
282
  """Handle DuckDB-specific special operations.
188
283
 
189
284
  DuckDB does not require special operation handling, so this method
@@ -298,7 +393,7 @@ class DuckDBDriver(SyncDriverAdapterBase):
298
393
  column_names = [col[0] for col in cursor.description or []]
299
394
 
300
395
  if fetched_data and isinstance(fetched_data[0], tuple):
301
- 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]
302
397
  else:
303
398
  dict_data = fetched_data
304
399
 
@@ -0,0 +1,5 @@
1
+ """Litestar integration for DuckDB adapter."""
2
+
3
+ from sqlspec.adapters.duckdb.litestar.store import DuckdbStore
4
+
5
+ __all__ = ("DuckdbStore",)