sqlspec 0.25.0__py3-none-any.whl → 0.26.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 (84) hide show
  1. sqlspec/_serialization.py +223 -21
  2. sqlspec/_sql.py +12 -50
  3. sqlspec/_typing.py +9 -0
  4. sqlspec/adapters/adbc/config.py +8 -1
  5. sqlspec/adapters/adbc/data_dictionary.py +290 -0
  6. sqlspec/adapters/adbc/driver.py +127 -18
  7. sqlspec/adapters/adbc/type_converter.py +159 -0
  8. sqlspec/adapters/aiosqlite/config.py +3 -0
  9. sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
  10. sqlspec/adapters/aiosqlite/driver.py +17 -3
  11. sqlspec/adapters/asyncmy/_types.py +1 -1
  12. sqlspec/adapters/asyncmy/config.py +11 -8
  13. sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
  14. sqlspec/adapters/asyncmy/driver.py +31 -7
  15. sqlspec/adapters/asyncpg/config.py +3 -0
  16. sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
  17. sqlspec/adapters/asyncpg/driver.py +19 -4
  18. sqlspec/adapters/bigquery/config.py +3 -0
  19. sqlspec/adapters/bigquery/data_dictionary.py +109 -0
  20. sqlspec/adapters/bigquery/driver.py +21 -3
  21. sqlspec/adapters/bigquery/type_converter.py +93 -0
  22. sqlspec/adapters/duckdb/_types.py +1 -1
  23. sqlspec/adapters/duckdb/config.py +2 -0
  24. sqlspec/adapters/duckdb/data_dictionary.py +124 -0
  25. sqlspec/adapters/duckdb/driver.py +32 -5
  26. sqlspec/adapters/duckdb/pool.py +1 -1
  27. sqlspec/adapters/duckdb/type_converter.py +103 -0
  28. sqlspec/adapters/oracledb/config.py +6 -0
  29. sqlspec/adapters/oracledb/data_dictionary.py +442 -0
  30. sqlspec/adapters/oracledb/driver.py +63 -9
  31. sqlspec/adapters/oracledb/migrations.py +51 -67
  32. sqlspec/adapters/oracledb/type_converter.py +132 -0
  33. sqlspec/adapters/psqlpy/config.py +3 -0
  34. sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
  35. sqlspec/adapters/psqlpy/driver.py +23 -179
  36. sqlspec/adapters/psqlpy/type_converter.py +73 -0
  37. sqlspec/adapters/psycopg/config.py +6 -0
  38. sqlspec/adapters/psycopg/data_dictionary.py +257 -0
  39. sqlspec/adapters/psycopg/driver.py +40 -5
  40. sqlspec/adapters/sqlite/config.py +3 -0
  41. sqlspec/adapters/sqlite/data_dictionary.py +117 -0
  42. sqlspec/adapters/sqlite/driver.py +18 -3
  43. sqlspec/adapters/sqlite/pool.py +13 -4
  44. sqlspec/builder/_base.py +82 -42
  45. sqlspec/builder/_column.py +57 -24
  46. sqlspec/builder/_ddl.py +84 -34
  47. sqlspec/builder/_insert.py +30 -52
  48. sqlspec/builder/_parsing_utils.py +104 -8
  49. sqlspec/builder/_select.py +147 -2
  50. sqlspec/builder/mixins/_cte_and_set_ops.py +1 -2
  51. sqlspec/builder/mixins/_join_operations.py +14 -30
  52. sqlspec/builder/mixins/_merge_operations.py +167 -61
  53. sqlspec/builder/mixins/_order_limit_operations.py +3 -10
  54. sqlspec/builder/mixins/_select_operations.py +3 -9
  55. sqlspec/builder/mixins/_update_operations.py +3 -22
  56. sqlspec/builder/mixins/_where_clause.py +4 -10
  57. sqlspec/cli.py +246 -140
  58. sqlspec/config.py +33 -19
  59. sqlspec/core/cache.py +2 -2
  60. sqlspec/core/compiler.py +56 -1
  61. sqlspec/core/parameters.py +7 -3
  62. sqlspec/core/statement.py +5 -0
  63. sqlspec/core/type_conversion.py +234 -0
  64. sqlspec/driver/__init__.py +6 -3
  65. sqlspec/driver/_async.py +106 -3
  66. sqlspec/driver/_common.py +156 -4
  67. sqlspec/driver/_sync.py +106 -3
  68. sqlspec/exceptions.py +5 -0
  69. sqlspec/migrations/__init__.py +4 -3
  70. sqlspec/migrations/base.py +153 -14
  71. sqlspec/migrations/commands.py +34 -96
  72. sqlspec/migrations/context.py +145 -0
  73. sqlspec/migrations/loaders.py +25 -8
  74. sqlspec/migrations/runner.py +352 -82
  75. sqlspec/typing.py +2 -0
  76. sqlspec/utils/config_resolver.py +153 -0
  77. sqlspec/utils/serializers.py +50 -2
  78. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
  79. sqlspec-0.26.0.dist-info/RECORD +157 -0
  80. sqlspec-0.25.0.dist-info/RECORD +0 -139
  81. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
  82. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
  83. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
  84. {sqlspec-0.25.0.dist-info → sqlspec-0.26.0.dist-info}/licenses/NOTICE +0 -0
