sqlspec 0.17.1__py3-none-any.whl → 0.19.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 (77) hide show
  1. sqlspec/__init__.py +1 -1
  2. sqlspec/_sql.py +54 -159
  3. sqlspec/adapters/adbc/config.py +24 -30
  4. sqlspec/adapters/adbc/driver.py +42 -61
  5. sqlspec/adapters/aiosqlite/config.py +5 -10
  6. sqlspec/adapters/aiosqlite/driver.py +9 -25
  7. sqlspec/adapters/aiosqlite/pool.py +43 -35
  8. sqlspec/adapters/asyncmy/config.py +10 -7
  9. sqlspec/adapters/asyncmy/driver.py +18 -39
  10. sqlspec/adapters/asyncpg/config.py +4 -0
  11. sqlspec/adapters/asyncpg/driver.py +32 -79
  12. sqlspec/adapters/bigquery/config.py +12 -65
  13. sqlspec/adapters/bigquery/driver.py +39 -133
  14. sqlspec/adapters/duckdb/config.py +11 -15
  15. sqlspec/adapters/duckdb/driver.py +61 -85
  16. sqlspec/adapters/duckdb/pool.py +2 -5
  17. sqlspec/adapters/oracledb/_types.py +8 -1
  18. sqlspec/adapters/oracledb/config.py +55 -38
  19. sqlspec/adapters/oracledb/driver.py +35 -92
  20. sqlspec/adapters/oracledb/migrations.py +257 -0
  21. sqlspec/adapters/psqlpy/config.py +13 -9
  22. sqlspec/adapters/psqlpy/driver.py +28 -103
  23. sqlspec/adapters/psycopg/config.py +9 -5
  24. sqlspec/adapters/psycopg/driver.py +107 -175
  25. sqlspec/adapters/sqlite/config.py +7 -5
  26. sqlspec/adapters/sqlite/driver.py +37 -73
  27. sqlspec/adapters/sqlite/pool.py +3 -12
  28. sqlspec/base.py +19 -22
  29. sqlspec/builder/__init__.py +1 -1
  30. sqlspec/builder/_base.py +34 -20
  31. sqlspec/builder/_ddl.py +407 -183
  32. sqlspec/builder/_insert.py +1 -1
  33. sqlspec/builder/mixins/_insert_operations.py +26 -6
  34. sqlspec/builder/mixins/_merge_operations.py +1 -1
  35. sqlspec/builder/mixins/_select_operations.py +1 -5
  36. sqlspec/cli.py +281 -33
  37. sqlspec/config.py +183 -14
  38. sqlspec/core/__init__.py +89 -14
  39. sqlspec/core/cache.py +57 -104
  40. sqlspec/core/compiler.py +57 -112
  41. sqlspec/core/filters.py +1 -21
  42. sqlspec/core/hashing.py +13 -47
  43. sqlspec/core/parameters.py +272 -261
  44. sqlspec/core/result.py +12 -27
  45. sqlspec/core/splitter.py +17 -21
  46. sqlspec/core/statement.py +150 -159
  47. sqlspec/driver/_async.py +2 -15
  48. sqlspec/driver/_common.py +16 -95
  49. sqlspec/driver/_sync.py +2 -15
  50. sqlspec/driver/mixins/_result_tools.py +8 -29
  51. sqlspec/driver/mixins/_sql_translator.py +6 -8
  52. sqlspec/exceptions.py +1 -2
  53. sqlspec/extensions/litestar/plugin.py +15 -8
  54. sqlspec/loader.py +43 -115
  55. sqlspec/migrations/__init__.py +1 -1
  56. sqlspec/migrations/base.py +34 -45
  57. sqlspec/migrations/commands.py +34 -15
  58. sqlspec/migrations/loaders.py +1 -1
  59. sqlspec/migrations/runner.py +104 -19
  60. sqlspec/migrations/tracker.py +49 -2
  61. sqlspec/protocols.py +3 -6
  62. sqlspec/storage/__init__.py +4 -4
  63. sqlspec/storage/backends/fsspec.py +5 -6
  64. sqlspec/storage/backends/obstore.py +7 -8
  65. sqlspec/storage/registry.py +3 -3
  66. sqlspec/utils/__init__.py +2 -2
  67. sqlspec/utils/logging.py +6 -10
  68. sqlspec/utils/sync_tools.py +27 -4
  69. sqlspec/utils/text.py +6 -1
  70. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/METADATA +1 -1
  71. sqlspec-0.19.0.dist-info/RECORD +138 -0
  72. sqlspec/builder/_ddl_utils.py +0 -103
  73. sqlspec-0.17.1.dist-info/RECORD +0 -138
  74. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/WHEEL +0 -0
  75. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/entry_points.txt +0 -0
  76. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/licenses/LICENSE +0 -0
  77. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/licenses/NOTICE +0 -0
