sqlspec 0.10.1__py3-none-any.whl → 0.11.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.

@@ -1,19 +1,24 @@
1
+ import logging
1
2
  from contextlib import contextmanager
2
3
  from typing import TYPE_CHECKING, Any, Optional, Union, cast, overload
3
4
 
4
5
  from duckdb import DuckDBPyConnection
5
6
 
6
7
  from sqlspec.base import SyncDriverAdapterProtocol
7
- from sqlspec.mixins import SQLTranslatorMixin, SyncArrowBulkOperationsMixin
8
+ from sqlspec.mixins import ResultConverter, SQLTranslatorMixin, SyncArrowBulkOperationsMixin
9
+ from sqlspec.statement import SQLStatement
8
10
  from sqlspec.typing import ArrowTable, StatementParameterType
9
11
 
10
12
  if TYPE_CHECKING:
11
13
  from collections.abc import Generator, Sequence
12
14
 
15
+ from sqlspec.filters import StatementFilter
13
16
  from sqlspec.typing import ArrowTable, ModelDTOT, StatementParameterType, T
14
17
 
15
18
  __all__ = ("DuckDBConnection", "DuckDBDriver")
16
19
 
20
+ logger = logging.getLogger("sqlspec")
21
+
17
22
  DuckDBConnection = DuckDBPyConnection
18
23
 
19
24
 
@@ -21,6 +26,7 @@ class DuckDBDriver(
21
26
  SyncArrowBulkOperationsMixin["DuckDBConnection"],
22
27
  SQLTranslatorMixin["DuckDBConnection"],
23
28
  SyncDriverAdapterProtocol["DuckDBConnection"],
29
+ ResultConverter,
24
30
  ):
25
31
  """DuckDB Sync Driver Adapter."""
26
32
 
