sqlspec 0.11.1__py3-none-any.whl → 0.12.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.

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 -621
  8. sqlspec/adapters/aiosqlite/__init__.py +2 -6
  9. sqlspec/adapters/aiosqlite/config.py +143 -57
  10. sqlspec/adapters/aiosqlite/driver.py +269 -431
  11. sqlspec/adapters/asyncmy/__init__.py +3 -8
  12. sqlspec/adapters/asyncmy/config.py +247 -202
  13. sqlspec/adapters/asyncmy/driver.py +218 -436
  14. sqlspec/adapters/asyncpg/__init__.py +4 -7
  15. sqlspec/adapters/asyncpg/config.py +329 -176
  16. sqlspec/adapters/asyncpg/driver.py +417 -487
  17. sqlspec/adapters/bigquery/__init__.py +2 -2
  18. sqlspec/adapters/bigquery/config.py +407 -0
  19. sqlspec/adapters/bigquery/driver.py +600 -553
  20. sqlspec/adapters/duckdb/__init__.py +4 -1
  21. sqlspec/adapters/duckdb/config.py +432 -321
  22. sqlspec/adapters/duckdb/driver.py +392 -406
  23. sqlspec/adapters/oracledb/__init__.py +3 -8
  24. sqlspec/adapters/oracledb/config.py +625 -0
  25. sqlspec/adapters/oracledb/driver.py +548 -921
  26. sqlspec/adapters/psqlpy/__init__.py +4 -7
  27. sqlspec/adapters/psqlpy/config.py +372 -203
  28. sqlspec/adapters/psqlpy/driver.py +197 -533
  29. sqlspec/adapters/psycopg/__init__.py +3 -8
  30. sqlspec/adapters/psycopg/config.py +725 -0
  31. sqlspec/adapters/psycopg/driver.py +734 -694
  32. sqlspec/adapters/sqlite/__init__.py +2 -6
  33. sqlspec/adapters/sqlite/config.py +146 -81
  34. sqlspec/adapters/sqlite/driver.py +242 -405
  35. sqlspec/base.py +220 -784
  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.1.dist-info → sqlspec-0.12.1.dist-info}/METADATA +97 -26
  137. sqlspec-0.12.1.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 -331
  150. sqlspec/mixins.py +0 -305
  151. sqlspec/statement.py +0 -378
  152. sqlspec-0.11.1.dist-info/RECORD +0 -69
  153. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/WHEEL +0 -0
  154. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/LICENSE +0 -0
  155. {sqlspec-0.11.1.dist-info → sqlspec-0.12.1.dist-info}/licenses/NOTICE +0 -0