@@ -121,6 +121,7 @@ class OracleSyncExceptionHandler:
121
121
  return None
122
122
 
123
123
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
124
+ _ = exc_tb # Mark as intentionally unused
124
125
  if exc_type is None:
125
126
  return
126
127
 
@@ -167,6 +168,7 @@ class OracleAsyncExceptionHandler:
167
168
  return None
168
169
 
169
170
  async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
171
+ _ = exc_tb # Mark as intentionally unused
170
172
  if exc_type is None:
171
173
  return
172
174
 
@@ -205,36 +207,10 @@ class OracleAsyncExceptionHandler:
205
207
 
206
208
 
207
209
  class OracleSyncDriver(SyncDriverAdapterBase):
208
- """Enhanced Oracle Sync driver with CORE_ROUND_3 architecture integration.
209
-
210
- This sync driver leverages the complete core module system for maximum Oracle performance:
211
-
212
- Performance Improvements:
213
- - 5-10x faster SQL compilation through single-pass processing
214
- - 40-60% memory reduction through __slots__ optimization
215
- - Enhanced caching for repeated statement execution
216
- - Zero-copy parameter processing where possible
217
- - Sync-optimized resource management
218
- - Optimized Oracle parameter style conversion (QMARK -> NAMED_COLON)
219
-
220
- Oracle Features:
221
- - Parameter style conversion (QMARK to NAMED_COLON/POSITIONAL_COLON)
222
- - Oracle-specific type coercion and data handling
223
- - Enhanced error categorization for Oracle database errors
224
- - Transaction management with automatic commit/rollback
225
- - Oracle-specific data handling and optimization
226
-
227
- Core Integration Features:
228
- - sqlspec.core.statement for enhanced SQL processing
229
- - sqlspec.core.parameters for optimized parameter handling
230
- - sqlspec.core.cache for unified statement caching
231
- - sqlspec.core.config for centralized configuration management
232
-
233
- Compatibility:
234
- - 100% backward compatibility with existing Oracle driver interface
235
- - All existing sync Oracle tests pass without modification
236
- - Complete StatementConfig API compatibility
237
- - Preserved cursor management and exception handling patterns
210
+ """Synchronous Oracle Database driver.
211
+
212
+ Provides Oracle Database connectivity with parameter style conversion,
213
+ error handling, and transaction management.
238
214
  """
239
215
 
240
216
  __slots__ = ()
@@ -246,14 +222,14 @@ class OracleSyncDriver(SyncDriverAdapterBase):
246
222
  statement_config: "Optional[StatementConfig]" = None,
247
223
  driver_features: "Optional[dict[str, Any]]" = None,
248
224
  ) -> None:
249
- # Enhanced configuration with global settings integration
225
+ # Configure with default settings
250
226
  if statement_config is None:
251
227
  cache_config = get_cache_config()
252
228
  enhanced_config = oracledb_statement_config.replace(
253
229
  enable_caching=cache_config.compiled_cache_enabled,
254
- enable_parsing=True, # Default to enabled
255
- enable_validation=True, # Default to enabled
256
- dialect="oracle", # Use adapter-specific dialect
230
+ enable_parsing=True,
231
+ enable_validation=True,
232
+ dialect="oracle",
257
233
  )
258
234
  statement_config = enhanced_config
259
235
 
@@ -310,14 +286,14 @@ class OracleSyncDriver(SyncDriverAdapterBase):
310
286
  """
311
287
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
312
288
 
313
- # Enhanced parameter validation for executemany
289
+ # Parameter validation for executemany
314
290
  if not prepared_parameters:
315
291
  msg = "execute_many requires parameters"
316
292
  raise ValueError(msg)
317
293
 
318
294
  cursor.executemany(sql, prepared_parameters)
