sqlspec 0.24.1__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.
- sqlspec/_serialization.py +223 -21
- sqlspec/_sql.py +20 -62
- sqlspec/_typing.py +11 -0
- sqlspec/adapters/adbc/config.py +8 -1
- sqlspec/adapters/adbc/data_dictionary.py +290 -0
- sqlspec/adapters/adbc/driver.py +129 -20
- sqlspec/adapters/adbc/type_converter.py +159 -0
- sqlspec/adapters/aiosqlite/config.py +3 -0
- sqlspec/adapters/aiosqlite/data_dictionary.py +117 -0
- sqlspec/adapters/aiosqlite/driver.py +17 -3
- sqlspec/adapters/asyncmy/_types.py +1 -1
- sqlspec/adapters/asyncmy/config.py +11 -8
- sqlspec/adapters/asyncmy/data_dictionary.py +122 -0
- sqlspec/adapters/asyncmy/driver.py +31 -7
- sqlspec/adapters/asyncpg/config.py +3 -0
- sqlspec/adapters/asyncpg/data_dictionary.py +134 -0
- sqlspec/adapters/asyncpg/driver.py +19 -4
- sqlspec/adapters/bigquery/config.py +3 -0
- sqlspec/adapters/bigquery/data_dictionary.py +109 -0
- sqlspec/adapters/bigquery/driver.py +21 -3
- sqlspec/adapters/bigquery/type_converter.py +93 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/config.py +2 -0
- sqlspec/adapters/duckdb/data_dictionary.py +124 -0
- sqlspec/adapters/duckdb/driver.py +32 -5
- sqlspec/adapters/duckdb/pool.py +1 -1
- sqlspec/adapters/duckdb/type_converter.py +103 -0
- sqlspec/adapters/oracledb/config.py +6 -0
- sqlspec/adapters/oracledb/data_dictionary.py +442 -0
- sqlspec/adapters/oracledb/driver.py +68 -9
- sqlspec/adapters/oracledb/migrations.py +51 -67
- sqlspec/adapters/oracledb/type_converter.py +132 -0
- sqlspec/adapters/psqlpy/config.py +3 -0
- sqlspec/adapters/psqlpy/data_dictionary.py +133 -0
- sqlspec/adapters/psqlpy/driver.py +23 -179
- sqlspec/adapters/psqlpy/type_converter.py +73 -0
- sqlspec/adapters/psycopg/config.py +8 -4
- sqlspec/adapters/psycopg/data_dictionary.py +257 -0
- sqlspec/adapters/psycopg/driver.py +40 -5
- sqlspec/adapters/sqlite/config.py +3 -0
- sqlspec/adapters/sqlite/data_dictionary.py +117 -0
- sqlspec/adapters/sqlite/driver.py +18 -3
- sqlspec/adapters/sqlite/pool.py +13 -4
- sqlspec/base.py +3 -4
- sqlspec/builder/_base.py +130 -48
- sqlspec/builder/_column.py +66 -24
- sqlspec/builder/_ddl.py +91 -41
- sqlspec/builder/_insert.py +40 -58
- sqlspec/builder/_parsing_utils.py +127 -12
- sqlspec/builder/_select.py +147 -2
- sqlspec/builder/_update.py +1 -1
- sqlspec/builder/mixins/_cte_and_set_ops.py +31 -23
- sqlspec/builder/mixins/_delete_operations.py +12 -7
- sqlspec/builder/mixins/_insert_operations.py +50 -36
- sqlspec/builder/mixins/_join_operations.py +15 -30
- sqlspec/builder/mixins/_merge_operations.py +210 -78
- sqlspec/builder/mixins/_order_limit_operations.py +4 -10
- sqlspec/builder/mixins/_pivot_operations.py +1 -0
- sqlspec/builder/mixins/_select_operations.py +44 -22
- sqlspec/builder/mixins/_update_operations.py +30 -37
- sqlspec/builder/mixins/_where_clause.py +52 -70
- sqlspec/cli.py +246 -140
- sqlspec/config.py +33 -19
- sqlspec/core/__init__.py +3 -2
- sqlspec/core/cache.py +298 -352
- sqlspec/core/compiler.py +61 -4
- sqlspec/core/filters.py +246 -213
- sqlspec/core/hashing.py +9 -11
- sqlspec/core/parameters.py +27 -10
- sqlspec/core/statement.py +72 -12
- sqlspec/core/type_conversion.py +234 -0
- sqlspec/driver/__init__.py +6 -3
- sqlspec/driver/_async.py +108 -5
- sqlspec/driver/_common.py +186 -17
- sqlspec/driver/_sync.py +108 -5
- sqlspec/driver/mixins/_result_tools.py +60 -7
- sqlspec/exceptions.py +5 -0
- sqlspec/loader.py +8 -9
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +153 -14
- sqlspec/migrations/commands.py +34 -96
- sqlspec/migrations/context.py +145 -0
- sqlspec/migrations/loaders.py +25 -8
- sqlspec/migrations/runner.py +352 -82
- sqlspec/storage/backends/fsspec.py +1 -0
- sqlspec/typing.py +4 -0
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/serializers.py +50 -2
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/METADATA +1 -1
- sqlspec-0.26.0.dist-info/RECORD +157 -0
- sqlspec-0.24.1.dist-info/RECORD +0 -139
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.24.1.dist-info → sqlspec-0.26.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.24.1.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
|
|
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.
|
|
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=
|
|
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,
|
|
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
|
-
|
|
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.
|
|
@@ -286,6 +303,11 @@ class OracleSyncDriver(SyncDriverAdapterBase):
|
|
|
286
303
|
msg = "execute_many requires parameters"
|
|
287
304
|
raise ValueError(msg)
|
|
288
305
|
|
|
306
|
+
# Oracle-specific fix: Ensure parameters are in list format for executemany
|
|
307
|
+
# Oracle expects a list of sequences, not a tuple of sequences
|
|
308
|
+
if isinstance(prepared_parameters, tuple):
|
|
309
|
+
prepared_parameters = list(prepared_parameters)
|
|
310
|
+
|
|
289
311
|
cursor.executemany(sql, prepared_parameters)
|
|
290
312
|
|
|
291
313
|
# Calculate affected rows based on parameter count
|
|
@@ -304,6 +326,13 @@ class OracleSyncDriver(SyncDriverAdapterBase):
|
|
|
304
326
|
Execution result containing data for SELECT statements or row count for others
|
|
305
327
|
"""
|
|
306
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
|
+
|
|
307
336
|
cursor.execute(sql, prepared_parameters or {})
|
|
308
337
|
|
|
309
338
|
# SELECT result processing for Oracle
|
|
@@ -354,6 +383,17 @@ class OracleSyncDriver(SyncDriverAdapterBase):
|
|
|
354
383
|
msg = f"Failed to commit Oracle transaction: {e}"
|
|
355
384
|
raise SQLSpecError(msg) from e
|
|
356
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
|
+
|
|
357
397
|
|
|
358
398
|
class OracleAsyncDriver(AsyncDriverAdapterBase):
|
|
359
399
|
"""Asynchronous Oracle Database driver.
|
|
@@ -362,7 +402,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
|
|
|
362
402
|
error handling, and transaction management for async operations.
|
|
363
403
|
"""
|
|
364
404
|
|
|
365
|
-
__slots__ = ()
|
|
405
|
+
__slots__ = ("_data_dictionary",)
|
|
366
406
|
dialect = "oracle"
|
|
367
407
|
|
|
368
408
|
def __init__(
|
|
@@ -381,6 +421,7 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
|
|
|
381
421
|
)
|
|
382
422
|
|
|
383
423
|
super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
|
|
424
|
+
self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
|
|
384
425
|
|
|
385
426
|
def with_cursor(self, connection: OracleAsyncConnection) -> OracleAsyncCursor:
|
|
386
427
|
"""Create context manager for Oracle cursor.
|
|
@@ -477,6 +518,13 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
|
|
|
477
518
|
Execution result containing data for SELECT statements or row count for others
|
|
478
519
|
"""
|
|
479
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
|
+
|
|
480
528
|
await cursor.execute(sql, prepared_parameters or {})
|
|
481
529
|
|
|
482
530
|
# SELECT result processing for Oracle
|
|
@@ -526,3 +574,14 @@ class OracleAsyncDriver(AsyncDriverAdapterBase):
|
|
|
526
574
|
except oracledb.Error as e:
|
|
527
575
|
msg = f"Failed to commit Oracle transaction: {e}"
|
|
528
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
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
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
|
|
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
|
+
]
|