sqlspec 0.11.0__py3-none-any.whl → 0.12.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 (155) hide show
  1. sqlspec/__init__.py +16 -3
  2. sqlspec/_serialization.py +3 -10
  3. sqlspec/_sql.py +1147 -0
  4. sqlspec/_typing.py +343 -41
  5. sqlspec/adapters/adbc/__init__.py +2 -6
  6. sqlspec/adapters/adbc/config.py +474 -149
  7. sqlspec/adapters/adbc/driver.py +330 -644
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -462
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +217 -451
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +418 -498
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +592 -634
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +393 -436
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +549 -942
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -550
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +741 -0
  31. sqlspec/adapters/psycopg/driver.py +732 -733
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +243 -426
  35. sqlspec/base.py +220 -825
  36. sqlspec/config.py +354 -0
  37. sqlspec/driver/__init__.py +22 -0
  38. sqlspec/driver/_async.py +252 -0
  39. sqlspec/driver/_common.py +338 -0
  40. sqlspec/driver/_sync.py +261 -0
  41. sqlspec/driver/mixins/__init__.py +17 -0
  42. sqlspec/driver/mixins/_pipeline.py +523 -0
  43. sqlspec/driver/mixins/_result_utils.py +122 -0
  44. sqlspec/driver/mixins/_sql_translator.py +35 -0
  45. sqlspec/driver/mixins/_storage.py +993 -0
  46. sqlspec/driver/mixins/_type_coercion.py +131 -0
  47. sqlspec/exceptions.py +299 -7
  48. sqlspec/extensions/aiosql/__init__.py +10 -0
  49. sqlspec/extensions/aiosql/adapter.py +474 -0
  50. sqlspec/extensions/litestar/__init__.py +1 -6
  51. sqlspec/extensions/litestar/_utils.py +1 -5
  52. sqlspec/extensions/litestar/config.py +5 -6
  53. sqlspec/extensions/litestar/handlers.py +13 -12
  54. sqlspec/extensions/litestar/plugin.py +22 -24
  55. sqlspec/extensions/litestar/providers.py +37 -55
  56. sqlspec/loader.py +528 -0
  57. sqlspec/service/__init__.py +3 -0
  58. sqlspec/service/base.py +24 -0
  59. sqlspec/service/pagination.py +26 -0
  60. sqlspec/statement/__init__.py +21 -0
  61. sqlspec/statement/builder/__init__.py +54 -0
  62. sqlspec/statement/builder/_ddl_utils.py +119 -0
  63. sqlspec/statement/builder/_parsing_utils.py +135 -0
  64. sqlspec/statement/builder/base.py +328 -0
  65. sqlspec/statement/builder/ddl.py +1379 -0
  66. sqlspec/statement/builder/delete.py +80 -0
  67. sqlspec/statement/builder/insert.py +274 -0
  68. sqlspec/statement/builder/merge.py +95 -0
  69. sqlspec/statement/builder/mixins/__init__.py +65 -0
  70. sqlspec/statement/builder/mixins/_aggregate_functions.py +151 -0
  71. sqlspec/statement/builder/mixins/_case_builder.py +91 -0
  72. sqlspec/statement/builder/mixins/_common_table_expr.py +91 -0
  73. sqlspec/statement/builder/mixins/_delete_from.py +34 -0
  74. sqlspec/statement/builder/mixins/_from.py +61 -0
  75. sqlspec/statement/builder/mixins/_group_by.py +119 -0
  76. sqlspec/statement/builder/mixins/_having.py +35 -0
  77. sqlspec/statement/builder/mixins/_insert_from_select.py +48 -0
  78. sqlspec/statement/builder/mixins/_insert_into.py +36 -0
  79. sqlspec/statement/builder/mixins/_insert_values.py +69 -0
  80. sqlspec/statement/builder/mixins/_join.py +110 -0
  81. sqlspec/statement/builder/mixins/_limit_offset.py +53 -0
  82. sqlspec/statement/builder/mixins/_merge_clauses.py +405 -0
  83. sqlspec/statement/builder/mixins/_order_by.py +46 -0
  84. sqlspec/statement/builder/mixins/_pivot.py +82 -0
  85. sqlspec/statement/builder/mixins/_returning.py +37 -0
  86. sqlspec/statement/builder/mixins/_select_columns.py +60 -0
  87. sqlspec/statement/builder/mixins/_set_ops.py +122 -0
  88. sqlspec/statement/builder/mixins/_unpivot.py +80 -0
  89. sqlspec/statement/builder/mixins/_update_from.py +54 -0
  90. sqlspec/statement/builder/mixins/_update_set.py +91 -0
  91. sqlspec/statement/builder/mixins/_update_table.py +29 -0
  92. sqlspec/statement/builder/mixins/_where.py +374 -0
  93. sqlspec/statement/builder/mixins/_window_functions.py +86 -0
  94. sqlspec/statement/builder/protocols.py +20 -0
  95. sqlspec/statement/builder/select.py +206 -0
  96. sqlspec/statement/builder/update.py +178 -0
  97. sqlspec/statement/filters.py +571 -0
  98. sqlspec/statement/parameters.py +736 -0
  99. sqlspec/statement/pipelines/__init__.py +67 -0
  100. sqlspec/statement/pipelines/analyzers/__init__.py +9 -0
  101. sqlspec/statement/pipelines/analyzers/_analyzer.py +649 -0
  102. sqlspec/statement/pipelines/base.py +315 -0
  103. sqlspec/statement/pipelines/context.py +119 -0
  104. sqlspec/statement/pipelines/result_types.py +41 -0
  105. sqlspec/statement/pipelines/transformers/__init__.py +8 -0
  106. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +256 -0
  107. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +623 -0
  108. sqlspec/statement/pipelines/transformers/_remove_comments.py +66 -0
  109. sqlspec/statement/pipelines/transformers/_remove_hints.py +81 -0
  110. sqlspec/statement/pipelines/validators/__init__.py +23 -0
  111. sqlspec/statement/pipelines/validators/_dml_safety.py +275 -0
  112. sqlspec/statement/pipelines/validators/_parameter_style.py +297 -0
  113. sqlspec/statement/pipelines/validators/_performance.py +703 -0
  114. sqlspec/statement/pipelines/validators/_security.py +990 -0
  115. sqlspec/statement/pipelines/validators/base.py +67 -0
  116. sqlspec/statement/result.py +527 -0
  117. sqlspec/statement/splitter.py +701 -0
  118. sqlspec/statement/sql.py +1198 -0
  119. sqlspec/storage/__init__.py +15 -0
  120. sqlspec/storage/backends/__init__.py +0 -0
  121. sqlspec/storage/backends/base.py +166 -0
  122. sqlspec/storage/backends/fsspec.py +315 -0
  123. sqlspec/storage/backends/obstore.py +464 -0
  124. sqlspec/storage/protocol.py +170 -0
  125. sqlspec/storage/registry.py +315 -0
  126. sqlspec/typing.py +157 -36
  127. sqlspec/utils/correlation.py +155 -0
  128. sqlspec/utils/deprecation.py +3 -6
  129. sqlspec/utils/fixtures.py +6 -11
  130. sqlspec/utils/logging.py +135 -0
  131. sqlspec/utils/module_loader.py +45 -43
  132. sqlspec/utils/serializers.py +4 -0
  133. sqlspec/utils/singleton.py +6 -8
  134. sqlspec/utils/sync_tools.py +15 -27
  135. sqlspec/utils/text.py +58 -26
  136. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/METADATA +100 -26
  137. sqlspec-0.12.0.dist-info/RECORD +145 -0
  138. sqlspec/adapters/bigquery/config/__init__.py +0 -3
  139. sqlspec/adapters/bigquery/config/_common.py +0 -40
  140. sqlspec/adapters/bigquery/config/_sync.py +0 -87
  141. sqlspec/adapters/oracledb/config/__init__.py +0 -9
  142. sqlspec/adapters/oracledb/config/_asyncio.py +0 -186
  143. sqlspec/adapters/oracledb/config/_common.py +0 -131
  144. sqlspec/adapters/oracledb/config/_sync.py +0 -186
  145. sqlspec/adapters/psycopg/config/__init__.py +0 -19
  146. sqlspec/adapters/psycopg/config/_async.py +0 -169
  147. sqlspec/adapters/psycopg/config/_common.py +0 -56
  148. sqlspec/adapters/psycopg/config/_sync.py +0 -168
  149. sqlspec/filters.py +0 -330
  150. sqlspec/mixins.py +0 -306
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.0.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.0.dist-info → sqlspec-0.12.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,567 +1,214 @@
1
1
  """Psqlpy Driver Implementation."""