319
295
 
320
- # Calculate affected rows based on parameter count for Oracle
296
+ # Calculate affected rows based on parameter count
321
297
  affected_rows = len(prepared_parameters) if prepared_parameters else 0
322
298
 
323
299
  return self.create_execution_result(cursor, rowcount_override=affected_rows, is_many_result=True)
@@ -330,7 +306,7 @@ class OracleSyncDriver(SyncDriverAdapterBase):
330
306
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
331
307
  cursor.execute(sql, prepared_parameters or {})
332
308
 
333
- # Enhanced SELECT result processing for Oracle
309
+ # SELECT result processing for Oracle
334
310
  if statement.returns_rows():
335
311
  fetched_data = cursor.fetchall()
336
312
  column_names = [col[0] for col in cursor.description or []]
@@ -342,11 +318,11 @@ class OracleSyncDriver(SyncDriverAdapterBase):
342
318
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
343
319
  )
344
320
 
345
- # Enhanced non-SELECT result processing for Oracle
321
+ # Non-SELECT result processing
346
322
  affected_rows = cursor.rowcount if cursor.rowcount is not None else 0
347
323
  return self.create_execution_result(cursor, rowcount_override=affected_rows)
348
324
 
349
- # Oracle transaction management with enhanced error handling
325
+ # Oracle transaction management
350
326
  def begin(self) -> None:
351
327
  """Begin a database transaction with enhanced error handling.
352
328
 
@@ -372,36 +348,10 @@ class OracleSyncDriver(SyncDriverAdapterBase):
372
348
 
373
349
 
374
350
  class OracleAsyncDriver(AsyncDriverAdapterBase):
375
- """Enhanced Oracle Async driver with CORE_ROUND_3 architecture integration.
376
-
377
- This async driver leverages the complete core module system for maximum Oracle performance:
378
-
379
- Performance Improvements:
380
- - 5-10x faster SQL compilation through single-pass processing
381
- - 40-60% memory reduction through __slots__ optimization
382
- - Enhanced caching for repeated statement execution
383
- - Zero-copy parameter processing where possible
384
- - Async-optimized resource management
385
- - Optimized Oracle parameter style conversion (QMARK -> NAMED_COLON)
386
-
387
- Oracle Features:
388
- - Parameter style conversion (QMARK to NAMED_COLON/POSITIONAL_COLON)
389
- - Oracle-specific type coercion and data handling
390
- - Enhanced error categorization for Oracle database errors
391
- - Transaction management with automatic commit/rollback
392
- - Oracle-specific data handling and optimization
393
-
394
- Core Integration Features:
395
- - sqlspec.core.statement for enhanced SQL processing
396
- - sqlspec.core.parameters for optimized parameter handling
397
- - sqlspec.core.cache for unified statement caching
398
- - sqlspec.core.config for centralized configuration management
399
-
400
- Compatibility:
401
- - 100% backward compatibility with existing Oracle driver interface
402
- - All existing async Oracle tests pass without modification
403
- - Complete StatementConfig API compatibility
404
- - Preserved async cursor management and exception handling patterns
351
+ """Asynchronous Oracle Database driver.
352
+
353
+ Provides Oracle Database connectivity with parameter style conversion,
354
+ error handling, and transaction management for async operations.
405
355
  """
406
356
 
407
357
  __slots__ = ()
@@ -413,21 +363,21 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
413
363
  statement_config: "Optional[StatementConfig]" = None,
414
364
  driver_features: "Optional[dict[str, Any]]" = None,
415
365
  ) -> None:
416
- # Enhanced configuration with global settings integration
366
+ # Configure with default settings
417
367
  if statement_config is None:
418
368
  cache_config = get_cache_config()
419
369
  enhanced_config = oracledb_statement_config.replace(
420
370
  enable_caching=cache_config.compiled_cache_enabled,
421
- enable_parsing=True, # Default to enabled
422
- enable_validation=True, # Default to enabled
423
- dialect="oracle", # Use adapter-specific dialect
371
+ enable_parsing=True,
372
+ enable_validation=True,
373
+ dialect="oracle",
424
374
  )
425
375
  statement_config = enhanced_config
426
376
 
427
377
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
428
378
 
429
379
  def with_cursor(self, connection: OracleAsyncConnection) -> OracleAsyncCursor:
430
- """Create async context manager for Oracle cursor with enhanced resource management."""
380
+ """Create async context manager for Oracle cursor."""
431
381
  return OracleAsyncCursor(connection)
432
382
 
433
383
  def handle_database_exceptions(self) -> "AbstractAsyncContextManager[None]":
@@ -451,9 +401,8 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
451
401
  return None
452
402
 
453
403
  async def _execute_script(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
454
- """Execute SQL script using enhanced statement splitting and parameter handling.
404
+ """Execute SQL script with statement splitting and parameter handling.
455
405
 
456
- Uses core module optimization for statement parsing and parameter processing.
457
406
  Parameters are embedded as static values for script execution compatibility.
458
407
  """
459
408
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
@@ -471,33 +420,27 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
471
420
  )
472
421
 
473
422
  async def _execute_many(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
474
- """Execute SQL with multiple parameter sets using optimized Oracle batch processing.
475
-
476
- Leverages core parameter processing for enhanced Oracle type handling and parameter conversion.
477
- """
423
+ """Execute SQL with multiple parameter sets using Oracle batch processing."""
478
424
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
479
425
 
480
- # Enhanced parameter validation for executemany
426
+ # Parameter validation for executemany
481
427
  if not prepared_parameters:
482
428
  msg = "execute_many requires parameters"
483
429
  raise ValueError(msg)
484
430
 
485
431
  await cursor.executemany(sql, prepared_parameters)
486
432
 
487
- # Calculate affected rows based on parameter count for Oracle
433
+ # Calculate affected rows based on parameter count
488
434
  affected_rows = len(prepared_parameters) if prepared_parameters else 0
489
435
 
490
436
  return self.create_execution_result(cursor, rowcount_override=affected_rows, is_many_result=True)
491
437
 
492
438
  async def _execute_statement(self, cursor: Any, statement: "SQL") -> "ExecutionResult":
493
- """Execute single SQL statement with enhanced Oracle data handling and performance optimization.
494
-
495
- Uses core processing for optimal parameter handling and Oracle result processing.
496
- """
439
+ """Execute single SQL statement with Oracle data handling."""
497
440
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
498
441
  await cursor.execute(sql, prepared_parameters or {})
499
442
 
500
- # Enhanced SELECT result processing for Oracle
443
+ # SELECT result processing for Oracle
501
444
  if statement.returns_rows():
502
445
  fetched_data = await cursor.fetchall()
503
446
  column_names = [col[0] for col in cursor.description or []]
@@ -509,20 +452,20 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
509
452
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
510
453
  )
511
454
 
512
- # Enhanced non-SELECT result processing for Oracle
455
+ # Non-SELECT result processing
513
456
  affected_rows = cursor.rowcount if cursor.rowcount is not None else 0
514
457
  return self.create_execution_result(cursor, rowcount_override=affected_rows)
515
458
 
516
- # Oracle transaction management with enhanced async error handling
459
+ # Oracle transaction management
517
460
  async def begin(self) -> None:
518
- """Begin a database transaction with enhanced async error handling.
461
+ """Begin a database transaction.
519
462
 
520
463
  Oracle handles transactions automatically, so this is a no-op.
521
464
  """
522
465
  # Oracle handles transactions implicitly
523
466
 
524
467
  async def rollback(self) -> None:
525
- """Rollback the current transaction with enhanced async error handling."""
468
+ """Rollback the current transaction."""
526
469
  try:
527
470
  await self.connection.rollback()
528
471
  except oracledb.Error as e:
@@ -530,7 +473,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
530
473
  raise SQLSpecError(msg) from e
531
474
 
532
475
  async def commit(self) -> None:
533
- """Commit the current transaction with enhanced async error handling."""
476
+ """Commit the current transaction."""
534
477
  try:
535
478
  await self.connection.commit()
536
479
  except oracledb.Error as e:
@@ -0,0 +1,257 @@
1
+ """Oracle-specific migration implementations.
2
+
3
+ This module provides Oracle Database-specific overrides for migration functionality
4
+ to handle Oracle's unique SQL syntax requirements.
5
+ """
6
+
7
+ import getpass
8
+ from typing import TYPE_CHECKING, Any, Optional, cast
9
+
10
+ from sqlspec._sql import sql
11
+ from sqlspec.builder._ddl import CreateTable
12
+ from sqlspec.migrations.base import BaseMigrationTracker
13
+ from sqlspec.utils.logging import get_logger
14
+
15
+ if TYPE_CHECKING:
16
+ from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase
17
+
18
+ __all__ = ("OracleAsyncMigrationTracker", "OracleSyncMigrationTracker")
19
+
20
+ logger = get_logger("migrations.oracle")
21
+
22
+
23
+ class OracleMigrationTrackerMixin:
24
+ """Mixin providing Oracle-specific migration table creation."""
25
+
26
+ __slots__ = ()
27
+
28
+ version_table: str
29
+
30
+ def _get_create_table_sql(self) -> CreateTable:
31
+ """Get Oracle-specific SQL builder for creating the tracking table.
32
+
33
+ Oracle doesn't support:
34
+ - CREATE TABLE IF NOT EXISTS (need try/catch logic)
35
+ - TEXT type (use VARCHAR2)
36
+ - DEFAULT before NOT NULL is required
37
+
38
+ Returns:
39
+ SQL builder object for Oracle table creation.
40
+ """
41
+ return (
42
+ sql.create_table(self.version_table)
43
+ .column("version_num", "VARCHAR2(32)", primary_key=True)
44
+ .column("description", "VARCHAR2(2000)")
45
+ .column("applied_at", "TIMESTAMP", default="CURRENT_TIMESTAMP")
46
+ .column("execution_time_ms", "INTEGER")
47
+ .column("checksum", "VARCHAR2(64)")
48
+ .column("applied_by", "VARCHAR2(255)")
49
+ )
50
+
51
+
52
+ class OracleSyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTracker["SyncDriverAdapterBase"]):
53
+ """Oracle-specific sync migration tracker."""
54
+
55
+ __slots__ = ()
56
+
57
+ def ensure_tracking_table(self, driver: "SyncDriverAdapterBase") -> None:
58
+ """Create the migration tracking table if it doesn't exist.
59
+
60
+ Oracle doesn't support IF NOT EXISTS, so we check for table existence first.
61
+
62
+ Args:
63
+ driver: The database driver to use.
64
+ """
65
+
66
+ check_sql = (
67
+ sql.select(sql.count().as_("table_count"))
68
+ .from_("user_tables")
69
+ .where(sql.column("table_name") == self.version_table.upper())
70
+ )
71
+ result = driver.execute(check_sql)
72
+
73
+ if result.data[0]["TABLE_COUNT"] == 0:
74
+ driver.execute(self._get_create_table_sql())
75
+ self._safe_commit(driver)
76
+
77
+ def get_current_version(self, driver: "SyncDriverAdapterBase") -> "Optional[str]":
78
+ """Get the latest applied migration version.
79
+
80
+ Args:
81
+ driver: The database driver to use.
82
+
83
+ Returns:
84
+ The current migration version or None if no migrations applied.
85
+ """
86
+ result = driver.execute(self._get_current_version_sql())
87
+ return result.data[0]["VERSION_NUM"] if result.data else None
88
+
89
+ def get_applied_migrations(self, driver: "SyncDriverAdapterBase") -> "list[dict[str, Any]]":
90
+ """Get all applied migrations in order.
91
+
92
+ Args:
93
+ driver: The database driver to use.
94
+
95
+ Returns:
96
+ List of migration records as dictionaries.
97
+ """
98
+ result = driver.execute(self._get_applied_migrations_sql())
99
+ if not result.data:
100
+ return []
101
+
102
+ normalized_data = [{key.lower(): value for key, value in row.items()} for row in result.data]
103
+
104
+ return cast("list[dict[str, Any]]", normalized_data)
105
+
106
+ def record_migration(
107
+ self, driver: "SyncDriverAdapterBase", version: str, description: str, execution_time_ms: int, checksum: str
108
+ ) -> None:
109
+ """Record a successfully applied migration.
110
+
111
+ Args:
112
+ driver: The database driver to use.
113
+ version: Version number of the migration.
114
+ description: Description of the migration.
115
+ execution_time_ms: Execution time in milliseconds.
116
+ checksum: MD5 checksum of the migration content.
117
+ """
118
+
119
+ applied_by = getpass.getuser()
120
+
121
+ record_sql = self._get_record_migration_sql(version, description, execution_time_ms, checksum, applied_by)
122
+ driver.execute(record_sql)
123
+ self._safe_commit(driver)
124
+
125
+ def remove_migration(self, driver: "SyncDriverAdapterBase", version: str) -> None:
126
+ """Remove a migration record.
127
+
128
+ Args:
129
+ driver: The database driver to use.
130
+ version: Version number to remove.
131
+ """
132
+ remove_sql = self._get_remove_migration_sql(version)
133
+ driver.execute(remove_sql)
134
+ self._safe_commit(driver)
135
+
136
+ def _safe_commit(self, driver: "SyncDriverAdapterBase") -> None:
137
+ """Safely commit a transaction only if autocommit is disabled.
138
+
139
+ Args:
140
+ driver: The database driver to use.
141
+ """
142
+ try:
143
+ # Check driver features first (preferred approach)
144
+ if driver.driver_features.get("autocommit", False):
145
+ return
146
+
147
+ # Fallback to connection-level autocommit check
148
+ if driver.connection and driver.connection.autocommit:
149
+ return
150
+
151
+ driver.commit()
152
+ except Exception:
153
+ logger.debug("Failed to commit transaction, likely due to autocommit being enabled")
154
+
155
+
156
+ class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTracker["AsyncDriverAdapterBase"]):
157
+ """Oracle-specific async migration tracker."""
158
+
159
+ __slots__ = ()
160
+
161
+ async def ensure_tracking_table(self, driver: "AsyncDriverAdapterBase") -> None:
162
+ """Create the migration tracking table if it doesn't exist.
163
+
164
+ Oracle doesn't support IF NOT EXISTS, so we check for table existence first.
165
+
166
+ Args:
167
+ driver: The database driver to use.
168
+ """
169
+
170
+ check_sql = (
171
+ sql.select(sql.count().as_("table_count"))
172
+ .from_("user_tables")
173
+ .where(sql.column("table_name") == self.version_table.upper())
174
+ )
175
+ result = await driver.execute(check_sql)
176
+
177
+ if result.data[0]["TABLE_COUNT"] == 0:
178
+ await driver.execute(self._get_create_table_sql())
179
+ await self._safe_commit_async(driver)
180
+
181
+ async def get_current_version(self, driver: "AsyncDriverAdapterBase") -> "Optional[str]":
182
+ """Get the latest applied migration version.
183
+
184
+ Args:
185
+ driver: The database driver to use.
186
+
187
+ Returns:
188
+ The current migration version or None if no migrations applied.
189
+ """
190
+ result = await driver.execute(self._get_current_version_sql())
191
+ return result.data[0]["VERSION_NUM"] if result.data else None
192
+
193
+ async def get_applied_migrations(self, driver: "AsyncDriverAdapterBase") -> "list[dict[str, Any]]":
194
+ """Get all applied migrations in order.
195
+
196
+ Args:
197
+ driver: The database driver to use.
198
+
199
+ Returns:
200
+ List of migration records as dictionaries.
201
+ """
202
+ result = await driver.execute(self._get_applied_migrations_sql())
203
+ if not result.data:
204
+ return []
205
+
206
+ normalized_data = [{key.lower(): value for key, value in row.items()} for row in result.data]
207
+
208
+ return cast("list[dict[str, Any]]", normalized_data)
209
+
210
+ async def record_migration(
211
+ self, driver: "AsyncDriverAdapterBase", version: str, description: str, execution_time_ms: int, checksum: str
212
+ ) -> None:
213
+ """Record a successfully applied migration.
214
+
215
+ Args:
216
+ driver: The database driver to use.
217
+ version: Version number of the migration.
218
+ description: Description of the migration.
219
+ execution_time_ms: Execution time in milliseconds.
220
+ checksum: MD5 checksum of the migration content.
221
+ """
222
+
223
+ applied_by = getpass.getuser()
224
+
225
+ record_sql = self._get_record_migration_sql(version, description, execution_time_ms, checksum, applied_by)
226
+ await driver.execute(record_sql)
227
+ await self._safe_commit_async(driver)
228
+
229
+ async def remove_migration(self, driver: "AsyncDriverAdapterBase", version: str) -> None:
230
+ """Remove a migration record.
231
+
232
+ Args:
233
+ driver: The database driver to use.
234
+ version: Version number to remove.
235
+ """
236
+ remove_sql = self._get_remove_migration_sql(version)
237
+ await driver.execute(remove_sql)
238
+ await self._safe_commit_async(driver)
239
+
240
+ async def _safe_commit_async(self, driver: "AsyncDriverAdapterBase") -> None:
241
+ """Safely commit a transaction only if autocommit is disabled.
242
+
243
+ Args:
244
+ driver: The database driver to use.
245
+ """
246
+ try:
247
+ # Check driver features first (preferred approach)
248
+ if driver.driver_features.get("autocommit", False):
249
+ return
250
+
251
+ # Fallback to connection-level autocommit check
252
+ if driver.connection and driver.connection.autocommit:
253
+ return
254
+
255
+ await driver.commit()
256
+ except Exception:
257
+ logger.debug("Failed to commit transaction, likely due to autocommit being enabled")
@@ -1,4 +1,4 @@
1
- """Psqlpy database configuration with direct field-based configuration."""
1
+ """Psqlpy database configuration."""
2
2
 
