sqlspec 0.17.0__py3-none-any.whl → 0.18.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.
- sqlspec/__init__.py +1 -1
- sqlspec/_sql.py +188 -234
- sqlspec/adapters/adbc/config.py +24 -30
- sqlspec/adapters/adbc/driver.py +42 -61
- sqlspec/adapters/aiosqlite/config.py +5 -10
- sqlspec/adapters/aiosqlite/driver.py +9 -25
- sqlspec/adapters/aiosqlite/pool.py +43 -35
- sqlspec/adapters/asyncmy/config.py +10 -7
- sqlspec/adapters/asyncmy/driver.py +18 -39
- sqlspec/adapters/asyncpg/config.py +4 -0
- sqlspec/adapters/asyncpg/driver.py +32 -79
- sqlspec/adapters/bigquery/config.py +12 -65
- sqlspec/adapters/bigquery/driver.py +39 -133
- sqlspec/adapters/duckdb/config.py +11 -15
- sqlspec/adapters/duckdb/driver.py +61 -85
- sqlspec/adapters/duckdb/pool.py +2 -5
- sqlspec/adapters/oracledb/_types.py +8 -1
- sqlspec/adapters/oracledb/config.py +55 -38
- sqlspec/adapters/oracledb/driver.py +35 -92
- sqlspec/adapters/oracledb/migrations.py +257 -0
- sqlspec/adapters/psqlpy/config.py +13 -9
- sqlspec/adapters/psqlpy/driver.py +28 -103
- sqlspec/adapters/psycopg/config.py +9 -5
- sqlspec/adapters/psycopg/driver.py +107 -175
- sqlspec/adapters/sqlite/config.py +7 -5
- sqlspec/adapters/sqlite/driver.py +37 -73
- sqlspec/adapters/sqlite/pool.py +3 -12
- sqlspec/base.py +1 -8
- sqlspec/builder/__init__.py +1 -1
- sqlspec/builder/_base.py +34 -20
- sqlspec/builder/_column.py +5 -1
- sqlspec/builder/_ddl.py +407 -183
- sqlspec/builder/_expression_wrappers.py +46 -0
- sqlspec/builder/_insert.py +2 -4
- sqlspec/builder/_update.py +5 -5
- sqlspec/builder/mixins/_insert_operations.py +26 -6
- sqlspec/builder/mixins/_merge_operations.py +1 -1
- sqlspec/builder/mixins/_order_limit_operations.py +16 -4
- sqlspec/builder/mixins/_select_operations.py +3 -7
- sqlspec/builder/mixins/_update_operations.py +4 -4
- sqlspec/config.py +32 -13
- sqlspec/core/__init__.py +89 -14
- sqlspec/core/cache.py +57 -104
- sqlspec/core/compiler.py +57 -112
- sqlspec/core/filters.py +1 -21
- sqlspec/core/hashing.py +13 -47
- sqlspec/core/parameters.py +272 -261
- sqlspec/core/result.py +12 -27
- sqlspec/core/splitter.py +17 -21
- sqlspec/core/statement.py +150 -159
- sqlspec/driver/_async.py +2 -15
- sqlspec/driver/_common.py +16 -95
- sqlspec/driver/_sync.py +2 -15
- sqlspec/driver/mixins/_result_tools.py +8 -29
- sqlspec/driver/mixins/_sql_translator.py +6 -8
- sqlspec/exceptions.py +1 -2
- sqlspec/loader.py +43 -115
- sqlspec/migrations/__init__.py +1 -1
- sqlspec/migrations/base.py +34 -45
- sqlspec/migrations/commands.py +34 -15
- sqlspec/migrations/loaders.py +1 -1
- sqlspec/migrations/runner.py +104 -19
- sqlspec/migrations/tracker.py +49 -2
- sqlspec/protocols.py +13 -6
- sqlspec/storage/__init__.py +4 -4
- sqlspec/storage/backends/fsspec.py +5 -6
- sqlspec/storage/backends/obstore.py +7 -8
- sqlspec/storage/registry.py +3 -3
- sqlspec/utils/__init__.py +2 -2
- sqlspec/utils/logging.py +6 -10
- sqlspec/utils/sync_tools.py +27 -4
- sqlspec/utils/text.py +6 -1
- {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/METADATA +1 -1
- sqlspec-0.18.0.dist-info/RECORD +138 -0
- sqlspec/builder/_ddl_utils.py +0 -103
- sqlspec-0.17.0.dist-info/RECORD +0 -137
- {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,25 +1,17 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
This driver
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
Architecture Features:
|
|
10
|
-
- Direct integration with sqlspec.core modules
|
|
11
|
-
- Enhanced PostgreSQL parameter processing with advanced type coercion
|
|
12
|
-
- PostgreSQL-specific features (COPY, arrays, JSON, advanced types)
|
|
13
|
-
- Thread-safe unified caching system
|
|
14
|
-
- MyPyC-optimized performance patterns
|
|
15
|
-
- Zero-copy data access where possible
|
|
1
|
+
"""PostgreSQL psycopg driver implementation.
|
|
2
|
+
|
|
3
|
+
This driver provides PostgreSQL database connectivity using psycopg3:
|
|
4
|
+
- SQL statement execution with parameter binding
|
|
5
|
+
- Connection and transaction management
|
|
6
|
+
- Row result processing with dictionary-based access
|
|
7
|
+
- PostgreSQL-specific features (COPY, arrays, JSON types)
|
|
16
8
|
|
|
17
9
|
PostgreSQL Features:
|
|
18
|
-
-
|
|
19
|
-
- PostgreSQL array support
|
|
20
|
-
- COPY operations
|
|
10
|
+
- Parameter styles ($1, %s, %(name)s)
|
|
11
|
+
- PostgreSQL array support
|
|
12
|
+
- COPY operations for bulk data transfer
|
|
21
13
|
- JSON/JSONB type handling
|
|
22
|
-
- PostgreSQL-specific error
|
|
14
|
+
- PostgreSQL-specific error handling
|
|
23
15
|
"""
|
|
24
16
|
|
|
25
17
|
import io
|
|
@@ -44,7 +36,7 @@ if TYPE_CHECKING:
|
|
|
44
36
|
|
|
45
37
|
logger = get_logger("adapters.psycopg")
|
|
46
38
|
|
|
47
|
-
|
|
39
|
+
|
|
48
40
|
TRANSACTION_STATUS_IDLE = 0
|
|
49
41
|
TRANSACTION_STATUS_ACTIVE = 1
|
|
50
42
|
TRANSACTION_STATUS_INTRANS = 2
|
|
@@ -53,7 +45,7 @@ TRANSACTION_STATUS_UNKNOWN = 4
|
|
|
53
45
|
|
|
54
46
|
|
|
55
47
|
def _convert_list_to_postgres_array(value: Any) -> str:
|
|
56
|
-
"""Convert Python list to PostgreSQL array literal format
|
|
48
|
+
"""Convert Python list to PostgreSQL array literal format.
|
|
57
49
|
|
|
58
50
|
Args:
|
|
59
51
|
value: Python list to convert
|
|
@@ -64,13 +56,11 @@ def _convert_list_to_postgres_array(value: Any) -> str:
|
|
|
64
56
|
if not isinstance(value, list):
|
|
65
57
|
return str(value)
|
|
66
58
|
|
|
67
|
-
# Handle nested arrays and complex types
|
|
68
59
|
elements = []
|
|
69
60
|
for item in value:
|
|
70
61
|
if isinstance(item, list):
|
|
71
62
|
elements.append(_convert_list_to_postgres_array(item))
|
|
72
63
|
elif isinstance(item, str):
|
|
73
|
-
# Escape quotes and handle special characters
|
|
74
64
|
escaped = item.replace("'", "''")
|
|
75
65
|
elements.append(f"'{escaped}'")
|
|
76
66
|
elif item is None:
|
|
@@ -81,7 +71,6 @@ def _convert_list_to_postgres_array(value: Any) -> str:
|
|
|
81
71
|
return f"{{{','.join(elements)}}}"
|
|
82
72
|
|
|
83
73
|
|
|
84
|
-
# Enhanced PostgreSQL statement configuration using core modules with performance optimizations
|
|
85
74
|
psycopg_statement_config = StatementConfig(
|
|
86
75
|
dialect="postgres",
|
|
87
76
|
pre_process_steps=None,
|
|
@@ -105,12 +94,7 @@ psycopg_statement_config = StatementConfig(
|
|
|
105
94
|
ParameterStyle.NAMED_PYFORMAT,
|
|
106
95
|
ParameterStyle.NUMERIC,
|
|
107
96
|
},
|
|
108
|
-
type_coercion_map={
|
|
109
|
-
dict: to_json
|
|
110
|
-
# Note: Psycopg3 handles Python lists natively, so no conversion needed
|
|
111
|
-
# list: _convert_list_to_postgres_array,
|
|
112
|
-
# tuple: lambda v: _convert_list_to_postgres_array(list(v)),
|
|
113
|
-
},
|
|
97
|
+
type_coercion_map={dict: to_json},
|
|
114
98
|
has_native_list_expansion=True,
|
|
115
99
|
needs_static_script_compilation=False,
|
|
116
100
|
preserve_parameter_format=True,
|
|
@@ -129,7 +113,7 @@ __all__ = (
|
|
|
129
113
|
|
|
130
114
|
|
|
131
115
|
class PsycopgSyncCursor:
|
|
132
|
-
"""Context manager for PostgreSQL psycopg cursor management
|
|
116
|
+
"""Context manager for PostgreSQL psycopg cursor management."""
|
|
133
117
|
|
|
134
118
|
__slots__ = ("connection", "cursor")
|
|
135
119
|
|
|
@@ -142,13 +126,13 @@ class PsycopgSyncCursor:
|
|
|
142
126
|
return self.cursor
|
|
143
127
|
|
|
144
128
|
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
145
|
-
_ = (exc_type, exc_val, exc_tb)
|
|
129
|
+
_ = (exc_type, exc_val, exc_tb)
|
|
146
130
|
if self.cursor is not None:
|
|
147
131
|
self.cursor.close()
|
|
148
132
|
|
|
149
133
|
|
|
150
134
|
class PsycopgSyncExceptionHandler:
|
|
151
|
-
"""
|
|
135
|
+
"""Context manager for handling PostgreSQL psycopg database exceptions."""
|
|
152
136
|
|
|
153
137
|
__slots__ = ()
|
|
154
138
|
|
|
@@ -194,35 +178,19 @@ class PsycopgSyncExceptionHandler:
|
|
|
194
178
|
|
|
195
179
|
|
|
196
180
|
class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
197
|
-
"""
|
|
198
|
-
|
|
199
|
-
This driver leverages the complete core module system for maximum PostgreSQL performance:
|
|
181
|
+
"""PostgreSQL psycopg synchronous driver.
|
|
200
182
|
|
|
201
|
-
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
- Zero-copy parameter processing where possible
|
|
183
|
+
Provides synchronous database operations for PostgreSQL using psycopg3:
|
|
184
|
+
- SQL statement execution with parameter binding
|
|
185
|
+
- Transaction management (begin, commit, rollback)
|
|
186
|
+
- Result processing with column metadata
|
|
187
|
+
- PostgreSQL-specific features support
|
|
207
188
|
|
|
208
189
|
PostgreSQL Features:
|
|
209
|
-
-
|
|
210
|
-
- PostgreSQL
|
|
211
|
-
- COPY operations
|
|
212
|
-
-
|
|
213
|
-
- PostgreSQL-specific error categorization
|
|
214
|
-
|
|
215
|
-
Core Integration Features:
|
|
216
|
-
- sqlspec.core.statement for enhanced SQL processing
|
|
217
|
-
- sqlspec.core.parameters for optimized parameter handling
|
|
218
|
-
- sqlspec.core.cache for unified statement caching
|
|
219
|
-
- sqlspec.core.config for centralized configuration management
|
|
220
|
-
|
|
221
|
-
Compatibility:
|
|
222
|
-
- 100% backward compatibility with existing psycopg driver interface
|
|
223
|
-
- All existing PostgreSQL tests pass without modification
|
|
224
|
-
- Complete StatementConfig API compatibility
|
|
225
|
-
- Preserved cursor management and exception handling patterns
|
|
190
|
+
- Parameter styles ($1, %s, %(name)s)
|
|
191
|
+
- PostgreSQL arrays and JSON handling
|
|
192
|
+
- COPY operations for bulk data transfer
|
|
193
|
+
- PostgreSQL-specific error handling
|
|
226
194
|
"""
|
|
227
195
|
|
|
228
196
|
__slots__ = ()
|
|
@@ -234,33 +202,28 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
234
202
|
statement_config: "Optional[StatementConfig]" = None,
|
|
235
203
|
driver_features: "Optional[dict[str, Any]]" = None,
|
|
236
204
|
) -> None:
|
|
237
|
-
# Enhanced configuration with global settings integration
|
|
238
205
|
if statement_config is None:
|
|
239
206
|
cache_config = get_cache_config()
|
|
240
|
-
|
|
207
|
+
default_config = psycopg_statement_config.replace(
|
|
241
208
|
enable_caching=cache_config.compiled_cache_enabled,
|
|
242
|
-
enable_parsing=True,
|
|
243
|
-
enable_validation=True,
|
|
244
|
-
dialect="postgres",
|
|
209
|
+
enable_parsing=True,
|
|
210
|
+
enable_validation=True,
|
|
211
|
+
dialect="postgres",
|
|
245
212
|
)
|
|
246
|
-
statement_config =
|
|
213
|
+
statement_config = default_config
|
|
247
214
|
|
|
248
215
|
super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
|
|
249
216
|
|
|
250
217
|
def with_cursor(self, connection: PsycopgSyncConnection) -> PsycopgSyncCursor:
|
|
251
|
-
"""Create context manager for PostgreSQL cursor
|
|
218
|
+
"""Create context manager for PostgreSQL cursor."""
|
|
252
219
|
return PsycopgSyncCursor(connection)
|
|
253
220
|
|
|
254
221
|
def begin(self) -> None:
|
|
255
222
|
"""Begin a database transaction on the current connection."""
|
|
256
223
|
try:
|
|
257
|
-
# psycopg3 has explicit transaction support
|
|
258
|
-
# If already in a transaction, this is a no-op
|
|
259
224
|
if hasattr(self.connection, "autocommit") and not self.connection.autocommit:
|
|
260
|
-
# Already in manual commit mode, just ensure we're in a clean state
|
|
261
225
|
pass
|
|
262
226
|
else:
|
|
263
|
-
# Start manual transaction mode
|
|
264
227
|
self.connection.autocommit = False
|
|
265
228
|
except Exception as e:
|
|
266
229
|
msg = f"Failed to begin transaction: {e}"
|
|
@@ -287,17 +250,15 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
287
250
|
return PsycopgSyncExceptionHandler()
|
|
288
251
|
|
|
289
252
|
def _handle_transaction_error_cleanup(self) -> None:
|
|
290
|
-
"""Handle transaction cleanup after database errors
|
|
253
|
+
"""Handle transaction cleanup after database errors."""
|
|
291
254
|
try:
|
|
292
|
-
# Check if connection is in a failed transaction state
|
|
293
255
|
if hasattr(self.connection, "info") and hasattr(self.connection.info, "transaction_status"):
|
|
294
256
|
status = self.connection.info.transaction_status
|
|
295
|
-
|
|
257
|
+
|
|
296
258
|
if status == TRANSACTION_STATUS_INERROR:
|
|
297
259
|
logger.debug("Connection in aborted transaction state, performing rollback")
|
|
298
260
|
self.connection.rollback()
|
|
299
261
|
except Exception as cleanup_error:
|
|
300
|
-
# If cleanup fails, log but don't raise - the original error is more important
|
|
301
262
|
logger.warning("Failed to cleanup transaction state: %s", cleanup_error)
|
|
302
263
|
|
|
303
264
|
def _try_special_handling(self, cursor: Any, statement: "SQL") -> "Optional[SQLResult]":
|
|
@@ -310,14 +271,12 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
310
271
|
Returns:
|
|
311
272
|
SQLResult if special handling was applied, None otherwise
|
|
312
273
|
"""
|
|
313
|
-
|
|
274
|
+
|
|
314
275
|
statement.compile()
|
|
315
276
|
|
|
316
|
-
# Use the operation_type from the statement object
|
|
317
277
|
if statement.operation_type in {"COPY_FROM", "COPY_TO"}:
|
|
318
278
|
return self._handle_copy_operation(cursor, statement)
|
|
319
279
|
|
|
320
|
-
# No special handling needed - proceed with standard execution
|
|
321
280
|
return None
|
|
322
281
|
|
|
323
282
|
def _handle_copy_operation(self, cursor: Any, statement: "SQL") -> "SQLResult":
|
|
@@ -330,27 +289,21 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
330
289
|
Returns:
|
|
331
290
|
SQLResult with COPY operation results
|
|
332
291
|
"""
|
|
333
|
-
|
|
292
|
+
|
|
334
293
|
sql = statement.sql
|
|
335
294
|
|
|
336
|
-
# Get COPY data from parameters - handle both direct value and list format
|
|
337
295
|
copy_data = statement.parameters
|
|
338
296
|
if isinstance(copy_data, list) and len(copy_data) == 1:
|
|
339
297
|
copy_data = copy_data[0]
|
|
340
298
|
|
|
341
|
-
# Use the operation_type from the statement
|
|
342
299
|
if statement.operation_type == "COPY_FROM":
|
|
343
|
-
# COPY FROM STDIN - import data
|
|
344
300
|
if isinstance(copy_data, (str, bytes)):
|
|
345
301
|
data_file = io.StringIO(copy_data) if isinstance(copy_data, str) else io.BytesIO(copy_data)
|
|
346
302
|
elif hasattr(copy_data, "read"):
|
|
347
|
-
# Already a file-like object
|
|
348
303
|
data_file = copy_data
|
|
349
304
|
else:
|
|
350
|
-
# Convert to string representation
|
|
351
305
|
data_file = io.StringIO(str(copy_data))
|
|
352
306
|
|
|
353
|
-
# Use context manager for COPY FROM (sync version)
|
|
354
307
|
with cursor.copy(sql) as copy_ctx:
|
|
355
308
|
data_to_write = data_file.read() if hasattr(data_file, "read") else str(copy_data) # pyright: ignore
|
|
356
309
|
if isinstance(data_to_write, str):
|
|
@@ -364,7 +317,6 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
364
317
|
)
|
|
365
318
|
|
|
366
319
|
if statement.operation_type == "COPY_TO":
|
|
367
|
-
# COPY TO STDOUT - export data
|
|
368
320
|
output_data: list[str] = []
|
|
369
321
|
with cursor.copy(sql) as copy_ctx:
|
|
370
322
|
output_data.extend(row.decode() if isinstance(row, bytes) else str(row) for row in copy_ctx)
|
|
@@ -372,13 +324,12 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
372
324
|
exported_data = "".join(output_data)
|
|
373
325
|
|
|
374
326
|
return SQLResult(
|
|
375
|
-
data=[{"copy_output": exported_data}],
|
|
327
|
+
data=[{"copy_output": exported_data}],
|
|
376
328
|
rows_affected=0,
|
|
377
329
|
statement=statement,
|
|
378
330
|
metadata={"copy_operation": "TO_STDOUT"},
|
|
379
331
|
)
|
|
380
332
|
|
|
381
|
-
# Regular COPY with file - execute normally
|
|
382
333
|
cursor.execute(sql)
|
|
383
334
|
rows_affected = max(cursor.rowcount, 0)
|
|
384
335
|
|
|
@@ -387,10 +338,14 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
387
338
|
)
|
|
388
339
|
|
|
389
340
|
def _execute_script(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
|
|
390
|
-
"""Execute SQL script
|
|
341
|
+
"""Execute SQL script with multiple statements.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
cursor: Database cursor
|
|
345
|
+
statement: SQL statement containing multiple commands
|
|
391
346
|
|
|
392
|
-
|
|
393
|
-
|
|
347
|
+
Returns:
|
|
348
|
+
ExecutionResult with script execution details
|
|
394
349
|
"""
|
|
395
350
|
sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
|
|
396
351
|
statements = self.split_script_statements(sql, statement.statement_config, strip_trailing_semicolon=True)
|
|
@@ -399,7 +354,6 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
399
354
|
last_cursor = cursor
|
|
400
355
|
|
|
401
356
|
for stmt in statements:
|
|
402
|
-
# Only pass parameters if they exist - psycopg treats empty containers as parameterized mode
|
|
403
357
|
if prepared_parameters:
|
|
404
358
|
cursor.execute(stmt, prepared_parameters)
|
|
405
359
|
else:
|
|
@@ -411,42 +365,47 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
411
365
|
)
|
|
412
366
|
|
|
413
367
|
def _execute_many(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
|
|
414
|
-
"""Execute SQL with multiple parameter sets
|
|
368
|
+
"""Execute SQL with multiple parameter sets.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
cursor: Database cursor
|
|
372
|
+
statement: SQL statement with parameter list
|
|
415
373
|
|
|
416
|
-
|
|
374
|
+
Returns:
|
|
375
|
+
ExecutionResult with batch execution details
|
|
417
376
|
"""
|
|
418
377
|
sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
|
|
419
378
|
|
|
420
|
-
# Handle empty parameter list case
|
|
421
379
|
if not prepared_parameters:
|
|
422
|
-
# For empty parameter list, return a result with no rows affected
|
|
423
380
|
return self.create_execution_result(cursor, rowcount_override=0, is_many_result=True)
|
|
424
381
|
|
|
425
382
|
cursor.executemany(sql, prepared_parameters)
|
|
426
383
|
|
|
427
|
-
# PostgreSQL cursor.rowcount gives total affected rows
|
|
428
384
|
affected_rows = cursor.rowcount if cursor.rowcount and cursor.rowcount > 0 else 0
|
|
429
385
|
|
|
430
386
|
return self.create_execution_result(cursor, rowcount_override=affected_rows, is_many_result=True)
|
|
431
387
|
|
|
432
388
|
def _execute_statement(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
|
|
433
|
-
"""Execute single SQL statement
|
|
389
|
+
"""Execute single SQL statement.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
cursor: Database cursor
|
|
393
|
+
statement: SQL statement to execute
|
|
434
394
|
|
|
435
|
-
|
|
395
|
+
Returns:
|
|
396
|
+
ExecutionResult with statement execution details
|
|
436
397
|
"""
|
|
437
398
|
sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
|
|
438
|
-
|
|
399
|
+
|
|
439
400
|
if prepared_parameters:
|
|
440
401
|
cursor.execute(sql, prepared_parameters)
|
|
441
402
|
else:
|
|
442
403
|
cursor.execute(sql)
|
|
443
404
|
|
|
444
|
-
# Enhanced SELECT result processing for PostgreSQL
|
|
445
405
|
if statement.returns_rows():
|
|
446
406
|
fetched_data = cursor.fetchall()
|
|
447
407
|
column_names = [col.name for col in cursor.description or []]
|
|
448
408
|
|
|
449
|
-
# PostgreSQL returns raw data - pass it directly like the old driver
|
|
450
409
|
return self.create_execution_result(
|
|
451
410
|
cursor,
|
|
452
411
|
selected_data=fetched_data,
|
|
@@ -455,13 +414,12 @@ class PsycopgSyncDriver(SyncDriverAdapterBase):
|
|
|
455
414
|
is_select_result=True,
|
|
456
415
|
)
|
|
457
416
|
|
|
458
|
-
# Enhanced non-SELECT result processing for PostgreSQL
|
|
459
417
|
affected_rows = cursor.rowcount if cursor.rowcount and cursor.rowcount > 0 else 0
|
|
460
418
|
return self.create_execution_result(cursor, rowcount_override=affected_rows)
|
|
461
419
|
|
|
462
420
|
|
|
463
421
|
class PsycopgAsyncCursor:
|
|
464
|
-
"""Async context manager for PostgreSQL psycopg cursor management
|
|
422
|
+
"""Async context manager for PostgreSQL psycopg cursor management."""
|
|
465
423
|
|
|
466
424
|
__slots__ = ("connection", "cursor")
|
|
467
425
|
|
|
@@ -474,13 +432,13 @@ class PsycopgAsyncCursor:
|
|
|
474
432
|
return self.cursor
|
|
475
433
|
|
|
476
434
|
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
477
|
-
_ = (exc_type, exc_val, exc_tb)
|
|
435
|
+
_ = (exc_type, exc_val, exc_tb)
|
|
478
436
|
if self.cursor is not None:
|
|
479
437
|
await self.cursor.close()
|
|
480
438
|
|
|
481
439
|
|
|
482
440
|
class PsycopgAsyncExceptionHandler:
|
|
483
|
-
"""
|
|
441
|
+
"""Async context manager for handling PostgreSQL psycopg database exceptions."""
|
|
484
442
|
|
|
485
443
|
__slots__ = ()
|
|
486
444
|
|
|
@@ -526,37 +484,20 @@ class PsycopgAsyncExceptionHandler:
|
|
|
526
484
|
|
|
527
485
|
|
|
528
486
|
class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
529
|
-
"""
|
|
530
|
-
|
|
531
|
-
This async driver leverages the complete core module system for maximum PostgreSQL performance:
|
|
487
|
+
"""PostgreSQL psycopg asynchronous driver.
|
|
532
488
|
|
|
533
|
-
|
|
534
|
-
-
|
|
535
|
-
-
|
|
536
|
-
-
|
|
537
|
-
-
|
|
538
|
-
- Zero-copy parameter processing where possible
|
|
539
|
-
- Async-optimized resource management
|
|
489
|
+
Provides asynchronous database operations for PostgreSQL using psycopg3:
|
|
490
|
+
- Async SQL statement execution with parameter binding
|
|
491
|
+
- Async transaction management (begin, commit, rollback)
|
|
492
|
+
- Async result processing with column metadata
|
|
493
|
+
- PostgreSQL-specific features support
|
|
540
494
|
|
|
541
495
|
PostgreSQL Features:
|
|
542
|
-
-
|
|
543
|
-
- PostgreSQL
|
|
544
|
-
- COPY operations
|
|
545
|
-
-
|
|
546
|
-
- PostgreSQL-specific error categorization
|
|
496
|
+
- Parameter styles ($1, %s, %(name)s)
|
|
497
|
+
- PostgreSQL arrays and JSON handling
|
|
498
|
+
- COPY operations for bulk data transfer
|
|
499
|
+
- PostgreSQL-specific error handling
|
|
547
500
|
- Async pub/sub support (LISTEN/NOTIFY)
|
|
548
|
-
|
|
549
|
-
Core Integration Features:
|
|
550
|
-
- sqlspec.core.statement for enhanced SQL processing
|
|
551
|
-
- sqlspec.core.parameters for optimized parameter handling
|
|
552
|
-
- sqlspec.core.cache for unified statement caching
|
|
553
|
-
- sqlspec.core.config for centralized configuration management
|
|
554
|
-
|
|
555
|
-
Compatibility:
|
|
556
|
-
- 100% backward compatibility with existing async psycopg driver interface
|
|
557
|
-
- All existing async PostgreSQL tests pass without modification
|
|
558
|
-
- Complete StatementConfig API compatibility
|
|
559
|
-
- Preserved async cursor management and exception handling patterns
|
|
560
501
|
"""
|
|
561
502
|
|
|
562
503
|
__slots__ = ()
|
|
@@ -568,33 +509,28 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
568
509
|
statement_config: "Optional[StatementConfig]" = None,
|
|
569
510
|
driver_features: "Optional[dict[str, Any]]" = None,
|
|
570
511
|
) -> None:
|
|
571
|
-
# Enhanced configuration with global settings integration
|
|
572
512
|
if statement_config is None:
|
|
573
513
|
cache_config = get_cache_config()
|
|
574
|
-
|
|
514
|
+
default_config = psycopg_statement_config.replace(
|
|
575
515
|
enable_caching=cache_config.compiled_cache_enabled,
|
|
576
|
-
enable_parsing=True,
|
|
577
|
-
enable_validation=True,
|
|
578
|
-
dialect="postgres",
|
|
516
|
+
enable_parsing=True,
|
|
517
|
+
enable_validation=True,
|
|
518
|
+
dialect="postgres",
|
|
579
519
|
)
|
|
580
|
-
statement_config =
|
|
520
|
+
statement_config = default_config
|
|
581
521
|
|
|
582
522
|
super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
|
|
583
523
|
|
|
584
524
|
def with_cursor(self, connection: "PsycopgAsyncConnection") -> "PsycopgAsyncCursor":
|
|
585
|
-
"""Create async context manager for PostgreSQL cursor
|
|
525
|
+
"""Create async context manager for PostgreSQL cursor."""
|
|
586
526
|
return PsycopgAsyncCursor(connection)
|
|
587
527
|
|
|
588
528
|
async def begin(self) -> None:
|
|
589
529
|
"""Begin a database transaction on the current connection."""
|
|
590
530
|
try:
|
|
591
|
-
# psycopg3 has explicit transaction support
|
|
592
|
-
# If already in a transaction, this is a no-op
|
|
593
531
|
if hasattr(self.connection, "autocommit") and not self.connection.autocommit:
|
|
594
|
-
# Already in manual commit mode, just ensure we're in a clean state
|
|
595
532
|
pass
|
|
596
533
|
else:
|
|
597
|
-
# Start manual transaction mode
|
|
598
534
|
self.connection.autocommit = False
|
|
599
535
|
except Exception as e:
|
|
600
536
|
msg = f"Failed to begin transaction: {e}"
|
|
@@ -621,17 +557,15 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
621
557
|
return PsycopgAsyncExceptionHandler()
|
|
622
558
|
|
|
623
559
|
async def _handle_transaction_error_cleanup_async(self) -> None:
|
|
624
|
-
"""Handle transaction cleanup after database errors
|
|
560
|
+
"""Handle async transaction cleanup after database errors."""
|
|
625
561
|
try:
|
|
626
|
-
# Check if connection is in a failed transaction state
|
|
627
562
|
if hasattr(self.connection, "info") and hasattr(self.connection.info, "transaction_status"):
|
|
628
563
|
status = self.connection.info.transaction_status
|
|
629
|
-
|
|
564
|
+
|
|
630
565
|
if status == TRANSACTION_STATUS_INERROR:
|
|
631
566
|
logger.debug("Connection in aborted transaction state, performing async rollback")
|
|
632
567
|
await self.connection.rollback()
|
|
633
568
|
except Exception as cleanup_error:
|
|
634
|
-
# If cleanup fails, log but don't raise - the original error is more important
|
|
635
569
|
logger.warning("Failed to cleanup transaction state: %s", cleanup_error)
|
|
636
570
|
|
|
637
571
|
async def _try_special_handling(self, cursor: Any, statement: "SQL") -> "Optional[SQLResult]":
|
|
@@ -644,16 +578,15 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
644
578
|
Returns:
|
|
645
579
|
SQLResult if special handling was applied, None otherwise
|
|
646
580
|
"""
|
|
647
|
-
|
|
581
|
+
|
|
648
582
|
sql_upper = statement.sql.strip().upper()
|
|
649
583
|
if sql_upper.startswith("COPY ") and ("FROM STDIN" in sql_upper or "TO STDOUT" in sql_upper):
|
|
650
584
|
return await self._handle_copy_operation_async(cursor, statement)
|
|
651
585
|
|
|
652
|
-
# No special handling needed - proceed with standard execution
|
|
653
586
|
return None
|
|
654
587
|
|
|
655
588
|
async def _handle_copy_operation_async(self, cursor: Any, statement: "SQL") -> "SQLResult":
|
|
656
|
-
"""Handle PostgreSQL COPY operations
|
|
589
|
+
"""Handle PostgreSQL COPY operations (async).
|
|
657
590
|
|
|
658
591
|
Args:
|
|
659
592
|
cursor: Psycopg async cursor object
|
|
@@ -662,31 +595,25 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
662
595
|
Returns:
|
|
663
596
|
SQLResult with COPY operation results
|
|
664
597
|
"""
|
|
665
|
-
|
|
598
|
+
|
|
666
599
|
sql = statement.sql
|
|
667
600
|
|
|
668
|
-
# Get COPY data from parameters - handle both direct value and list format
|
|
669
601
|
copy_data = statement.parameters
|
|
670
602
|
if isinstance(copy_data, list) and len(copy_data) == 1:
|
|
671
603
|
copy_data = copy_data[0]
|
|
672
604
|
|
|
673
|
-
# Simple string-based direction detection
|
|
674
605
|
sql_upper = sql.upper()
|
|
675
606
|
is_stdin = "FROM STDIN" in sql_upper
|
|
676
607
|
is_stdout = "TO STDOUT" in sql_upper
|
|
677
608
|
|
|
678
609
|
if is_stdin:
|
|
679
|
-
# COPY FROM STDIN - import data
|
|
680
610
|
if isinstance(copy_data, (str, bytes)):
|
|
681
611
|
data_file = io.StringIO(copy_data) if isinstance(copy_data, str) else io.BytesIO(copy_data)
|
|
682
612
|
elif hasattr(copy_data, "read"):
|
|
683
|
-
# Already a file-like object
|
|
684
613
|
data_file = copy_data
|
|
685
614
|
else:
|
|
686
|
-
# Convert to string representation
|
|
687
615
|
data_file = io.StringIO(str(copy_data))
|
|
688
616
|
|
|
689
|
-
# Use async context manager for COPY FROM
|
|
690
617
|
async with cursor.copy(sql) as copy_ctx:
|
|
691
618
|
data_to_write = data_file.read() if hasattr(data_file, "read") else str(copy_data) # pyright: ignore
|
|
692
619
|
if isinstance(data_to_write, str):
|
|
@@ -700,7 +627,6 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
700
627
|
)
|
|
701
628
|
|
|
702
629
|
if is_stdout:
|
|
703
|
-
# COPY TO STDOUT - export data
|
|
704
630
|
output_data: list[str] = []
|
|
705
631
|
async with cursor.copy(sql) as copy_ctx:
|
|
706
632
|
output_data.extend([row.decode() if isinstance(row, bytes) else str(row) async for row in copy_ctx])
|
|
@@ -708,13 +634,12 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
708
634
|
exported_data = "".join(output_data)
|
|
709
635
|
|
|
710
636
|
return SQLResult(
|
|
711
|
-
data=[{"copy_output": exported_data}],
|
|
637
|
+
data=[{"copy_output": exported_data}],
|
|
712
638
|
rows_affected=0,
|
|
713
639
|
statement=statement,
|
|
714
640
|
metadata={"copy_operation": "TO_STDOUT"},
|
|
715
641
|
)
|
|
716
642
|
|
|
717
|
-
# Regular COPY with file - execute normally
|
|
718
643
|
await cursor.execute(sql)
|
|
719
644
|
rows_affected = max(cursor.rowcount, 0)
|
|
720
645
|
|
|
@@ -723,10 +648,14 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
723
648
|
)
|
|
724
649
|
|
|
725
650
|
async def _execute_script(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
|
|
726
|
-
"""Execute SQL script
|
|
651
|
+
"""Execute SQL script with multiple statements (async).
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
cursor: Database cursor
|
|
655
|
+
statement: SQL statement containing multiple commands
|
|
727
656
|
|
|
728
|
-
|
|
729
|
-
|
|
657
|
+
Returns:
|
|
658
|
+
ExecutionResult with script execution details
|
|
730
659
|
"""
|
|
731
660
|
sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
|
|
732
661
|
statements = self.split_script_statements(sql, statement.statement_config, strip_trailing_semicolon=True)
|
|
@@ -735,7 +664,6 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
735
664
|
last_cursor = cursor
|
|
736
665
|
|
|
737
666
|
for stmt in statements:
|
|
738
|
-
# Only pass parameters if they exist - psycopg treats empty containers as parameterized mode
|
|
739
667
|
if prepared_parameters:
|
|
740
668
|
await cursor.execute(stmt, prepared_parameters)
|
|
741
669
|
else:
|
|
@@ -747,42 +675,47 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
747
675
|
)
|
|
748
676
|
|
|
749
677
|
async def _execute_many(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
|
|
750
|
-
"""Execute SQL with multiple parameter sets
|
|
678
|
+
"""Execute SQL with multiple parameter sets (async).
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
cursor: Database cursor
|
|
682
|
+
statement: SQL statement with parameter list
|
|
751
683
|
|
|
752
|
-
|
|
684
|
+
Returns:
|
|
685
|
+
ExecutionResult with batch execution details
|
|
753
686
|
"""
|
|
754
687
|
sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
|
|
755
688
|
|
|
756
|
-
# Handle empty parameter list case
|
|
757
689
|
if not prepared_parameters:
|
|
758
|
-
# For empty parameter list, return a result with no rows affected
|
|
759
690
|
return self.create_execution_result(cursor, rowcount_override=0, is_many_result=True)
|
|
760
691
|
|
|
761
692
|
await cursor.executemany(sql, prepared_parameters)
|
|
762
693
|
|
|
763
|
-
# PostgreSQL cursor.rowcount gives total affected rows
|
|
764
694
|
affected_rows = cursor.rowcount if cursor.rowcount and cursor.rowcount > 0 else 0
|
|
765
695
|
|
|
766
696
|
return self.create_execution_result(cursor, rowcount_override=affected_rows, is_many_result=True)
|
|
767
697
|
|
|
768
698
|
async def _execute_statement(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
|
|
769
|
-
"""Execute single SQL statement
|
|
699
|
+
"""Execute single SQL statement (async).
|
|
770
700
|
|
|
771
|
-
|
|
701
|
+
Args:
|
|
702
|
+
cursor: Database cursor
|
|
703
|
+
statement: SQL statement to execute
|
|
704
|
+
|
|
705
|
+
Returns:
|
|
706
|
+
ExecutionResult with statement execution details
|
|
772
707
|
"""
|
|
773
708
|
sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
|
|
774
|
-
|
|
709
|
+
|
|
775
710
|
if prepared_parameters:
|
|
776
711
|
await cursor.execute(sql, prepared_parameters)
|
|
777
712
|
else:
|
|
778
713
|
await cursor.execute(sql)
|
|
779
714
|
|
|
780
|
-
# Enhanced SELECT result processing for PostgreSQL
|
|
781
715
|
if statement.returns_rows():
|
|
782
716
|
fetched_data = await cursor.fetchall()
|
|
783
717
|
column_names = [col.name for col in cursor.description or []]
|
|
784
718
|
|
|
785
|
-
# PostgreSQL returns raw data - pass it directly like the old driver
|
|
786
719
|
return self.create_execution_result(
|
|
787
720
|
cursor,
|
|
788
721
|
selected_data=fetched_data,
|
|
@@ -791,6 +724,5 @@ class PsycopgAsyncDriver(AsyncDriverAdapterBase):
|
|
|
791
724
|
is_select_result=True,
|
|
792
725
|
)
|
|
793
726
|
|
|
794
|
-
# Enhanced non-SELECT result processing for PostgreSQL
|
|
795
727
|
affected_rows = cursor.rowcount if cursor.rowcount and cursor.rowcount > 0 else 0
|
|
796
728
|
return self.create_execution_result(cursor, rowcount_override=affected_rows)
|