@@ -32,7 +38,6 @@ class DuckDBDriver(
32
38
  self.connection = connection
33
39
  self.use_cursor = use_cursor
34
40
 
35
- # --- Helper Methods --- #
36
41
  def _cursor(self, connection: "DuckDBConnection") -> "DuckDBConnection":
37
42
  if self.use_cursor:
38
43
  return connection.cursor()
@@ -49,6 +54,44 @@ class DuckDBDriver(
49
54
  else:
50
55
  yield connection
51
56
 
57
+ def _process_sql_params(
58
+ self,
59
+ sql: str,
60
+ parameters: "Optional[StatementParameterType]" = None,
61
+ /,
62
+ *filters: "StatementFilter",
63
+ **kwargs: Any,
64
+ ) -> "tuple[str, Optional[Union[tuple[Any, ...], list[Any], dict[str, Any]]]]":
65
+ """Process SQL and parameters for DuckDB using SQLStatement.
66
+
67
+ DuckDB supports both named (:name, $name) and positional (?) parameters.
68
+ This method processes the SQL with dialect-aware parsing and handles
69
+ parameters appropriately for DuckDB.
70
+
71
+ Args:
72
+ sql: SQL statement.
73
+ parameters: Query parameters.
74
+ *filters: Statement filters to apply.
75
+ **kwargs: Additional keyword arguments.
76
+
77
+ Returns:
78
+ Tuple of processed SQL and parameters.
79
+ """
80
+ statement = SQLStatement(sql, parameters, kwargs=kwargs, dialect=self.dialect)
81
+
82
+ # Apply any filters
83
+ for filter_obj in filters:
84
+ statement = statement.apply_filter(filter_obj)
85
+
86
+ processed_sql, processed_params, _ = statement.process()
87
+ if processed_params is None:
88
+ return processed_sql, None
89
+ if isinstance(processed_params, dict):
90
+ return processed_sql, processed_params
91
+ if isinstance(processed_params, (list, tuple)):
92
+ return processed_sql, tuple(processed_params)
93
+ return processed_sql, (processed_params,) # type: ignore[unreachable]
94
+
52
95
  # --- Public API Methods --- #
53
96
  @overload
54
97
  def select(
@@ -56,7 +99,7 @@ class DuckDBDriver(
56
99
  sql: str,
57
100
  parameters: "Optional[StatementParameterType]" = None,
58
101
  /,
59
- *,
102
+ *filters: "StatementFilter",
60
103
  connection: "Optional[DuckDBConnection]" = None,
61
104
  schema_type: None = None,
62
105
  **kwargs: Any,
@@ -67,7 +110,7 @@ class DuckDBDriver(
67
110
  sql: str,
68
111
  parameters: "Optional[StatementParameterType]" = None,
69
112
  /,
70
- *,
113
+ *filters: "StatementFilter",
71
114
  connection: "Optional[DuckDBConnection]" = None,
72
115
  schema_type: "type[ModelDTOT]",
73
116
  **kwargs: Any,
@@ -77,24 +120,28 @@ class DuckDBDriver(
77
120
  sql: str,
78
121
  parameters: "Optional[StatementParameterType]" = None,
79
122
  /,
80
- *,
123
+ *filters: "StatementFilter",
81
124
  connection: "Optional[DuckDBConnection]" = None,
82
125
  schema_type: "Optional[type[ModelDTOT]]" = None,
83
126
  **kwargs: Any,
84
- ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
127
+ ) -> "Sequence[Union[dict[str, Any], ModelDTOT]]":
128
+ """Fetch data from the database.
129
+
130
+ Returns:
131
+ List of row data as either model instances or dictionaries.
132
+ """
85
133
  connection = self._connection(connection)
86
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
134
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
87
135
  with self._with_cursor(connection) as cursor:
88
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
89
- results = cursor.fetchall() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
136
+ cursor.execute(sql, [] if parameters is None else parameters)
137
+ results = cursor.fetchall()
90
138
  if not results:
91
139
  return []
140
+ column_names = [column[0] for column in cursor.description or []]
92
141
 
93
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
94
-
95
- if schema_type is not None:
96
- return [cast("ModelDTOT", schema_type(**dict(zip(column_names, row)))) for row in results] # pyright: ignore[reportUnknownArgumentType]
97
- return [dict(zip(column_names, row)) for row in results] # pyright: ignore[reportUnknownArgumentType]
142
+ # Convert to dicts first
143
+ dict_results = [dict(zip(column_names, row)) for row in results]
144
+ return self.to_schema(dict_results, schema_type=schema_type)
98
145
 
99
146
  @overload
100
147
  def select_one(
@@ -102,7 +149,7 @@ class DuckDBDriver(
102
149
  sql: str,
103
150
  parameters: "Optional[StatementParameterType]" = None,
104
151
  /,
105
- *,
152
+ *filters: "StatementFilter",
106
153
  connection: "Optional[DuckDBConnection]" = None,
107
154
  schema_type: None = None,
108
155
  **kwargs: Any,
@@ -113,7 +160,7 @@ class DuckDBDriver(
113
160
  sql: str,
114
161
  parameters: "Optional[StatementParameterType]" = None,
115
162
  /,
116
- *,
163
+ *filters: "StatementFilter",
117
164
  connection: "Optional[DuckDBConnection]" = None,
118
165
  schema_type: "type[ModelDTOT]",
119
166
  **kwargs: Any,
@@ -121,25 +168,29 @@ class DuckDBDriver(
121
168
  def select_one(
122
169
  self,
123
170
  sql: str,
124
- parameters: Optional["StatementParameterType"] = None,
171
+ parameters: "Optional[StatementParameterType]" = None,
125
172
  /,
126
- *,
127
- connection: Optional["DuckDBConnection"] = None,
173
+ *filters: "StatementFilter",
174
+ connection: "Optional[DuckDBConnection]" = None,
128
175
  schema_type: "Optional[type[ModelDTOT]]" = None,
129
176
  **kwargs: Any,
130
- ) -> "Union[ModelDTOT, dict[str, Any]]":
177
+ ) -> "Union[dict[str, Any], ModelDTOT]":
178
+ """Fetch one row from the database.
179
+
180
+ Returns:
181
+ The first row of the query results.
182
+ """
131
183
  connection = self._connection(connection)
132
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
184
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
133
185
  with self._with_cursor(connection) as cursor:
134
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
135
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
136
- result = self.check_not_found(result) # pyright: ignore
186
+ cursor.execute(sql, [] if parameters is None else parameters)
187
+ result = cursor.fetchone()
188
+ result = self.check_not_found(result)
189
+ column_names = [column[0] for column in cursor.description or []]
137
190
 
138
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
139
- if schema_type is not None:
140
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result)))) # pyright: ignore[reportUnknownArgumentType]
141
- # Always return dictionaries
142
- return dict(zip(column_names, result)) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
191
+ # Convert to dict and use ResultConverter
192
+ dict_result = dict(zip(column_names, result))
193
+ return self.to_schema(dict_result, schema_type=schema_type)
143
194
 
144
195
  @overload
145
196
  def select_one_or_none(
@@ -147,7 +198,7 @@ class DuckDBDriver(
147
198
  sql: str,
148
199
  parameters: "Optional[StatementParameterType]" = None,
149
200
  /,
150
- *,
201
+ *filters: "StatementFilter",
151
202
  connection: "Optional[DuckDBConnection]" = None,
152
203
  schema_type: None = None,
153
204
  **kwargs: Any,
@@ -158,7 +209,7 @@ class DuckDBDriver(
158
209
  sql: str,
159
210
  parameters: "Optional[StatementParameterType]" = None,
160
211
  /,
161
- *,
212
+ *filters: "StatementFilter",
162
213
  connection: "Optional[DuckDBConnection]" = None,
163
214
  schema_type: "type[ModelDTOT]",
164
215
  **kwargs: Any,
@@ -166,25 +217,30 @@ class DuckDBDriver(
166
217
  def select_one_or_none(
167
218
  self,
168
219
  sql: str,
169
- parameters: Optional["StatementParameterType"] = None,
220
+ parameters: "Optional[StatementParameterType]" = None,
170
221
  /,
171
- *,
172
- connection: Optional["DuckDBConnection"] = None,
222
+ *filters: "StatementFilter",
223
+ connection: "Optional[DuckDBConnection]" = None,
173
224
  schema_type: "Optional[type[ModelDTOT]]" = None,
174
225
  **kwargs: Any,
175
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
226
+ ) -> "Optional[Union[dict[str, Any], ModelDTOT]]":
227
+ """Fetch one row from the database.
228
+
229
+ Returns:
230
+ The first row of the query results, or None if no results.
231
+ """
176
232
  connection = self._connection(connection)
177
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
233
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
178
234
  with self._with_cursor(connection) as cursor:
179
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
180
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
235
+ cursor.execute(sql, [] if parameters is None else parameters)
236
+ result = cursor.fetchone()
181
237
  if result is None:
182
238
  return None
239
+ column_names = [column[0] for column in cursor.description or []]
183
240
 
184
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
185
- if schema_type is not None:
186
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result)))) # pyright: ignore[reportUnknownArgumentType]
187
- return dict(zip(column_names, result)) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
241
+ # Convert to dict and use ResultConverter
242
+ dict_result = dict(zip(column_names, result))
243
+ return self.to_schema(dict_result, schema_type=schema_type)
188
244
 
189
245
  @overload
190
246
  def select_value(
@@ -192,7 +248,7 @@ class DuckDBDriver(
192
248
  sql: str,
193
249
  parameters: "Optional[StatementParameterType]" = None,
194
250
  /,
195
- *,
251
+ *filters: "StatementFilter",
196
252
  connection: "Optional[DuckDBConnection]" = None,
197
253
  schema_type: None = None,
198
254
  **kwargs: Any,
@@ -203,7 +259,7 @@ class DuckDBDriver(
203
259
  sql: str,
204
260
  parameters: "Optional[StatementParameterType]" = None,
205
261
  /,
206
- *,
262
+ *filters: "StatementFilter",
207
263
  connection: "Optional[DuckDBConnection]" = None,
208
264
  schema_type: "type[T]",
209
265
  **kwargs: Any,
@@ -213,20 +269,26 @@ class DuckDBDriver(
213
269
  sql: str,
214
270
  parameters: "Optional[StatementParameterType]" = None,
215
271
  /,
216
- *,
272
+ *filters: "StatementFilter",
217
273
  connection: "Optional[DuckDBConnection]" = None,
218
274
  schema_type: "Optional[type[T]]" = None,
219
275
  **kwargs: Any,
220
276
  ) -> "Union[T, Any]":
277
+ """Fetch a single value from the database.
278
+
279
+ Returns:
280
+ The first value from the first row of results.
281
+ """
221
282
  connection = self._connection(connection)
222
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
283
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
223
284
  with self._with_cursor(connection) as cursor:
224
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
225
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
226
- result = self.check_not_found(result) # pyright: ignore
285
+ cursor.execute(sql, [] if parameters is None else parameters)
286
+ result = cursor.fetchone()
287
+ result = self.check_not_found(result)
288
+ result_value = result[0]
227
289
  if schema_type is None:
228
- return result[0] # pyright: ignore
229
- return schema_type(result[0]) # type: ignore[call-arg]
290
+ return result_value
291
+ return schema_type(result_value) # type: ignore[call-arg]
230
292
 
231
293
  @overload
232
294
  def select_value_or_none(
@@ -234,7 +296,7 @@ class DuckDBDriver(
234
296
  sql: str,
235
297
  parameters: "Optional[StatementParameterType]" = None,
236
298
  /,
237
- *,
299
+ *filters: "StatementFilter",
238
300
  connection: "Optional[DuckDBConnection]" = None,
239
301
  schema_type: None = None,
240
302
  **kwargs: Any,
@@ -245,7 +307,7 @@ class DuckDBDriver(
245
307
  sql: str,
246
308
  parameters: "Optional[StatementParameterType]" = None,
247
309
  /,
248
- *,
310
+ *filters: "StatementFilter",
249
311
  connection: "Optional[DuckDBConnection]" = None,
250
312
  schema_type: "type[T]",
251
313
  **kwargs: Any,
@@ -255,36 +317,37 @@ class DuckDBDriver(
255
317
  sql: str,
256
318
  parameters: "Optional[StatementParameterType]" = None,
257
319
  /,
258
- *,
320
+ *filters: "StatementFilter",
259
321
  connection: "Optional[DuckDBConnection]" = None,
260
322
  schema_type: "Optional[type[T]]" = None,
261
323
  **kwargs: Any,
262
324
  ) -> "Optional[Union[T, Any]]":
263
325
  connection = self._connection(connection)
264
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
326
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
265
327
  with self._with_cursor(connection) as cursor:
266
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
267
- result = cursor.fetchone() # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
328
+ cursor.execute(sql, [] if parameters is None else parameters)
329
+ result = cursor.fetchone()
268
330
  if result is None:
269
331
  return None
270
332
  if schema_type is None:
271
- return result[0] # pyright: ignore
333
+ return result[0]
272
334
  return schema_type(result[0]) # type: ignore[call-arg]
273
335
 
274
336
  def insert_update_delete(
275
337
  self,
276
338
  sql: str,
277
- parameters: Optional["StatementParameterType"] = None,
339
+ parameters: "Optional[StatementParameterType]" = None,
278
340
  /,
279
- *,
280
- connection: Optional["DuckDBConnection"] = None,
341
+ *filters: "StatementFilter",
342
+ connection: "Optional[DuckDBConnection]" = None,
281
343
  **kwargs: Any,
282
344
  ) -> int:
283
345
  connection = self._connection(connection)
284
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
346
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
285
347
  with self._with_cursor(connection) as cursor:
286
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
287
- return getattr(cursor, "rowcount", -1) # pyright: ignore[reportUnknownMemberType]
348
+ params = [] if parameters is None else parameters
349
+ cursor.execute(sql, params)
350
+ return getattr(cursor, "rowcount", -1)
288
351
 
289
352
  @overload
290
353
  def insert_update_delete_returning(
@@ -292,7 +355,7 @@ class DuckDBDriver(
292
355
  sql: str,
293
356
  parameters: "Optional[StatementParameterType]" = None,
294
357
  /,
295
- *,
358
+ *filters: "StatementFilter",
296
359
  connection: "Optional[DuckDBConnection]" = None,
297
360
  schema_type: None = None,
298
361
  **kwargs: Any,
@@ -303,7 +366,7 @@ class DuckDBDriver(
303
366
  sql: str,
304
367
  parameters: "Optional[StatementParameterType]" = None,
305
368
  /,
306
- *,
369
+ *filters: "StatementFilter",
307
370
  connection: "Optional[DuckDBConnection]" = None,
308
371
  schema_type: "type[ModelDTOT]",
309
372
  **kwargs: Any,
@@ -313,37 +376,38 @@ class DuckDBDriver(
313
376
  sql: str,
314
377
  parameters: "Optional[StatementParameterType]" = None,
315
378
  /,
316
- *,
379
+ *filters: "StatementFilter",
317
380
  connection: "Optional[DuckDBConnection]" = None,
318
381
  schema_type: "Optional[type[ModelDTOT]]" = None,
319
382
  **kwargs: Any,
320
383
  ) -> "Union[ModelDTOT, dict[str, Any]]":
321
384
  connection = self._connection(connection)
322
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
385
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
323
386
  with self._with_cursor(connection) as cursor:
324
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
325
- result = cursor.fetchall() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
326
- result = self.check_not_found(result) # pyright: ignore
327
- column_names = [col[0] for col in cursor.description or []] # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
328
- if schema_type is not None:
329
- return cast("ModelDTOT", schema_type(**dict(zip(column_names, result[0])))) # pyright: ignore[reportUnknownArgumentType]
330
- # Always return dictionaries
331
- return dict(zip(column_names, result[0])) # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType]
387
+ params = [] if parameters is None else parameters
388
+ cursor.execute(sql, params)
389
+ result = cursor.fetchall()
390
+ result = self.check_not_found(result)
391
+ column_names = [col[0] for col in cursor.description or []]
392
+
393
+ # Convert to dict and use ResultConverter
394
+ dict_result = dict(zip(column_names, result[0]))
395
+ return self.to_schema(dict_result, schema_type=schema_type)
332
396
 
333
397
  def execute_script(
334
398
  self,
335
399
  sql: str,
336
- parameters: Optional["StatementParameterType"] = None,
400
+ parameters: "Optional[StatementParameterType]" = None,
337
401
  /,
338
- *,
339
- connection: Optional["DuckDBConnection"] = None,
402
+ connection: "Optional[DuckDBConnection]" = None,
340
403
  **kwargs: Any,
341
404
  ) -> str:
342
405
  connection = self._connection(connection)
343
406
  sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
344
407
  with self._with_cursor(connection) as cursor:
345
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
346
- return cast("str", getattr(cursor, "statusmessage", "DONE")) # pyright: ignore[reportUnknownMemberType]
408
+ params = [] if parameters is None else parameters
409
+ cursor.execute(sql, params)
410
+ return cast("str", getattr(cursor, "statusmessage", "DONE"))
347
411
 
348
412
  # --- Arrow Bulk Operations ---
349
413
 
@@ -356,8 +420,35 @@ class DuckDBDriver(
356
420
  connection: "Optional[DuckDBConnection]" = None,
357
421
  **kwargs: Any,
358
422
  ) -> "ArrowTable":
423
+ """Execute a SQL query and return results as an Apache Arrow Table.
424
+
425
+ Args:
426
+ sql: The SQL query string.
427
+ parameters: Parameters for the query.
428
+ connection: Optional connection override.
429
+ **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
430
+
431
+ Returns:
432
+ An Apache Arrow Table containing the query results.
433
+ """
359
434
  connection = self._connection(connection)
360
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
435
+
436
+ # Extract filters from kwargs if present
437
+ filters = kwargs.pop("filters", [])
438
+
439
+ sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
361
440
  with self._with_cursor(connection) as cursor:
362
- cursor.execute(sql, parameters) # pyright: ignore[reportUnknownMemberType]
363
- return cast("ArrowTable", cursor.fetch_arrow_table()) # pyright: ignore[reportUnknownMemberType]
441
+ params = [] if parameters is None else parameters
442
+ cursor.execute(sql, params)
443
+ return cast("ArrowTable", cursor.fetch_arrow_table())
444
+
445
+ def _connection(self, connection: "Optional[DuckDBConnection]" = None) -> "DuckDBConnection":
446
+ """Get the connection to use for the operation.
447
+
448
+ Args:
449
+ connection: Optional connection to use.
450
+
451
+ Returns:
452
+ The connection to use.
453
+ """
454
+ return connection or self.connection