3
3
  import logging
4
4
  from collections.abc import AsyncGenerator
@@ -77,7 +77,7 @@ __all__ = ("PsqlpyConfig", "PsqlpyConnectionParams", "PsqlpyCursor", "PsqlpyPool
77
77
 
78
78
 
79
79
  class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, ConnectionPool, PsqlpyDriver]):
80
- """Configuration for Psqlpy asynchronous database connections with direct field-based configuration."""
80
+ """Configuration for Psqlpy asynchronous database connections."""
81
81
 
82
82
  driver_type: ClassVar[type[PsqlpyDriver]] = PsqlpyDriver
83
83
  connection_type: "ClassVar[type[PsqlpyConnection]]" = PsqlpyConnection
@@ -86,17 +86,19 @@ class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, ConnectionPool, PsqlpyD
86
86
  self,
87
87
  *,
88
88
  pool_config: Optional[Union[PsqlpyPoolParams, dict[str, Any]]] = None,
89
- statement_config: Optional[StatementConfig] = None,
90
89
  pool_instance: Optional[ConnectionPool] = None,
91
90
  migration_config: Optional[dict[str, Any]] = None,
91
+ statement_config: Optional[StatementConfig] = None,
92
+ driver_features: Optional[dict[str, Any]] = None,
92
93
  ) -> None:
93
- """Initialize Psqlpy asynchronous configuration.
94
+ """Initialize Psqlpy configuration.
94
95
 
95
96
  Args:
96
- pool_config: Pool configuration parameters (TypedDict or dict)
97
+ pool_config: Pool configuration parameters
97
98
  pool_instance: Existing connection pool instance to use
98
- statement_config: Default SQL statement configuration
99
99
  migration_config: Migration configuration
100
+ statement_config: SQL statement configuration
101
+ driver_features: Driver feature configuration
100
102
  """
101
103
  processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {}
102
104
  if "extra" in processed_pool_config:
@@ -107,6 +109,7 @@ class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, ConnectionPool, PsqlpyD
107
109
  pool_instance=pool_instance,
108
110
  migration_config=migration_config,
109
111
  statement_config=statement_config or psqlpy_statement_config,
112
+ driver_features=driver_features or {},
110
113
  )
111
114
 
112
115
  def _get_pool_config_dict(self) -> dict[str, Any]:
@@ -145,6 +148,10 @@ class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, ConnectionPool, PsqlpyD
145
148
  logger.exception("Failed to close psqlpy connection pool", extra={"adapter": "psqlpy", "error": str(e)})
146
149
  raise
147
150
 
151
+ async def close_pool(self) -> None:
152
+ """Close the connection pool."""
153
+ await self._close_pool()
154
+
148
155
  async def create_connection(self) -> "PsqlpyConnection":
149
156
  """Create a single async connection (not from pool).
150
157
 
@@ -203,9 +210,6 @@ class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, ConnectionPool, PsqlpyD
203
210
  def get_signature_namespace(self) -> "dict[str, type[Any]]":
204
211
  """Get the signature namespace for Psqlpy types.
205
212
 
206
- This provides all Psqlpy-specific types that Litestar needs to recognize
207
- to avoid serialization attempts.
208
-
209
213
  Returns:
210
214
  Dictionary mapping type names to types.
211
215
  """