@@ -1,5 +1,6 @@
1
1
  """Oracle Driver"""
2
2
 
3
+ import contextlib
3
4
  import logging
4
5
  from typing import TYPE_CHECKING, Any, Optional
5
6
 
@@ -7,11 +8,19 @@ import oracledb
7
8
  from oracledb import AsyncCursor, Cursor
8
9
 
9
10
  from sqlspec.adapters.oracledb._types import OracleAsyncConnection, OracleSyncConnection
11
+ from sqlspec.adapters.oracledb.data_dictionary import OracleAsyncDataDictionary, OracleSyncDataDictionary
12
+ from sqlspec.adapters.oracledb.type_converter import OracleTypeConverter
10
13
  from sqlspec.core.cache import get_cache_config
11
14
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
12
15
  from sqlspec.core.statement import StatementConfig
13
- from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase
16
+ from sqlspec.driver import (
17
+ AsyncDataDictionaryBase,
18
+ AsyncDriverAdapterBase,
19
+ SyncDataDictionaryBase,
20
+ SyncDriverAdapterBase,
21
+ )
14
22
  from sqlspec.exceptions import SQLParsingError, SQLSpecError
23
+ from sqlspec.utils.serializers import to_json
15
24
 
16
25
  if TYPE_CHECKING:
17
26
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
@@ -22,6 +31,11 @@ if TYPE_CHECKING:
22
31
 
23
32
  logger = logging.getLogger(__name__)
24
33
 