@@ -1,550 +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 Mapping, 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
- *filters: "StatementFilter",
68
- **kwargs: Any,
69
- ) -> "tuple[str, Optional[Union[tuple[Any, ...], dict[str, Any]]]]":
70
- """Process SQL and parameters for psqlpy.
71
-
72
- Args:
73
- sql: SQL statement.
74
- parameters: Query parameters.
75
- *filters: Statement filters to apply.
76
- **kwargs: Additional keyword arguments.
77
-
78
- Returns:
79
- The SQL statement and parameters.
80
-
81
- Raises:
82
- SQLParsingError: If the SQL parsing fails.
83
- """
84
- data_params_for_statement: Optional[Union[Mapping[str, Any], Sequence[Any]]] = None
85
- combined_filters_list: list[StatementFilter] = list(filters)
86
-
87
- if parameters is not None:
88
- if isinstance(parameters, StatementFilter):
89
- combined_filters_list.insert(0, parameters)
90
- else:
91
- data_params_for_statement = parameters
92
- if data_params_for_statement is not None and not isinstance(data_params_for_statement, (list, tuple, dict)):
93
- data_params_for_statement = (data_params_for_statement,)
94
- statement = SQLStatement(sql, data_params_for_statement, kwargs=kwargs, dialect=self.dialect)
95
-
96
- for filter_obj in combined_filters_list:
97
- statement = statement.apply_filter(filter_obj)
98
-
99
- # Process the statement
100
- sql, validated_params, parsed_expr = statement.process()
101
-
102
- if validated_params is None:
103
- return sql, None # psqlpy can handle None
104
-
105
- # Convert positional parameters from question mark style to PostgreSQL's $N style
106
- if isinstance(validated_params, (list, tuple)):
107
- # Use a counter to generate $1, $2, etc. for each ? in the SQL that's outside strings/comments
108
- param_index = 0
109
-
110
- def replace_question_mark(match: Match[str]) -> str:
111
- # Only process the match if it's not in a skipped context (string/comment)
112
- if match.group(1): # This is a question mark outside string/comment
113
- nonlocal param_index
114
- param_index += 1
115
- return f"${param_index}"
116
- # Return the entire matched text unchanged for strings/comments
117
- return match.group(0)
118
-
119
- return QUESTION_MARK_PATTERN.sub(replace_question_mark, sql), tuple(validated_params)
120
-
121
- # If no parsed expression is available, we can't safely transform dictionary parameters
122
- if is_dict(validated_params) and parsed_expr is None:
123
- msg = f"psqlpy: SQL parsing failed and dictionary parameters were provided. Cannot determine parameter order without successful parse. SQL: {sql}"
124
- raise SQLParsingError(msg)
125
-
126
- # Convert dictionary parameters to the format expected by psqlpy
127
- if is_dict(validated_params) and parsed_expr is not None:
128
- # Find all named parameters in the SQL expression
129
- named_params = []
130
-
131
- for node in parsed_expr.find_all(exp.Parameter, exp.Placeholder):
132
- if isinstance(node, exp.Parameter) and node.name and node.name in validated_params:
133
- named_params.append(node.name)
134
- elif isinstance(node, exp.Placeholder) and isinstance(node.this, str) and node.this in validated_params:
135
- named_params.append(node.this)
136
-
137
- if named_params:
138
- # Transform the SQL to use $1, $2, etc.
139
- def convert_named_to_dollar(node: exp.Expression) -> exp.Expression:
140
- if isinstance(node, exp.Parameter) and node.name and node.name in validated_params:
141
- idx = named_params.index(node.name) + 1
142
- return exp.Parameter(this=str(idx))
143
- if (
144
- isinstance(node, exp.Placeholder)
145
- and isinstance(node.this, str)
146
- and node.this in validated_params
147
- ):
148
- idx = named_params.index(node.this) + 1
149
- return exp.Parameter(this=str(idx))
150
- return node
151
-
152
- return parsed_expr.transform(convert_named_to_dollar, copy=True).sql(dialect=self.dialect), tuple(
153
- validated_params[name] for name in named_params
154
- )
155
-
156
- # If no named parameters were found in the SQL but dictionary was provided
157
- return sql, tuple(validated_params.values())
158
-
159
- # For any other case, return validated params
160
- return sql, (validated_params,) if not isinstance(validated_params, (list, tuple)) else tuple(validated_params) # type: ignore[unreachable]
161
-
162
- # --- Public API Methods --- #
163
- @overload
164
- async def select(
165
- self,
166
- sql: str,
167
- parameters: "Optional[StatementParameterType]" = None,
168
- *filters: "StatementFilter",
169
- connection: "Optional[PsqlpyConnection]" = None,
170
- schema_type: None = None,
171
- **kwargs: Any,
172
- ) -> "Sequence[dict[str, Any]]": ...
173
- @overload
174
- async def select(
175
- self,
176
- sql: str,
177
- parameters: "Optional[StatementParameterType]" = None,
178
- *filters: "StatementFilter",
179
- connection: "Optional[PsqlpyConnection]" = None,
180
- schema_type: "type[ModelDTOT]",
181
- **kwargs: Any,
182
- ) -> "Sequence[ModelDTOT]": ...
183
- async def select(
184
- self,
185
- sql: str,
186
- parameters: "Optional[StatementParameterType]" = None,
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
- *filters: "StatementFilter",
221
- connection: "Optional[PsqlpyConnection]" = None,
222
- schema_type: None = None,
223
- **kwargs: Any,
224
- ) -> "dict[str, Any]": ...
225
- @overload
226
- async def select_one(
227
- self,
228
- sql: str,
229
- parameters: "Optional[StatementParameterType]" = None,
230
- *filters: "StatementFilter",
231
- connection: "Optional[PsqlpyConnection]" = None,
232
- schema_type: "type[ModelDTOT]",
233
- **kwargs: Any,
234
- ) -> "ModelDTOT": ...
235
- async def select_one(
236
- self,
237
- sql: str,
238
- parameters: "Optional[StatementParameterType]" = None,
239
- *filters: "StatementFilter",
240
- connection: "Optional[PsqlpyConnection]" = None,
241
- schema_type: "Optional[type[ModelDTOT]]" = None,
242
- **kwargs: Any,
243
- ) -> "Union[ModelDTOT, dict[str, Any]]":
244
- """Fetch one row from the database.
245
-
246
- Args:
247
- sql: The SQL query string.
248
- parameters: The parameters for the query (dict, tuple, list, or None).
249
- *filters: Statement filters to apply.
250
- connection: Optional connection override.
251
- schema_type: Optional schema class for the result.
252
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
253
-
254
- Returns:
255
- The first row of the query results.
256
- """
257
- connection = self._connection(connection)
258
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
259
- parameters = parameters or []
260
-
261
- result = await connection.fetch(sql, parameters=parameters)
262
-
263
- # Convert to dict and use ResultConverter
264
- dict_results = result.result()
265
- if not dict_results:
266
- self.check_not_found(None)
267
-
268
- return self.to_schema(dict_results[0], schema_type=schema_type)
269
-
270
- @overload
271
- async def select_one_or_none(
272
- self,
273
- sql: str,
274
- parameters: "Optional[StatementParameterType]" = None,
275
- *filters: "StatementFilter",
276
- connection: "Optional[PsqlpyConnection]" = None,
277
- schema_type: None = None,
278
- **kwargs: Any,
279
- ) -> "Optional[dict[str, Any]]": ...
280
- @overload
281
- async def select_one_or_none(
282
- self,
283
- sql: str,
284
- parameters: "Optional[StatementParameterType]" = None,
285
- *filters: "StatementFilter",
286
- connection: "Optional[PsqlpyConnection]" = None,
287
- schema_type: "type[ModelDTOT]",
288
- **kwargs: Any,
289
- ) -> "Optional[ModelDTOT]": ...
290
- async def select_one_or_none(
291
- self,
292
- sql: str,
293
- parameters: "Optional[StatementParameterType]" = None,
294
- *filters: "StatementFilter",
295
- connection: "Optional[PsqlpyConnection]" = None,
296
- schema_type: "Optional[type[ModelDTOT]]" = None,
297
- **kwargs: Any,
298
- ) -> "Optional[Union[ModelDTOT, dict[str, Any]]]":
299
- """Fetch one row from the database or return None if no rows found.
300
-
301
- Args:
302
- sql: The SQL query string.
303
- parameters: The parameters for the query (dict, tuple, list, or None).
304
- *filters: Statement filters to apply.
305
- connection: Optional connection override.
306
- schema_type: Optional schema class for the result.
307
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
308
-
309
- Returns:
310
- The first row of the query results, or None if no results found.
311
- """
312
- connection = self._connection(connection)
313
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
314
- parameters = parameters or []
315
-
316
- result = await connection.fetch(sql, parameters=parameters)
317
- dict_results = result.result()
318
-
319
- if not dict_results:
320
- return None
321
-
322
- return self.to_schema(dict_results[0], schema_type=schema_type)
323
-
324
- @overload
325
- async def select_value(
326
- self,
327
- sql: str,
328
- parameters: "Optional[StatementParameterType]" = None,
329
- *filters: "StatementFilter",
330
- connection: "Optional[PsqlpyConnection]" = None,
331
- schema_type: None = None,
332
- **kwargs: Any,
333
- ) -> "Any": ...
334
- @overload
335
- async def select_value(
336
- self,
337
- sql: str,
338
- parameters: "Optional[StatementParameterType]" = None,
339
- *filters: "StatementFilter",
340
- connection: "Optional[PsqlpyConnection]" = None,
341
- schema_type: "type[T]",
342
- **kwargs: Any,
343
- ) -> "T": ...
344
- async def select_value(
345
- self,
346
- sql: str,
347
- parameters: "Optional[StatementParameterType]" = None,
348
- *filters: "StatementFilter",
349
- connection: "Optional[PsqlpyConnection]" = None,
350
- schema_type: "Optional[type[T]]" = None,
351
- **kwargs: Any,
352
- ) -> "Union[T, Any]":
353
- """Fetch a single value from the database.
354
-
355
- Args:
356
- sql: The SQL query string.
357
- parameters: The parameters for the query (dict, tuple, list, or None).
358
- *filters: Statement filters to apply.
359
- connection: Optional connection override.
360
- schema_type: Optional type to convert the result to.
361
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
362
-
363
- Returns:
364
- The first value of the first row of the query results.
365
- """
366
- connection = self._connection(connection)
367
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
368
- parameters = parameters or []
369
-
370
- value = await connection.fetch_val(sql, parameters=parameters)
371
- value = self.check_not_found(value)
372
-
373
- if schema_type is None:
374
- return value
375
- return schema_type(value) # type: ignore[call-arg]
376
-
377
- @overload
378
- async def select_value_or_none(
379
- self,
380
- sql: str,
381
- parameters: "Optional[StatementParameterType]" = None,
382
- *filters: "StatementFilter",
383
- connection: "Optional[PsqlpyConnection]" = None,
384
- schema_type: None = None,
385
- **kwargs: Any,
386
- ) -> "Optional[Any]": ...
387
- @overload
388
- async def select_value_or_none(
389
- self,
390
- sql: str,
391
- parameters: "Optional[StatementParameterType]" = None,
392
- *filters: "StatementFilter",
393
- connection: "Optional[PsqlpyConnection]" = None,
394
- schema_type: "type[T]",
395
- **kwargs: Any,
396
- ) -> "Optional[T]": ...
397
- async def select_value_or_none(
398
- self,
399
- sql: str,
400
- parameters: "Optional[StatementParameterType]" = None,
401
- *filters: "StatementFilter",
402
- connection: "Optional[PsqlpyConnection]" = None,
403
- schema_type: "Optional[type[T]]" = None,
404
- **kwargs: Any,
405
- ) -> "Optional[Union[T, Any]]":
406
- """Fetch a single value or None if not found.
407
-
408
- Args:
409
- sql: The SQL query string.
410
- parameters: The parameters for the query (dict, tuple, list, or None).
411
- *filters: Statement filters to apply.
412
- connection: Optional connection override.
413
- schema_type: Optional type to convert the result to.
414
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
415
-
416
- Returns:
417
- The first value of the first row of the query results, or None if no results found.
418
- """
419
- connection = self._connection(connection)
420
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
421
- parameters = parameters or []
422
- try:
423
- value = await connection.fetch_val(sql, parameters=parameters)
424
- except RustPSQLDriverPyBaseError:
425
- return None
426
-
427
- if value is None:
428
- return None
429
- if schema_type is None:
430
- return value
431
- return schema_type(value) # type: ignore[call-arg]
432
-
433
- async def insert_update_delete(
434
- self,
435
- sql: str,
436
- parameters: "Optional[StatementParameterType]" = None,
437
- *filters: "StatementFilter",
438
- connection: "Optional[PsqlpyConnection]" = None,
439
- **kwargs: Any,
440
- ) -> int:
441
- """Execute an insert, update, or delete statement.
442
-
443
- Args:
444
- sql: The SQL statement to execute.
445
- parameters: The parameters for the statement (dict, tuple, list, or None).
446
- *filters: Statement filters to apply.
447
- connection: Optional connection override.
448
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
449
-
450
- Returns:
451
- The number of rows affected by the statement.
452
- """
453
- connection = self._connection(connection)
454
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
455
- parameters = parameters or []
456
-
457
- await connection.execute(sql, parameters=parameters)
458
- # For INSERT/UPDATE/DELETE, psqlpy returns an empty list but the operation succeeded
459
- # if no error was raised
460
- return 1
461
-
462
- @overload
463
- async def insert_update_delete_returning(
464
- self,
465
- sql: str,
466
- parameters: "Optional[StatementParameterType]" = None,
467
- *filters: "StatementFilter",
468
- connection: "Optional[PsqlpyConnection]" = None,
469
- schema_type: None = None,
470
- **kwargs: Any,
471
- ) -> "dict[str, Any]": ...
472
- @overload
473
- async def insert_update_delete_returning(
474
- self,
475
- sql: str,
476
- parameters: "Optional[StatementParameterType]" = None,
477
- *filters: "StatementFilter",
478
- connection: "Optional[PsqlpyConnection]" = None,
479
- schema_type: "type[ModelDTOT]",
480
- **kwargs: Any,
481
- ) -> "ModelDTOT": ...
482
- async def insert_update_delete_returning(
483
- self,
484
- sql: str,
485
- parameters: "Optional[StatementParameterType]" = None,
486
- *filters: "StatementFilter",
487
- connection: "Optional[PsqlpyConnection]" = None,
488
- schema_type: "Optional[type[ModelDTOT]]" = None,
489
- **kwargs: Any,
490
- ) -> "Union[ModelDTOT, dict[str, Any]]":
491
- """Insert, update, or delete data with RETURNING clause.
492
-
493
- Args:
494
- sql: The SQL statement with RETURNING clause.
495
- parameters: The parameters for the statement (dict, tuple, list, or None).
496
- *filters: Statement filters to apply.
497
- connection: Optional connection override.
498
- schema_type: Optional schema class for the result.
499
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
500
-
501
- Returns:
502
- The returned row data, as either a model instance or dictionary.
503
- """
504
- connection = self._connection(connection)
505
- sql, parameters = self._process_sql_params(sql, parameters, *filters, **kwargs)
506
- parameters = parameters or []
507
-
508
- result = await connection.fetch(sql, parameters=parameters)
509
-
510
- dict_results = result.result()
511
- if not dict_results:
512
- self.check_not_found(None)
513
-
514
- return self.to_schema(dict_results[0], schema_type=schema_type)
515
-
516
- async def execute_script(
517
- self,
518
- sql: str,
519
- parameters: "Optional[StatementParameterType]" = None,
520
- connection: "Optional[PsqlpyConnection]" = None,
521
- **kwargs: Any,
522
- ) -> str:
523
- """Execute a SQL script.
524
-
525
- Args:
526
- sql: The SQL script to execute.
527
- parameters: The parameters for the script (dict, tuple, list, or None).
528
- connection: Optional connection override.
529
- **kwargs: Additional keyword arguments to merge with parameters if parameters is a dict.
530
-
531
- Returns:
532
- A success message.
533
- """
534
- connection = self._connection(connection)
535
- sql, parameters = self._process_sql_params(sql, parameters, **kwargs)
536
- parameters = parameters or []
537
-
538
- await connection.execute(sql, parameters=parameters)
539
- return "Script executed successfully"
540
-
541
- def _connection(self, connection: "Optional[PsqlpyConnection]" = None) -> "PsqlpyConnection":
542
- """Get the connection to use.
543
-
544
- Args:
545
- connection: Optional connection to use. If not provided, use the default connection.
546
-
547
- Returns:
548
- The connection to use.
549
- """
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."""
550
214
  return connection or self.connection