2
2
 
3
+ import io
3
4
  import logging
4
- import re
5
- from re import Match
6
- from typing import TYPE_CHECKING, Any, Optional, Union, overload
5
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
7
6
 
8
- from psqlpy import Connection, QueryResult
9
- from psqlpy.exceptions import RustPSQLDriverPyBaseError
10
- from sqlglot import exp
7
+ from psqlpy import Connection
11
8
 
12
- from sqlspec.base import AsyncDriverAdapterProtocol
13
- from sqlspec.exceptions import SQLParsingError
14
- from sqlspec.filters import StatementFilter
15
- from sqlspec.mixins import ResultConverter, SQLTranslatorMixin
16
- from sqlspec.statement import SQLStatement
17
- from sqlspec.typing import is_dict
9
+ from sqlspec.driver import AsyncDriverAdapterProtocol
10
+ from sqlspec.driver.mixins import (
11
+ AsyncPipelinedExecutionMixin,
12
+ AsyncStorageMixin,
13
+ SQLTranslatorMixin,
14
+ ToSchemaMixin,
15
+ TypeCoercionMixin,
16
+ )
17
+ from sqlspec.statement.parameters import ParameterStyle
18
+ from sqlspec.statement.result import DMLResultDict, ScriptResultDict, SelectResultDict, SQLResult
19
+ from sqlspec.statement.sql import SQL, SQLConfig
20
+ from sqlspec.typing import DictRow, ModelDTOT, RowT
18
21
 