34
+ # Oracle-specific constants
35
+ LARGE_STRING_THRESHOLD = 3000 # Threshold for large string parameters to avoid ORA-01704
36
+
37
+ _type_converter = OracleTypeConverter()
38
+
25
39
  __all__ = (
26
40
  "OracleAsyncDriver",
27
41
  "OracleAsyncExceptionHandler",
@@ -36,11 +50,11 @@ oracledb_statement_config = StatementConfig(
36
50
  parameter_config=ParameterStyleConfig(
37
51
  default_parameter_style=ParameterStyle.POSITIONAL_COLON,
38
52
  supported_parameter_styles={ParameterStyle.NAMED_COLON, ParameterStyle.POSITIONAL_COLON, ParameterStyle.QMARK},
39
- default_execution_parameter_style=ParameterStyle.POSITIONAL_COLON,
53
+ default_execution_parameter_style=ParameterStyle.NAMED_COLON,
40
54
  supported_execution_parameter_styles={ParameterStyle.NAMED_COLON, ParameterStyle.POSITIONAL_COLON},
41
- type_coercion_map={},
55
+ type_coercion_map={dict: to_json, list: to_json},
42
56
  has_native_list_expansion=False,
43
- needs_static_script_compilation=True,
57
+ needs_static_script_compilation=False,
44
58
  preserve_parameter_format=True,
45
59
  ),
46
60
  enable_parsing=True,
@@ -63,8 +77,7 @@ class OracleSyncCursor:
63
77
  self.cursor = self.connection.cursor()
64
78
  return self.cursor
65
79
 
66
- def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
67
- _ = (exc_type, exc_val, exc_tb) # Mark as intentionally unused
80
+ def __exit__(self, *_: Any) -> None:
68
81
  if self.cursor is not None:
69
82
  self.cursor.close()
70
83
 
@@ -85,7 +98,10 @@ class OracleAsyncCursor:
85
98
  async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
86
99
  _ = (exc_type, exc_val, exc_tb) # Mark as intentionally unused
87
100
  if self.cursor is not None:
88
- self.cursor.close() # Synchronous method - do not await
101
+ with contextlib.suppress(Exception):
102
+ # Oracle async cursors have a synchronous close method
103
+ # but we need to ensure proper cleanup in the event loop context
104
+ self.cursor.close()
89
105
 
90
106
 
91
107
  class OracleSyncExceptionHandler:
@@ -189,7 +205,7 @@ class OracleSyncDriver(SyncDriverAdapterBase):
189
205
  error handling, and transaction management.
190
206
  """
191
207
 
192
- __slots__ = ()
208
+ __slots__ = ("_data_dictionary",)
193
209
  dialect = "oracle"
194
210
 
195
211
  def __init__(
@@ -208,6 +224,7 @@ class OracleSyncDriver(SyncDriverAdapterBase):
208
224
  )
209
225
 
210
226
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
227
+ self._data_dictionary: Optional[SyncDataDictionaryBase] = None
211
228
 
212
229
  def with_cursor(self, connection: OracleSyncConnection) -> OracleSyncCursor:
213
230
  """Create context manager for Oracle cursor.
@@ -309,6 +326,13 @@ class OracleSyncDriver(SyncDriverAdapterBase):
309
326
  Execution result containing data for SELECT statements or row count for others
310
327
  """
311
328
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
329
+
330
+ # Oracle-specific: Use setinputsizes for large string parameters to avoid ORA-01704
331
+ if prepared_parameters and isinstance(prepared_parameters, dict):
332
+ for param_name, param_value in prepared_parameters.items():
333
+ if isinstance(param_value, str) and len(param_value) > LARGE_STRING_THRESHOLD:
334
+ cursor.setinputsizes(**{param_name: len(param_value)})
335
+
312
336
  cursor.execute(sql, prepared_parameters or {})
313
337
 
314
338
  # SELECT result processing for Oracle
@@ -359,6 +383,17 @@ class OracleSyncDriver(SyncDriverAdapterBase):
359
383
  msg = f"Failed to commit Oracle transaction: {e}"
360
384
  raise SQLSpecError(msg) from e
361
385
 
386
+ @property
387
+ def data_dictionary(self) -> "SyncDataDictionaryBase":
388
+ """Get the data dictionary for this driver.
389
+
390
+ Returns:
391
+ Data dictionary instance for metadata queries
392
+ """
393
+ if self._data_dictionary is None:
394
+ self._data_dictionary = OracleSyncDataDictionary()
395
+ return self._data_dictionary
396
+
362
397
 
363
398
  class OracleAsyncDriver(AsyncDriverAdapterBase):
364
399
  """Asynchronous Oracle Database driver.
@@ -367,7 +402,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
367
402
  error handling, and transaction management for async operations.
368
403
  """
369
404
 
370
- __slots__ = ()
405
+ __slots__ = ("_data_dictionary",)
371
406
  dialect = "oracle"
372
407
 
373
408
  def __init__(
@@ -386,6 +421,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
386
421
  )
387
422
 
388
423
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
424
+ self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
389
425
 
390
426
  def with_cursor(self, connection: OracleAsyncConnection) -> OracleAsyncCursor:
391
427
  """Create context manager for Oracle cursor.
@@ -482,6 +518,13 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
482
518
  Execution result containing data for SELECT statements or row count for others
483
519
  """
484
520
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
521
+
522
+ # Oracle-specific: Use setinputsizes for large string parameters to avoid ORA-01704
523
+ if prepared_parameters and isinstance(prepared_parameters, dict):
524
+ for param_name, param_value in prepared_parameters.items():
525
+ if isinstance(param_value, str) and len(param_value) > LARGE_STRING_THRESHOLD:
526
+ cursor.setinputsizes(**{param_name: len(param_value)})
527
+
485
528
  await cursor.execute(sql, prepared_parameters or {})
486
529
 
487
530
  # SELECT result processing for Oracle
@@ -531,3 +574,14 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
531
574
  except oracledb.Error as e:
532
575
  msg = f"Failed to commit Oracle transaction: {e}"
533
576
  raise SQLSpecError(msg) from e
577
+
578
+ @property
579
+ def data_dictionary(self) -> "AsyncDataDictionaryBase":
580
+ """Get the data dictionary for this driver.
581
+
582
+ Returns:
583
+ Data dictionary instance for metadata queries
584
+ """
585
+ if self._data_dictionary is None:
586
+ self._data_dictionary = OracleAsyncDataDictionary()
587
+ return self._data_dictionary
@@ -8,7 +8,7 @@ import getpass
8
8
  from typing import TYPE_CHECKING, Any, Optional, cast
9
9
 
10
10
  from sqlspec._sql import sql
11
- from sqlspec.builder._ddl import CreateTable
11
+ from sqlspec.builder import CreateTable
12
12
  from sqlspec.migrations.base import BaseMigrationTracker
13
13
  from sqlspec.utils.logging import get_logger
14
14
 
@@ -57,22 +57,33 @@ class OracleSyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrack
57
57
  def ensure_tracking_table(self, driver: "SyncDriverAdapterBase") -> None:
58
58
  """Create the migration tracking table if it doesn't exist.
59
59
 
60
- Oracle doesn't support IF NOT EXISTS, so we check for table existence first.
60
+ Uses a PL/SQL block to make the operation atomic and prevent race conditions.
61
61
 
62
62
  Args:
63
63
  driver: The database driver to use.
64
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)
65
+ create_script = f"""
66
+ BEGIN
67
+ EXECUTE IMMEDIATE '
68
+ CREATE TABLE {self.version_table} (
69
+ version_num VARCHAR2(32) PRIMARY KEY,
70
+ description VARCHAR2(2000),
71
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
72
+ execution_time_ms INTEGER,
73
+ checksum VARCHAR2(64),
74
+ applied_by VARCHAR2(255)
75
+ )';
76
+ EXCEPTION
77
+ WHEN OTHERS THEN
78
+ IF SQLCODE = -955 THEN
79
+ NULL; -- Table already exists
80
+ ELSE
81
+ RAISE;
82
+ END IF;
83
+ END;
84
+ """
85
+ driver.execute_script(create_script)
86
+ driver.commit()
76
87
 
77
88
  def get_current_version(self, driver: "SyncDriverAdapterBase") -> "Optional[str]":
78
89
  """Get the latest applied migration version.
@@ -120,7 +131,7 @@ class OracleSyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrack
120
131
 
121
132
  record_sql = self._get_record_migration_sql(version, description, execution_time_ms, checksum, applied_by)
122
133
  driver.execute(record_sql)
123
- self._safe_commit(driver)
134
+ driver.commit()
124
135
 
125
136
  def remove_migration(self, driver: "SyncDriverAdapterBase", version: str) -> None:
126
137
  """Remove a migration record.
@@ -131,26 +142,7 @@ class OracleSyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrack
131
142
  """
132
143
  remove_sql = self._get_remove_migration_sql(version)
133
144
  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")
145
+ driver.commit()
154
146
 
155
147
 
156
148
  class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTracker["AsyncDriverAdapterBase"]):
@@ -161,22 +153,33 @@ class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrac
161
153
  async def ensure_tracking_table(self, driver: "AsyncDriverAdapterBase") -> None:
162
154
  """Create the migration tracking table if it doesn't exist.
163
155
 
164
- Oracle doesn't support IF NOT EXISTS, so we check for table existence first.
156
+ Uses a PL/SQL block to make the operation atomic and prevent race conditions.
165
157
 
166
158
  Args:
167
159
  driver: The database driver to use.
168
160
  """
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)
161
+ create_script = f"""
162
+ BEGIN
163
+ EXECUTE IMMEDIATE '
164
+ CREATE TABLE {self.version_table} (
165
+ version_num VARCHAR2(32) PRIMARY KEY,
166
+ description VARCHAR2(2000),
167
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
168
+ execution_time_ms INTEGER,
169
+ checksum VARCHAR2(64),
170
+ applied_by VARCHAR2(255)
171
+ )';
172
+ EXCEPTION
173
+ WHEN OTHERS THEN
174
+ IF SQLCODE = -955 THEN
175
+ NULL; -- Table already exists
176
+ ELSE
177
+ RAISE;
178
+ END IF;
179
+ END;
180
+ """
181
+ await driver.execute_script(create_script)
182
+ await driver.commit()
180
183
 
181
184
  async def get_current_version(self, driver: "AsyncDriverAdapterBase") -> "Optional[str]":
182
185
  """Get the latest applied migration version.
@@ -224,7 +227,7 @@ class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrac
224
227
 
225
228
  record_sql = self._get_record_migration_sql(version, description, execution_time_ms, checksum, applied_by)
226
229
  await driver.execute(record_sql)
227
- await self._safe_commit_async(driver)
230
+ await driver.commit()
228
231
 
229
232
  async def remove_migration(self, driver: "AsyncDriverAdapterBase", version: str) -> None:
230
233
  """Remove a migration record.
@@ -235,23 +238,4 @@ class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrac
235
238
  """
236
239
  remove_sql = self._get_remove_migration_sql(version)
237
240
  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")
241
+ await driver.commit()
@@ -0,0 +1,132 @@
1
+ """Oracle-specific type conversion with LOB optimization.
2
+
3
+ Provides specialized type handling for Oracle databases, including
4
+ efficient LOB (Large Object) processing and JSON storage detection.
5
+ """
6
+
7
+ import re
8
+ from datetime import datetime
9
+ from typing import Any, Final
10
+
11
+ from sqlspec.core.type_conversion import BaseTypeConverter
12
+ from sqlspec.utils.sync_tools import ensure_async_
13
+
14
+ # Oracle-specific JSON storage detection
15
+ ORACLE_JSON_STORAGE_REGEX: Final[re.Pattern[str]] = re.compile(
16
+ r"^(?:"
17
+ r"(?P<json_type>JSON)|"
18
+ r"(?P<blob_oson>BLOB.*OSON)|"
19
+ r"(?P<blob_json>BLOB.*JSON)|"
20
+ r"(?P<clob_json>CLOB.*JSON)"
21
+ r")$",
22
+ re.IGNORECASE,
23
+ )
24
+
25
+
26
+ class OracleTypeConverter(BaseTypeConverter):
27
+ """Oracle-specific type conversion with LOB optimization.
28
+
29
+ Extends the base TypeDetector with Oracle-specific functionality
30
+ including streaming LOB support and JSON storage type detection.
31
+ """
32
+
33
+ __slots__ = ()
34
+
35
+ async def process_lob(self, value: Any) -> Any:
36
+ """Process Oracle LOB objects efficiently.
37
+
38
+ Args:
39
+ value: Potential LOB object or regular value.
40
+
41
+ Returns:
42
+ LOB content if value is a LOB, original value otherwise.
43
+ """
44
+ if not hasattr(value, "read"):
45
+ return value
46
+
47
+ # Use ensure_async_ for unified sync/async handling
48
+ read_func = ensure_async_(value.read)
49
+ return await read_func()
50
+
51
+ def detect_json_storage_type(self, column_info: dict[str, Any]) -> bool:
52
+ """Detect if column stores JSON data.
53
+
54
+ Args:
55
+ column_info: Database column metadata.
56
+
57
+ Returns:
58
+ True if column is configured for JSON storage.
59
+ """
60
+ type_name = column_info.get("type_name", "").upper()
61
+ return bool(ORACLE_JSON_STORAGE_REGEX.match(type_name))
62
+
63
+ def format_datetime_for_oracle(self, dt: datetime) -> str:
64
+ """Format datetime for Oracle TO_DATE function.
65
+
66
+ Args:
67
+ dt: datetime object to format.
68
+
69
+ Returns:
70
+ Oracle TO_DATE SQL expression.
71
+ """
72
+ return f"TO_DATE('{dt.strftime('%Y-%m-%d %H:%M:%S')}', 'YYYY-MM-DD HH24:MI:SS')"
73
+
74
+ def handle_large_lob(self, lob_obj: Any, chunk_size: int = 1024 * 1024) -> bytes:
75
+ """Handle large LOB objects with streaming.
76
+
77
+ Args:
78
+ lob_obj: Oracle LOB object.
79
+ chunk_size: Size of chunks to read at a time.
80
+
81
+ Returns:
82
+ Complete LOB content as bytes.
83
+ """
84
+ if not hasattr(lob_obj, "read"):
85
+ return lob_obj if isinstance(lob_obj, bytes) else str(lob_obj).encode("utf-8")
86
+
87
+ chunks = []
88
+ while True:
89
+ chunk = lob_obj.read(chunk_size)
90
+ if not chunk:
91
+ break
92
+ chunks.append(chunk)
93
+
94
+ if not chunks:
95
+ return b""
96
+
97
+ return b"".join(chunks) if isinstance(chunks[0], bytes) else "".join(chunks).encode("utf-8")
98
+
99
+ def convert_oracle_value(self, value: Any, column_info: dict[str, Any]) -> Any:
100
+ """Convert Oracle-specific value with column context.
101
+
102
+ Args:
103
+ value: Value to convert.
104
+ column_info: Column metadata for context.
105
+
106
+ Returns:
107
+ Converted value appropriate for the column type.
108
+ """
109
+ # Handle LOB objects
110
+ if hasattr(value, "read"):
111
+ if self.detect_json_storage_type(column_info):
112
+ # For JSON storage types, decode the LOB content
113
+ content = self.handle_large_lob(value)
114
+ content_str = content.decode("utf-8") if isinstance(content, bytes) else content
115
+ # Try to parse as JSON
116
+ detected_type = self.detect_type(content_str)
117
+ if detected_type == "json":
118
+ return self.convert_value(content_str, detected_type)
119
+ return content_str
120
+ # For other LOB types, return raw content
121
+ return self.handle_large_lob(value)
122
+
123
+ # Use base type detection for non-LOB values
124
+ if isinstance(value, str):
125
+ detected_type = self.detect_type(value)
126
+ if detected_type:
127
+ return self.convert_value(value, detected_type)
128
+
129
+ return value
130
+
131
+
132
+ __all__ = ("ORACLE_JSON_STORAGE_REGEX", "OracleTypeConverter")
@@ -90,6 +90,7 @@ class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, ConnectionPool, PsqlpyD
90
90
  migration_config: Optional[dict[str, Any]] = None,
91
91
  statement_config: Optional[StatementConfig] = None,
92
92
  driver_features: Optional[dict[str, Any]] = None,
93
+ bind_key: Optional[str] = None,
93
94
  ) -> None:
94
95
  """Initialize Psqlpy configuration.
95
96
 
@@ -99,6 +100,7 @@ class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, ConnectionPool, PsqlpyD
99
100
  migration_config: Migration configuration
100
101
  statement_config: SQL statement configuration
101
102
  driver_features: Driver feature configuration
103
+ bind_key: Optional unique identifier for this configuration
102
104
  """
103
105
  processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {}
104
106
  if "extra" in processed_pool_config:
@@ -110,6 +112,7 @@ class PsqlpyConfig(AsyncDatabaseConfig[PsqlpyConnection, ConnectionPool, PsqlpyD
110
112
  migration_config=migration_config,
111
113
  statement_config=statement_config or psqlpy_statement_config,
112
114
  driver_features=driver_features or {},
115
+ bind_key=bind_key,
113
116
  )
114
117
 
115
118
  def _get_pool_config_dict(self) -> dict[str, Any]:
@@ -0,0 +1,133 @@
1
+ """PostgreSQL-specific data dictionary for metadata queries via psqlpy."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Callable, Optional, cast
5
+
6
+ from sqlspec.driver import AsyncDataDictionaryBase, AsyncDriverAdapterBase, VersionInfo
7
+ from sqlspec.utils.logging import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from sqlspec.adapters.psqlpy.driver import PsqlpyDriver
11
+
12
+ logger = get_logger("adapters.psqlpy.data_dictionary")
13
+
14
+ # Compiled regex patterns
15
+ POSTGRES_VERSION_PATTERN = re.compile(r"PostgreSQL (\d+)\.(\d+)(?:\.(\d+))?")
16
+
17
+ __all__ = ("PsqlpyAsyncDataDictionary",)
18
+
19
+
20
+ class PsqlpyAsyncDataDictionary(AsyncDataDictionaryBase):
21
+ """PostgreSQL-specific async data dictionary via psqlpy."""
22
+
23
+ async def get_version(self, driver: AsyncDriverAdapterBase) -> "Optional[VersionInfo]":
24
+ """Get PostgreSQL database version information.
25
+
26
+ Args:
27
+ driver: Async database driver instance
28
+
29
+ Returns:
30
+ PostgreSQL version information or None if detection fails
31
+ """
32
+ version_str = await cast("PsqlpyDriver", driver).select_value("SELECT version()")
33
+ if not version_str:
34
+ logger.warning("No PostgreSQL version information found")
35
+ return None
36
+
37
+ # Parse version like "PostgreSQL 15.3 on x86_64-pc-linux-gnu..."
38
+ version_match = POSTGRES_VERSION_PATTERN.search(str(version_str))
39
+ if not version_match:
40
+ logger.warning("Could not parse PostgreSQL version: %s", version_str)
41
+ return None
42
+
43
+ major = int(version_match.group(1))
44
+ minor = int(version_match.group(2))
45
+ patch = int(version_match.group(3)) if version_match.group(3) else 0
46
+
47
+ version_info = VersionInfo(major, minor, patch)
48
+ logger.debug("Detected PostgreSQL version: %s", version_info)
49
+ return version_info
50
+
51
+ async def get_feature_flag(self, driver: AsyncDriverAdapterBase, feature: str) -> bool:
52
+ """Check if PostgreSQL database supports a specific feature.
53
+
54
+ Args:
55
+ driver: Async database driver instance
56
+ feature: Feature name to check
57
+
58
+ Returns:
59
+ True if feature is supported, False otherwise
60
+ """
61
+ version_info = await self.get_version(driver)
62
+ if not version_info:
63
+ return False
64
+
65
+ feature_checks: dict[str, Callable[[VersionInfo], bool]] = {
66
+ "supports_json": lambda v: v >= VersionInfo(9, 2, 0),
67
+ "supports_jsonb": lambda v: v >= VersionInfo(9, 4, 0),
68
+ "supports_uuid": lambda _: True, # UUID extension widely available
69
+ "supports_arrays": lambda _: True, # PostgreSQL has excellent array support
70
+ "supports_returning": lambda v: v >= VersionInfo(8, 2, 0),
71
+ "supports_upsert": lambda v: v >= VersionInfo(9, 5, 0), # ON CONFLICT
72
+ "supports_window_functions": lambda v: v >= VersionInfo(8, 4, 0),
73
+ "supports_cte": lambda v: v >= VersionInfo(8, 4, 0),
74
+ "supports_transactions": lambda _: True,
75
+ "supports_prepared_statements": lambda _: True,
76
+ "supports_schemas": lambda _: True,
77
+ "supports_partitioning": lambda v: v >= VersionInfo(10, 0, 0),
78
+ }
79
+
80
+ if feature in feature_checks:
81
+ return bool(feature_checks[feature](version_info))
82
+
83
+ return False
84
+
85
+ async def get_optimal_type(self, driver: AsyncDriverAdapterBase, type_category: str) -> str:
86
+ """Get optimal PostgreSQL type for a category.
87
+
88
+ Args:
89
+ driver: Async database driver instance
90
+ type_category: Type category
91
+
92
+ Returns:
93
+ PostgreSQL-specific type name
94
+ """
95
+ version_info = await self.get_version(driver)
96
+
97
+ if type_category == "json":
98
+ if version_info and version_info >= VersionInfo(9, 4, 0):
99
+ return "JSONB" # Prefer JSONB over JSON
100
+ if version_info and version_info >= VersionInfo(9, 2, 0):
101
+ return "JSON"
102
+ return "TEXT"
103
+
104
+ type_map = {
105
+ "uuid": "UUID",
106
+ "boolean": "BOOLEAN",
107
+ "timestamp": "TIMESTAMP WITH TIME ZONE",
108
+ "text": "TEXT",
109
+ "blob": "BYTEA",
110
+ "array": "ARRAY",
111
+ }
112
+ return type_map.get(type_category, "TEXT")
113
+
114
+ def list_available_features(self) -> "list[str]":
115
+ """List available PostgreSQL feature flags.
116
+
117
+ Returns:
118
+ List of supported feature names
119
+ """
120
+ return [
121
+ "supports_json",
122
+ "supports_jsonb",
123
+ "supports_uuid",
124
+ "supports_arrays",
125
+ "supports_returning",
126
+ "supports_upsert",
127
+ "supports_window_functions",
128
+ "supports_cte",
129
+ "supports_transactions",
130
+ "supports_prepared_statements",
131
+ "supports_schemas",
132
+ "supports_partitioning",
133
+ ]