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