19
22
  if TYPE_CHECKING:
20
- from collections.abc import Sequence
21
-
22
- from psqlpy import QueryResult
23
-
24
- from sqlspec.typing import ModelDTOT, StatementParameterType, T
23
+ from sqlglot.dialects.dialect import DialectType
25
24
 
26
25
  __all__ = ("PsqlpyConnection", "PsqlpyDriver")
27
26
 
28
- # Improved regex to match question mark placeholders only when they are outside string literals and comments
29
- # This pattern handles:
30
- # 1. Single quoted strings with escaped quotes
31
- # 2. Double quoted strings with escaped quotes
32
- # 3. Single-line comments (-- to end of line)
33
- # 4. Multi-line comments (/* to */)
34
- # 5. Only question marks outside of these contexts are considered parameters
35
- QUESTION_MARK_PATTERN = re.compile(
36
- r"""
37
- (?:'[^']*(?:''[^']*)*') | # Skip single-quoted strings (with '' escapes)
38
- (?:"[^"]*(?:""[^"]*)*") | # Skip double-quoted strings (with "" escapes)
39
- (?:--.*?(?:\n|$)) | # Skip single-line comments
40
- (?:/\*(?:[^*]|\*(?!/))*\*/) | # Skip multi-line comments
41
- (\?) # Capture only question marks outside of these contexts
42
- """,
43
- re.VERBOSE | re.DOTALL,
44
- )
45
-
46
27
  PsqlpyConnection = Connection
47
28
  logger = logging.getLogger("sqlspec")
48
29
 
49
30
 
50
31
  class PsqlpyDriver(
51
- SQLTranslatorMixin["PsqlpyConnection"],
52
- AsyncDriverAdapterProtocol["PsqlpyConnection"],
53
- ResultConverter,
32
+ AsyncDriverAdapterProtocol[PsqlpyConnection, RowT],
33
+ SQLTranslatorMixin,
34
+ TypeCoercionMixin,
35
+ AsyncStorageMixin,
36
+ AsyncPipelinedExecutionMixin,
37
+ ToSchemaMixin,
54
38
  ):
55
- """Psqlpy Postgres Driver Adapter."""
56
-
57
- connection: "PsqlpyConnection"
58
- dialect: str = "postgres"
59
-
60
- def __init__(self, connection: "PsqlpyConnection") -> None:
61
- self.connection = connection
62
-
63
- def _process_sql_params(
64
- self,
65
- sql: str,
66
- parameters: "Optional[StatementParameterType]" = None,
67
- /,
68
- *filters: StatementFilter,
69
- **kwargs: Any,
70
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], dict[str, Any]]]]":
71
- """Process SQL and parameters for psqlpy.
72
-
73
- Args:
74
- sql: SQL statement.
75
- parameters: Query parameters.
76
- *filters: Statement filters to apply.
77
- **kwargs: Additional keyword arguments.
78
-
79
- Returns:
80
- The SQL statement and parameters.
81
-
82
- Raises:
83
- SQLParsingError: If the SQL parsing fails.
84
- """
85
- # Handle scalar parameter by converting to a single-item tuple
86
- if parameters is not None and not isinstance(parameters, (list, tuple, dict)):
87
- parameters = (parameters,)
88
-
89
- # Create and process the statement
90
- statement = SQLStatement(sql=sql, parameters=parameters, kwargs=kwargs, dialect=self.dialect)
91
-
92
- # Apply any filters
93
- for filter_obj in filters:
94
- statement = statement.apply_filter(filter_obj)
95
-
96
- # Process the statement
97
- sql, validated_params, parsed_expr = statement.process()
98
-
99
- if validated_params is None:
100
- return sql, None # psqlpy can handle None
101
-
102
- # Convert positional parameters from question mark style to PostgreSQL's $N style
103
- if isinstance(validated_params, (list, tuple)):
104
- # Use a counter to generate $1, $2, etc. for each ? in the SQL that's outside strings/comments
105
- param_index = 0
106
-
107
- def replace_question_mark(match: Match[str]) -> str:
108
- # Only process the match if it's not in a skipped context (string/comment)
109
- if match.group(1): # This is a question mark outside string/comment
110
- nonlocal param_index
111
- param_index += 1
112
- return f"${param_index}"
113
- # Return the entire matched text unchanged for strings/comments
114
- return match.group(0)
115
-
116
- return QUESTION_MARK_PATTERN.sub(replace_question_mark, sql), tuple(validated_params)
117
-
118
- # If no parsed expression is available, we can't safely transform dictionary parameters
119
- if is_dict(validated_params) and parsed_expr is None:
120
- msg = f"psqlpy: SQL parsing failed and dictionary parameters were provided. Cannot determine parameter order without successful parse. SQL: {sql}"
121
- raise SQLParsingError(msg)
122
-
123
- # Convert dictionary parameters to the format expected by psqlpy
124
- if is_dict(validated_params) and parsed_expr is not None:
125
- # Find all named parameters in the SQL expression
126
- named_params = []
127
-
128
- for node in parsed_expr.find_all(exp.Parameter, exp.Placeholder):
129
- if isinstance(node, exp.Parameter) and node.name and node.name in validated_params:
130
- named_params.append(node.name)
131
- elif isinstance(node, exp.Placeholder) and isinstance(node.this, str) and node.this in validated_params:
132
- named_params.append(node.this)
133
-
134
- if named_params:
135
- # Transform the SQL to use $1, $2, etc.
136
- def convert_named_to_dollar(node: exp.Expression) -> exp.Expression:
137
- if isinstance(node, exp.Parameter) and node.name and node.name in validated_params:
138
- idx = named_params.index(node.name) + 1
139
- return exp.Parameter(this=str(idx))
140
- if (
141
- isinstance(node, exp.Placeholder)
142
- and isinstance(node.this, str)
143
- and node.this in validated_params
144
- ):
145
- idx = named_params.index(node.this) + 1
146
- return exp.Parameter(this=str(idx))
147
- return node
148
-
149
- return parsed_expr.transform(convert_named_to_dollar, copy=True).sql(dialect=self.dialect), tuple(
150
- validated_params[name] for name in named_params
151
- )
152
-
153
- # If no named parameters were found in the SQL but dictionary was provided
154
- return sql, tuple(validated_params.values())
155
-
156
- # For any other case, return validated params
157
- return sql, (validated_params,) if not isinstance(validated_params, (list, tuple)) else tuple(validated_params) # type: ignore[unreachable]
158
-
159
- # --- Public API Methods --- #
160
- @overload
161
- async def select(
162
- self,
163
- sql: str,
164
- parameters: "Optional[StatementParameterType]" = None,
165
- /,
166
- *filters: StatementFilter,
167
- connection: "Optional[PsqlpyConnection]" = None,
168
- schema_type: None = None,
169
- **kwargs: Any,
170
- ) -> "Sequence[dict[str, Any]]": ...
171
- @overload
172
- async def select(
173
- self,
174
- sql: str,
175
- parameters: "Optional[StatementParameterType]" = None,
176
- /,
177
- *filters: StatementFilter,
178
- connection: "Optional[PsqlpyConnection]" = None,
179
- schema_type: "type[ModelDTOT]",
180
- **kwargs: Any,
181
- ) -> "Sequence[ModelDTOT]": ...
182
- async def select(
183
- self,
184
- sql: str,
185
- parameters: "Optional[StatementParameterType]" = None,
186
- /,
187
- *filters: StatementFilter,
188
- connection: "Optional[PsqlpyConnection]" = None,
189
- schema_type: "Optional[type[ModelDTOT]]" = None,
190
- **kwargs: Any,
191
- ) -> "Sequence[Union[ModelDTOT, dict[str, Any]]]":
192
- """Fetch data from the database.
193
-
194
- Args:
195
- sql: The SQL query string.
196
- parameters: The parameters for the query (dict, tuple, list, or None).
197
- *filters: Statement filters to apply.
198
- connection: Optional connection override.
199
- schema_type: Optional schema class for the result.
200
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
201
-
202
- Returns:
203
- List of row data as either model instances or dictionaries.
204
- """
205
- connection = self._connection(connection)
206
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
207
- parameters = parameters or [] # psqlpy expects a list/tuple
208
-
209
- results: QueryResult = await connection.fetch(sql, parameters=parameters)
210
-
211
- # Convert to dicts and use ResultConverter
212
- dict_results = results.result()
213
- return self.to_schema(dict_results, schema_type=schema_type)
214
-
215
- @overload
216
- async def select_one(
217
- self,
218
- sql: str,
219
- parameters: "Optional[StatementParameterType]" = None,
220
- /,
221
- *filters: StatementFilter,
222
- connection: "Optional[PsqlpyConnection]" = None,
223
- schema_type: None = None,
224
- **kwargs: Any,
225
- ) -> "dict[str, Any]": ...
226
- @overload
227
- async def select_one(
228
- self,
229
- sql: str,
230
- parameters: "Optional[StatementParameterType]" = None,
231
- /,
232
- *filters: StatementFilter,
233
- connection: "Optional[PsqlpyConnection]" = None,
234
- schema_type: "type[ModelDTOT]",
235
- **kwargs: Any,
236
- ) -> "ModelDTOT": ...
237
- async def select_one(
238
- self,
239
- sql: str,
240
- parameters: "Optional[StatementParameterType]" = None,
241
- /,
242
- *filters: StatementFilter,
243
- connection: "Optional[PsqlpyConnection]" = None,
244
- schema_type: "Optional[type[ModelDTOT]]" = None,
245
- **kwargs: Any,
246
- ) -> "Union[ModelDTOT, dict[str, Any]]":
247
- """Fetch one row from the database.
248
-
249
- Args:
250
- sql: The SQL query string.
251
- parameters: The parameters for the query (dict, tuple, list, or None).
252
- *filters: Statement filters to apply.
253
- connection: Optional connection override.
254
- schema_type: Optional schema class for the result.
255
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
256
-
257
- Returns:
258
- The first row of the query results.
259
- """
260
- connection = self._connection(connection)
261
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
262
- parameters = parameters or []
263
-
264
- result = await connection.fetch(sql, parameters=parameters)
265
-
266
- # Convert to dict and use ResultConverter
267
- dict_results = result.result()
268
- if not dict_results:
269
- self.check_not_found(None)
270
-
271
- return self.to_schema(dict_results[0], schema_type=schema_type)
272
-
273
- @overload
274
- async def select_one_or_none(
275
- self,
276
- sql: str,
277
- parameters: "Optional[StatementParameterType]" = None,
278
- /,
279
- *filters: StatementFilter,
280
- connection: "Optional[PsqlpyConnection]" = None,
281
- schema_type: None = None,
282
- **kwargs: Any,
283
- ) -> "Optional[dict[str, Any]]": ...
284
- @overload
285
- async def select_one_or_none(
286
- self,
287
- sql: str,
288
- parameters: "Optional[StatementParameterType]" = None,
289
- /,
290
- *filters: StatementFilter,
291
- connection: "Optional[PsqlpyConnection]" = None,
292
- schema_type: "type[ModelDTOT]",
293
- **kwargs: Any,
294
- ) -> "Optional[ModelDTOT]": ...
295
- async def select_one_or_none(
296
- self,
297
- sql: str,
298
- parameters: "Optional[StatementParameterType]" = None,
299
- /,
300
- *filters: StatementFilter,
301
- connection: "Optional[PsqlpyConnection]" = None,
302
- schema_type: "Optional[type[ModelDTOT]]" = None,
303
- **kwargs: Any,
304
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
305
- """Fetch one row from the database or return None if no rows found.
306
-
307
- Args:
308
- sql: The SQL query string.
309
- parameters: The parameters for the query (dict, tuple, list, or None).
310
- *filters: Statement filters to apply.
311
- connection: Optional connection override.
312
- schema_type: Optional schema class for the result.
313
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
314
-
315
- Returns:
316
- The first row of the query results, or None if no results found.
317
- """
318
- connection = self._connection(connection)
319
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
320
- parameters = parameters or []
321
-
322
- result = await connection.fetch(sql, parameters=parameters)
323
- dict_results = result.result()
324
-
325
- if not dict_results:
326
- return None
327
-
328
- return self.to_schema(dict_results[0], schema_type=schema_type)
329
-
330
- @overload
331
- async def select_value(
332
- self,
333
- sql: str,
334
- parameters: "Optional[StatementParameterType]" = None,
335
- /,
336
- *filters: StatementFilter,
337
- connection: "Optional[PsqlpyConnection]" = None,
338
- schema_type: None = None,
339
- **kwargs: Any,
340
- ) -> "Any": ...
341
- @overload
342
- async def select_value(
343
- self,
344
- sql: str,
345
- parameters: "Optional[StatementParameterType]" = None,
346
- /,
347
- *filters: StatementFilter,
348
- connection: "Optional[PsqlpyConnection]" = None,
349
- schema_type: "type[T]",
350
- **kwargs: Any,
351
- ) -> "T": ...
352
- async def select_value(
353
- self,
354
- sql: str,
355
- parameters: "Optional[StatementParameterType]" = None,
356
- /,
357
- *filters: StatementFilter,
358
- connection: "Optional[PsqlpyConnection]" = None,
359
- schema_type: "Optional[type[T]]" = None,
360
- **kwargs: Any,
361
- ) -> "Union[T, Any]":
362
- """Fetch a single value from the database.
363
-
364
- Args:
365
- sql: The SQL query string.
366
- parameters: The parameters for the query (dict, tuple, list, or None).
367
- *filters: Statement filters to apply.
368
- connection: Optional connection override.
369
- schema_type: Optional type to convert the result to.
370
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
371
-
372
- Returns:
373
- The first value of the first row of the query results.
374
- """
375
- connection = self._connection(connection)
376
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
377
- parameters = parameters or []
378
-
379
- value = await connection.fetch_val(sql, parameters=parameters)
380
- value = self.check_not_found(value)
381
-
382
- if schema_type is None:
383
- return value
384
- return schema_type(value) # type: ignore[call-arg]
385
-
386
- @overload
387
- async def select_value_or_none(
388
- self,
389
- sql: str,
390
- parameters: "Optional[StatementParameterType]" = None,
391
- /,
392
- *filters: StatementFilter,
393
- connection: "Optional[PsqlpyConnection]" = None,
394
- schema_type: None = None,
395
- **kwargs: Any,
396
- ) -> "Optional[Any]": ...
397
- @overload
398
- async def select_value_or_none(
399
- self,
400
- sql: str,
401
- parameters: "Optional[StatementParameterType]" = None,
402
- /,
403
- *filters: StatementFilter,
404
- connection: "Optional[PsqlpyConnection]" = None,
405
- schema_type: "type[T]",
406
- **kwargs: Any,
407
- ) -> "Optional[T]": ...
408
- async def select_value_or_none(
409
- self,
410
- sql: str,
411
- parameters: "Optional[StatementParameterType]" = None,
412
- /,
413
- *filters: StatementFilter,
414
- connection: "Optional[PsqlpyConnection]" = None,
415
- schema_type: "Optional[type[T]]" = None,
416
- **kwargs: Any,
417
- ) -> "Optional[Union[T, Any]]":
418
- """Fetch a single value or None if not found.
419
-
420
- Args:
421
- sql: The SQL query string.
422
- parameters: The parameters for the query (dict, tuple, list, or None).
423
- *filters: Statement filters to apply.
424
- connection: Optional connection override.
425
- schema_type: Optional type to convert the result to.
426
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
427
-
428
- Returns:
429
- The first value of the first row of the query results, or None if no results found.
430
- """
431
- connection = self._connection(connection)
432
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
433
- parameters = parameters or []
434
- try:
435
- value = await connection.fetch_val(sql, parameters=parameters)
436
- except RustPSQLDriverPyBaseError:
437
- return None
438
-
439
- if value is None:
440
- return None
441
- if schema_type is None:
442
- return value
443
- return schema_type(value) # type: ignore[call-arg]
444
-
445
- async def insert_update_delete(
446
- self,
447
- sql: str,
448
- parameters: "Optional[StatementParameterType]" = None,
449
- /,
450
- *filters: StatementFilter,
451
- connection: "Optional[PsqlpyConnection]" = None,
452
- **kwargs: Any,
453
- ) -> int:
454
- """Execute an insert, update, or delete statement.
455
-
456
- Args:
457
- sql: The SQL statement to execute.
458
- parameters: The parameters for the statement (dict, tuple, list, or None).
459
- *filters: Statement filters to apply.
460
- connection: Optional connection override.
461
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
462
-
463
- Returns:
464
- The number of rows affected by the statement.
465
- """
466
- connection = self._connection(connection)
467
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
468
- parameters = parameters or []
469
-
470
- await connection.execute(sql, parameters=parameters)
471
- # For INSERT/UPDATE/DELETE, psqlpy returns an empty list but the operation succeeded
472
- # if no error was raised
473
- return 1
474
-
475
- @overload
476
- async def insert_update_delete_returning(
477
- self,
478
- sql: str,
479
- parameters: "Optional[StatementParameterType]" = None,
480
- /,
481
- *filters: StatementFilter,
482
- connection: "Optional[PsqlpyConnection]" = None,
483
- schema_type: None = None,
484
- **kwargs: Any,
485
- ) -> "dict[str, Any]": ...
486
- @overload
487
- async def insert_update_delete_returning(
488
- self,
489
- sql: str,
490
- parameters: "Optional[StatementParameterType]" = None,
491
- /,
492
- *filters: StatementFilter,
493
- connection: "Optional[PsqlpyConnection]" = None,
494
- schema_type: "type[ModelDTOT]",
495
- **kwargs: Any,
496
- ) -> "ModelDTOT": ...
497
- async def insert_update_delete_returning(
498
- self,
499
- sql: str,
500
- parameters: "Optional[StatementParameterType]" = None,
501
- /,
502
- *filters: StatementFilter,
503
- connection: "Optional[PsqlpyConnection]" = None,
504
- schema_type: "Optional[type[ModelDTOT]]" = None,
505
- **kwargs: Any,
506
- ) -> "Union[ModelDTOT, dict[str, Any]]":
507
- """Insert, update, or delete data from the database and return result.
508
-
509
- Args:
510
- sql: The SQL statement to execute.
511
- parameters: The parameters for the statement (dict, tuple, list, or None).
512
- *filters: Statement filters to apply.
513
- connection: Optional connection override.
514
- schema_type: Optional schema class for the result.
515
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
516
-
517
- Returns:
518
- The first row of results.
519
- """
520
- connection = self._connection(connection)
521
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
522
- parameters = parameters or []
523
-
524
- result = await connection.execute(sql, parameters=parameters)
525
- dict_results = result.result()
526
-
527
- if not dict_results:
528
- self.check_not_found(None)
529
-
530
- return self.to_schema(dict_results[0], schema_type=schema_type)
531
-
532
- async def execute_script(
533
- self,
534
- sql: str,
535
- parameters: "Optional[StatementParameterType]" = None,
536
- /,
537
- connection: "Optional[PsqlpyConnection]" = None,
538
- **kwargs: Any,
539
- ) -> str:
540
- """Execute a SQL script.
541
-
542
- Args:
543
- sql: The SQL script to execute.
544
- parameters: The parameters for the script (dict, tuple, list, or None).
545
- connection: Optional connection override.
546
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
547
-
548
- Returns:
549
- A success message.
550
- """
551
- connection = self._connection(connection)
552
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
553
- parameters = parameters or []
554
-
555
- await connection.execute(sql, parameters=parameters)
556
- return "Script executed successfully"
557
-
558
- def _connection(self, connection: "Optional[PsqlpyConnection]" = None) -> "PsqlpyConnection":
559
- """Get the connection to use.
560
-
561
- Args:
562
- connection: Optional connection to use. If not provided, use the default connection.
563
-
564
- Returns:
565
- The connection to use.
566
- """
39
+ """Psqlpy Driver Adapter.
40
+
41
+ Modern, high-performance driver for PostgreSQL.
42
+ """
43
+
44
+ dialect: "DialectType" = "postgres"
45
+ supported_parameter_styles: "tuple[ParameterStyle, ...]" = (ParameterStyle.NUMERIC,)
46
+ default_parameter_style: ParameterStyle = ParameterStyle.NUMERIC
47
+ __slots__ = ()
48
+
49
+ def __init__(
50
+ self,
51
+ connection: PsqlpyConnection,
52
+ config: "Optional[SQLConfig]" = None,
53
+ default_row_type: "type[DictRow]" = DictRow,
54
+ ) -> None:
55
+ super().__init__(connection=connection, config=config, default_row_type=default_row_type)
56
+
57
+ def _coerce_boolean(self, value: Any) -> Any:
58
+ """PostgreSQL has native boolean support, return as-is."""
59
+ return value
60
+
61
+ def _coerce_decimal(self, value: Any) -> Any:
62
+ """PostgreSQL has native decimal support."""
63
+ if isinstance(value, str):
64
+ from decimal import Decimal
65
+
66
+ return Decimal(value)
67
+ return value
68
+
69
+ def _coerce_json(self, value: Any) -> Any:
70
+ """PostgreSQL has native JSON/JSONB support, return as-is."""
71
+ return value
72
+
73
+ def _coerce_array(self, value: Any) -> Any:
74
+ """PostgreSQL has native array support, return as-is."""
75
+ return value
76
+
77
+ async def _execute_statement(
78
+ self, statement: SQL, connection: Optional[PsqlpyConnection] = None, **kwargs: Any
79
+ ) -> Union[SelectResultDict, DMLResultDict, ScriptResultDict]:
80
+ if statement.is_script:
81
+ sql, _ = statement.compile(placeholder_style=ParameterStyle.STATIC)
82
+ return await self._execute_script(sql, connection=connection, **kwargs)
83
+
84
+ # Let the SQL object handle parameter style conversion based on dialect support
85
+ sql, params = statement.compile(placeholder_style=self.default_parameter_style)
86
+ params = self._process_parameters(params)
87
+
88
+ if statement.is_many:
89
+ return await self._execute_many(sql, params, connection=connection, **kwargs)
90
+
91
+ return await self._execute(sql, params, statement, connection=connection, **kwargs)
92
+
93
+ async def _execute(
94
+ self, sql: str, parameters: Any, statement: SQL, connection: Optional[PsqlpyConnection] = None, **kwargs: Any
95
+ ) -> Union[SelectResultDict, DMLResultDict]:
96
+ conn = self._connection(connection)
97
+ if self.returns_rows(statement.expression):
98
+ query_result = await conn.fetch(sql, parameters=parameters)
99
+ # Convert query_result to list of dicts
100
+ dict_rows: list[dict[str, Any]] = []
101
+ if query_result:
102
+ # psqlpy QueryResult has a result() method that returns list of dicts
103
+ dict_rows = query_result.result()
104
+ column_names = list(dict_rows[0].keys()) if dict_rows else []
105
+ return {"data": dict_rows, "column_names": column_names, "rows_affected": len(dict_rows)}
106
+ query_result = await conn.execute(sql, parameters=parameters)
107
+ # Note: psqlpy doesn't provide rows_affected for DML operations
108
+ # The QueryResult object only has result(), as_class(), and row_factory() methods
109
+ # For accurate row counts, use RETURNING clause
110
+ affected_count = -1 # Unknown, as psqlpy doesn't provide this info
111
+ return {"rows_affected": affected_count, "status_message": "OK"}
112
+
113
+ async def _execute_many(
114
+ self, sql: str, param_list: Any, connection: Optional[PsqlpyConnection] = None, **kwargs: Any
115
+ ) -> DMLResultDict:
116
+ conn = self._connection(connection)
117
+ await conn.execute_many(sql, param_list or [])
118
+ # execute_many doesn't return a value with rows_affected
119
+ affected_count = -1
120
+ return {"rows_affected": affected_count, "status_message": "OK"}
121
+
122
+ async def _execute_script(
123
+ self, script: str, connection: Optional[PsqlpyConnection] = None, **kwargs: Any
124
+ ) -> ScriptResultDict:
125
+ conn = self._connection(connection)
126
+ # psqlpy can execute multi-statement scripts directly
127
+ await conn.execute(script)
128
+ return {
129
+ "statements_executed": -1, # Not directly supported, but script is executed
130
+ "status_message": "SCRIPT EXECUTED",
131
+ }
132
+
133
+ async def _ingest_arrow_table(self, table: "Any", table_name: str, mode: str = "append", **options: Any) -> int:
134
+ self._ensure_pyarrow_installed()
135
+ import pyarrow.csv as pacsv
136
+
137
+ conn = self._connection(None)
138
+ if mode == "replace":
139
+ await conn.execute(f"TRUNCATE TABLE {table_name}")
140
+ elif mode == "create":
141
+ msg = "'create' mode is not supported for psqlpy ingestion."
142
+ raise NotImplementedError(msg)
143
+
144
+ buffer = io.BytesIO()
145
+ pacsv.write_csv(table, buffer)
146
+ buffer.seek(0)
147
+
148
+ # Use copy_from_raw or copy_from depending on what's available
149
+ # The method name might have changed in newer versions
150
+ copy_method = getattr(conn, "copy_from_raw", getattr(conn, "copy_from_query", None))
151
+ if copy_method:
152
+ await copy_method(f"COPY {table_name} FROM STDIN WITH (FORMAT CSV, HEADER)", data=buffer.read())
153
+ return table.num_rows # type: ignore[no-any-return]
154
+ msg = "Connection does not support COPY operations"
155
+ raise NotImplementedError(msg)
156
+
157
+ async def _wrap_select_result(
158
+ self, statement: SQL, result: SelectResultDict, schema_type: Optional[type[ModelDTOT]] = None, **kwargs: Any
159
+ ) -> Union[SQLResult[ModelDTOT], SQLResult[RowT]]:
160
+ dict_rows = result["data"]
161
+ column_names = result["column_names"]
162
+ rows_affected = result["rows_affected"]
163
+
164
+ if schema_type:
165
+ converted_data = self.to_schema(data=dict_rows, schema_type=schema_type)
166
+ return SQLResult[ModelDTOT](
167
+ statement=statement,
168
+ data=list(converted_data),
169
+ column_names=column_names,
170
+ rows_affected=rows_affected,
171
+ operation_type="SELECT",
172
+ )
173
+ return SQLResult[RowT](
174
+ statement=statement,
175
+ data=cast("list[RowT]", dict_rows),
176
+ column_names=column_names,
177
+ rows_affected=rows_affected,
178
+ operation_type="SELECT",
179
+ )
180
+
181
+ async def _wrap_execute_result(
182
+ self, statement: SQL, result: Union[DMLResultDict, ScriptResultDict], **kwargs: Any
183
+ ) -> SQLResult[RowT]:
184
+ operation_type = "UNKNOWN"
185
+ if statement.expression:
186
+ operation_type = str(statement.expression.key).upper()
187
+
188
+ if "statements_executed" in result:
189
+ script_result = cast("ScriptResultDict", result)
190
+ return SQLResult[RowT](
191
+ statement=statement,
192
+ data=[],
193
+ rows_affected=0,
194
+ operation_type="SCRIPT",
195
+ metadata={
196
+ "status_message": script_result.get("status_message", ""),
197
+ "statements_executed": script_result.get("statements_executed", -1),
198
+ },
199
+ )
200
+
201
+ dml_result = cast("DMLResultDict", result)
202
+ rows_affected = dml_result.get("rows_affected", -1)
203
+ status_message = dml_result.get("status_message", "")
204
+ return SQLResult[RowT](
205
+ statement=statement,
206
+ data=[],
207
+ rows_affected=rows_affected,
208
+ operation_type=operation_type,
209
+ metadata={"status_message": status_message},
210
+ )
211
+
212
+ def _connection(self, connection: Optional[PsqlpyConnection] = None) -> PsqlpyConnection:
213
+ """Get the connection to use for the operation."""
567
214
  return connection or self.connection