sqlspec 0.25.0__py3-none-any.whl → 0.27.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 +7 -15
- sqlspec/_serialization.py +256 -24
- sqlspec/_typing.py +71 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +69 -12
- sqlspec/adapters/adbc/data_dictionary.py +340 -0
- sqlspec/adapters/adbc/driver.py +266 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +88 -15
- sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
- sqlspec/adapters/aiosqlite/driver.py +143 -40
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +2 -2
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +68 -23
- sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
- sqlspec/adapters/asyncmy/driver.py +313 -58
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +450 -0
- sqlspec/adapters/asyncpg/config.py +59 -35
- sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
- sqlspec/adapters/asyncpg/driver.py +170 -25
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +576 -0
- sqlspec/adapters/bigquery/config.py +27 -10
- sqlspec/adapters/bigquery/data_dictionary.py +149 -0
- sqlspec/adapters/bigquery/driver.py +368 -142
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +80 -20
- sqlspec/adapters/duckdb/data_dictionary.py +163 -0
- sqlspec/adapters/duckdb/driver.py +167 -45
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +4 -4
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1745 -0
- sqlspec/adapters/oracledb/config.py +122 -32
- sqlspec/adapters/oracledb/data_dictionary.py +509 -0
- sqlspec/adapters/oracledb/driver.py +353 -91
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +348 -73
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +482 -0
- sqlspec/adapters/psqlpy/config.py +46 -17
- sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
- sqlspec/adapters/psqlpy/driver.py +123 -209
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +944 -0
- sqlspec/adapters/psycopg/config.py +69 -35
- sqlspec/adapters/psycopg/data_dictionary.py +331 -0
- sqlspec/adapters/psycopg/driver.py +238 -81
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +572 -0
- sqlspec/adapters/sqlite/config.py +87 -15
- sqlspec/adapters/sqlite/data_dictionary.py +149 -0
- sqlspec/adapters/sqlite/driver.py +137 -54
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +18 -9
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +162 -89
- sqlspec/builder/_column.py +62 -29
- sqlspec/builder/_ddl.py +180 -121
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +53 -94
- sqlspec/builder/_insert.py +32 -131
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +111 -17
- sqlspec/builder/_select.py +1457 -24
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +307 -194
- sqlspec/config.py +252 -67
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +17 -17
- sqlspec/core/compiler.py +62 -9
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +83 -48
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +36 -30
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +7 -6
- sqlspec/driver/_async.py +188 -151
- sqlspec/driver/_common.py +285 -80
- sqlspec/driver/_sync.py +188 -152
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +75 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +302 -39
- sqlspec/migrations/commands.py +611 -144
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +68 -23
- sqlspec/migrations/runner.py +543 -107
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +16 -84
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +50 -2
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_join_operations.py +0 -389
- sqlspec/builder/mixins/_merge_operations.py +0 -592
- sqlspec/builder/mixins/_order_limit_operations.py +0 -152
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -936
- sqlspec/builder/mixins/_update_operations.py +0 -218
- sqlspec/builder/mixins/_where_clause.py +0 -1304
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,12 +5,14 @@ to handle Oracle's unique SQL syntax requirements.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import getpass
|
|
8
|
-
from typing import TYPE_CHECKING, Any
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from sqlspec.builder import CreateTable, Select, sql
|
|
12
13
|
from sqlspec.migrations.base import BaseMigrationTracker
|
|
13
14
|
from sqlspec.utils.logging import get_logger
|
|
15
|
+
from sqlspec.utils.version import parse_version
|
|
14
16
|
|
|
15
17
|
if TYPE_CHECKING:
|
|
16
18
|
from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase
|
|
@@ -18,10 +20,20 @@ if TYPE_CHECKING:
|
|
|
18
20
|
__all__ = ("OracleAsyncMigrationTracker", "OracleSyncMigrationTracker")
|
|
19
21
|
|
|
20
22
|
logger = get_logger("migrations.oracle")
|
|
23
|
+
console = Console()
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
class OracleMigrationTrackerMixin:
|
|
24
|
-
"""Mixin providing Oracle-specific migration table creation.
|
|
27
|
+
"""Mixin providing Oracle-specific migration table creation and querying.
|
|
28
|
+
|
|
29
|
+
Oracle has unique identifier handling rules:
|
|
30
|
+
- Unquoted identifiers are case-insensitive and stored as UPPERCASE
|
|
31
|
+
- Quoted identifiers are case-sensitive and stored exactly as written
|
|
32
|
+
|
|
33
|
+
This mixin overrides SQL builder methods to add quoted identifiers for
|
|
34
|
+
all column references, ensuring they match the lowercase column names
|
|
35
|
+
created by the migration table.
|
|
36
|
+
"""
|
|
25
37
|
|
|
26
38
|
__slots__ = ()
|
|
27
39
|
|
|
@@ -41,6 +53,8 @@ class OracleMigrationTrackerMixin:
|
|
|
41
53
|
return (
|
|
42
54
|
sql.create_table(self.version_table)
|
|
43
55
|
.column("version_num", "VARCHAR2(32)", primary_key=True)
|
|
56
|
+
.column("version_type", "VARCHAR2(16)")
|
|
57
|
+
.column("execution_sequence", "INTEGER")
|
|
44
58
|
.column("description", "VARCHAR2(2000)")
|
|
45
59
|
.column("applied_at", "TIMESTAMP", default="CURRENT_TIMESTAMP")
|
|
46
60
|
.column("execution_time_ms", "INTEGER")
|
|
@@ -48,33 +62,184 @@ class OracleMigrationTrackerMixin:
|
|
|
48
62
|
.column("applied_by", "VARCHAR2(255)")
|
|
49
63
|
)
|
|
50
64
|
|
|
65
|
+
def _get_current_version_sql(self) -> Select:
|
|
66
|
+
"""Get Oracle-specific SQL for retrieving current version.
|
|
67
|
+
|
|
68
|
+
Uses uppercase column names with lowercase aliases to match Python expectations.
|
|
69
|
+
Oracle stores unquoted identifiers as UPPERCASE, so we query UPPERCASE columns
|
|
70
|
+
and alias them as quoted "lowercase" for result consistency.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
SQL builder object for version query.
|
|
74
|
+
"""
|
|
75
|
+
return (
|
|
76
|
+
sql.select('VERSION_NUM AS "version_num"')
|
|
77
|
+
.from_(self.version_table)
|
|
78
|
+
.order_by("EXECUTION_SEQUENCE DESC")
|
|
79
|
+
.limit(1)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _get_applied_migrations_sql(self) -> Select:
|
|
83
|
+
"""Get Oracle-specific SQL for retrieving all applied migrations.
|
|
84
|
+
|
|
85
|
+
Uses uppercase column names with lowercase aliases to match Python expectations.
|
|
86
|
+
Oracle stores unquoted identifiers as UPPERCASE, so we query UPPERCASE columns
|
|
87
|
+
and alias them as quoted "lowercase" for result consistency.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
SQL builder object for migrations query.
|
|
91
|
+
"""
|
|
92
|
+
return (
|
|
93
|
+
sql.select(
|
|
94
|
+
'VERSION_NUM AS "version_num"',
|
|
95
|
+
'VERSION_TYPE AS "version_type"',
|
|
96
|
+
'EXECUTION_SEQUENCE AS "execution_sequence"',
|
|
97
|
+
'DESCRIPTION AS "description"',
|
|
98
|
+
'APPLIED_AT AS "applied_at"',
|
|
99
|
+
'EXECUTION_TIME_MS AS "execution_time_ms"',
|
|
100
|
+
'CHECKSUM AS "checksum"',
|
|
101
|
+
'APPLIED_BY AS "applied_by"',
|
|
102
|
+
)
|
|
103
|
+
.from_(self.version_table)
|
|
104
|
+
.order_by("EXECUTION_SEQUENCE")
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _get_next_execution_sequence_sql(self) -> Select:
|
|
108
|
+
"""Get Oracle-specific SQL for retrieving next execution sequence.
|
|
109
|
+
|
|
110
|
+
Uses uppercase column names with lowercase alias to match Python expectations.
|
|
111
|
+
Oracle stores unquoted identifiers as UPPERCASE, so we query UPPERCASE columns
|
|
112
|
+
and alias them as quoted "lowercase" for result consistency.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
SQL builder object for sequence query.
|
|
116
|
+
"""
|
|
117
|
+
return sql.select('COALESCE(MAX(EXECUTION_SEQUENCE), 0) + 1 AS "next_seq"').from_(self.version_table)
|
|
118
|
+
|
|
119
|
+
def _get_existing_columns_sql(self) -> str:
|
|
120
|
+
"""Get SQL to query existing columns in the tracking table.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Raw SQL string for Oracle's USER_TAB_COLUMNS query.
|
|
124
|
+
"""
|
|
125
|
+
return f"""
|
|
126
|
+
SELECT column_name
|
|
127
|
+
FROM user_tab_columns
|
|
128
|
+
WHERE table_name = '{self.version_table.upper()}'
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def _detect_missing_columns(self, existing_columns: "set[str]") -> "set[str]":
|
|
132
|
+
"""Detect which columns are missing from the current schema.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
existing_columns: Set of existing column names (uppercase).
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Set of missing column names (lowercase).
|
|
139
|
+
"""
|
|
140
|
+
target_create = self._get_create_table_sql()
|
|
141
|
+
target_columns = {col.name.lower() for col in target_create.columns}
|
|
142
|
+
existing_lower = {col.lower() for col in existing_columns}
|
|
143
|
+
return target_columns - existing_lower
|
|
144
|
+
|
|
51
145
|
|
|
52
146
|
class OracleSyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTracker["SyncDriverAdapterBase"]):
|
|
53
147
|
"""Oracle-specific sync migration tracker."""
|
|
54
148
|
|
|
55
149
|
__slots__ = ()
|
|
56
150
|
|
|
151
|
+
def _migrate_schema_if_needed(self, driver: "SyncDriverAdapterBase") -> None:
|
|
152
|
+
"""Check for and add any missing columns to the tracking table.
|
|
153
|
+
|
|
154
|
+
Uses the driver's data dictionary to query existing columns from Oracle's
|
|
155
|
+
USER_TAB_COLUMNS metadata table.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
driver: The database driver to use.
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
columns_data = driver.data_dictionary.get_columns(driver, self.version_table)
|
|
162
|
+
existing_columns = {str(row["column_name"]).upper() for row in columns_data}
|
|
163
|
+
missing_columns = self._detect_missing_columns(existing_columns)
|
|
164
|
+
|
|
165
|
+
if not missing_columns:
|
|
166
|
+
logger.debug("Migration tracking table schema is up-to-date")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
console.print(
|
|
170
|
+
f"[cyan]Migrating tracking table schema, adding columns: {', '.join(sorted(missing_columns))}[/]"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
for col_name in sorted(missing_columns):
|
|
174
|
+
self._add_column(driver, col_name)
|
|
175
|
+
|
|
176
|
+
driver.commit()
|
|
177
|
+
console.print("[green]Migration tracking table schema updated successfully[/]")
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.warning("Could not check or migrate tracking table schema: %s", e)
|
|
181
|
+
|
|
182
|
+
def _add_column(self, driver: "SyncDriverAdapterBase", column_name: str) -> None:
|
|
183
|
+
"""Add a single column to the tracking table.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
driver: The database driver to use.
|
|
187
|
+
column_name: Name of the column to add (lowercase).
|
|
188
|
+
"""
|
|
189
|
+
target_create = self._get_create_table_sql()
|
|
190
|
+
column_def = next((col for col in target_create.columns if col.name.lower() == column_name), None)
|
|
191
|
+
|
|
192
|
+
if not column_def:
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
default_clause = f" DEFAULT {column_def.default}" if column_def.default else ""
|
|
196
|
+
not_null_clause = " NOT NULL" if column_def.not_null else ""
|
|
197
|
+
|
|
198
|
+
alter_sql = f"""
|
|
199
|
+
ALTER TABLE {self.version_table}
|
|
200
|
+
ADD {column_def.name} {column_def.dtype}{default_clause}{not_null_clause}
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
driver.execute(alter_sql)
|
|
204
|
+
logger.debug("Added column %s to tracking table", column_name)
|
|
205
|
+
|
|
57
206
|
def ensure_tracking_table(self, driver: "SyncDriverAdapterBase") -> None:
|
|
58
207
|
"""Create the migration tracking table if it doesn't exist.
|
|
59
208
|
|
|
60
|
-
|
|
209
|
+
Uses a PL/SQL block to make the operation atomic and prevent race conditions.
|
|
210
|
+
Also checks for and adds missing columns to support schema migrations.
|
|
61
211
|
|
|
62
212
|
Args:
|
|
63
213
|
driver: The database driver to use.
|
|
64
214
|
"""
|
|
215
|
+
create_script = f"""
|
|
216
|
+
BEGIN
|
|
217
|
+
EXECUTE IMMEDIATE '
|
|
218
|
+
CREATE TABLE {self.version_table} (
|
|
219
|
+
version_num VARCHAR2(32) PRIMARY KEY,
|
|
220
|
+
version_type VARCHAR2(16),
|
|
221
|
+
execution_sequence INTEGER,
|
|
222
|
+
description VARCHAR2(2000),
|
|
223
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
224
|
+
execution_time_ms INTEGER,
|
|
225
|
+
checksum VARCHAR2(64),
|
|
226
|
+
applied_by VARCHAR2(255)
|
|
227
|
+
)';
|
|
228
|
+
EXCEPTION
|
|
229
|
+
WHEN OTHERS THEN
|
|
230
|
+
IF SQLCODE = -955 THEN
|
|
231
|
+
NULL; -- Table already exists
|
|
232
|
+
ELSE
|
|
233
|
+
RAISE;
|
|
234
|
+
END IF;
|
|
235
|
+
END;
|
|
236
|
+
"""
|
|
237
|
+
driver.execute_script(create_script)
|
|
238
|
+
driver.commit()
|
|
65
239
|
|
|
66
|
-
|
|
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)
|
|
240
|
+
self._migrate_schema_if_needed(driver)
|
|
76
241
|
|
|
77
|
-
def get_current_version(self, driver: "SyncDriverAdapterBase") -> "
|
|
242
|
+
def get_current_version(self, driver: "SyncDriverAdapterBase") -> "str | None":
|
|
78
243
|
"""Get the latest applied migration version.
|
|
79
244
|
|
|
80
245
|
Args:
|
|
@@ -84,7 +249,8 @@ class OracleSyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrack
|
|
|
84
249
|
The current migration version or None if no migrations applied.
|
|
85
250
|
"""
|
|
86
251
|
result = driver.execute(self._get_current_version_sql())
|
|
87
|
-
|
|
252
|
+
data = result.get_data()
|
|
253
|
+
return data[0]["version_num"] if data else None
|
|
88
254
|
|
|
89
255
|
def get_applied_migrations(self, driver: "SyncDriverAdapterBase") -> "list[dict[str, Any]]":
|
|
90
256
|
"""Get all applied migrations in order.
|
|
@@ -93,15 +259,10 @@ class OracleSyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrack
|
|
|
93
259
|
driver: The database driver to use.
|
|
94
260
|
|
|
95
261
|
Returns:
|
|
96
|
-
List of migration records as dictionaries.
|
|
262
|
+
List of migration records as dictionaries with lowercase keys.
|
|
97
263
|
"""
|
|
98
264
|
result = driver.execute(self._get_applied_migrations_sql())
|
|
99
|
-
|
|
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)
|
|
265
|
+
return result.get_data()
|
|
105
266
|
|
|
106
267
|
def record_migration(
|
|
107
268
|
self, driver: "SyncDriverAdapterBase", version: str, description: str, execution_time_ms: int, checksum: str
|
|
@@ -115,12 +276,19 @@ class OracleSyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrack
|
|
|
115
276
|
execution_time_ms: Execution time in milliseconds.
|
|
116
277
|
checksum: MD5 checksum of the migration content.
|
|
117
278
|
"""
|
|
118
|
-
|
|
119
279
|
applied_by = getpass.getuser()
|
|
280
|
+
parsed_version = parse_version(version)
|
|
281
|
+
version_type = parsed_version.type.value
|
|
120
282
|
|
|
121
|
-
|
|
283
|
+
next_seq_result = driver.execute(self._get_next_execution_sequence_sql())
|
|
284
|
+
seq_data = next_seq_result.get_data()
|
|
285
|
+
execution_sequence = seq_data[0]["next_seq"] if seq_data else 1
|
|
286
|
+
|
|
287
|
+
record_sql = self._get_record_migration_sql(
|
|
288
|
+
version, version_type, execution_sequence, description, execution_time_ms, checksum, applied_by
|
|
289
|
+
)
|
|
122
290
|
driver.execute(record_sql)
|
|
123
|
-
|
|
291
|
+
driver.commit()
|
|
124
292
|
|
|
125
293
|
def remove_migration(self, driver: "SyncDriverAdapterBase", version: str) -> None:
|
|
126
294
|
"""Remove a migration record.
|
|
@@ -131,26 +299,42 @@ class OracleSyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrack
|
|
|
131
299
|
"""
|
|
132
300
|
remove_sql = self._get_remove_migration_sql(version)
|
|
133
301
|
driver.execute(remove_sql)
|
|
134
|
-
|
|
302
|
+
driver.commit()
|
|
135
303
|
|
|
136
|
-
def
|
|
137
|
-
"""
|
|
304
|
+
def update_version_record(self, driver: "SyncDriverAdapterBase", old_version: str, new_version: str) -> None:
|
|
305
|
+
"""Update migration version record from timestamp to sequential.
|
|
306
|
+
|
|
307
|
+
Updates version_num and version_type while preserving execution_sequence,
|
|
308
|
+
applied_at, and other tracking metadata. Used during fix command.
|
|
309
|
+
|
|
310
|
+
Idempotent: If the version is already updated, logs and continues without error.
|
|
311
|
+
This allows fix command to be safely re-run after pulling changes.
|
|
138
312
|
|
|
139
313
|
Args:
|
|
140
314
|
driver: The database driver to use.
|
|
315
|
+
old_version: Current timestamp version string.
|
|
316
|
+
new_version: New sequential version string.
|
|
317
|
+
|
|
318
|
+
Raises:
|
|
319
|
+
ValueError: If neither old_version nor new_version found in database.
|
|
141
320
|
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
321
|
+
parsed_new_version = parse_version(new_version)
|
|
322
|
+
new_version_type = parsed_new_version.type.value
|
|
323
|
+
|
|
324
|
+
result = driver.execute(self._get_update_version_sql(old_version, new_version, new_version_type))
|
|
325
|
+
|
|
326
|
+
if result.rows_affected == 0:
|
|
327
|
+
check_result = driver.execute(self._get_applied_migrations_sql())
|
|
328
|
+
applied_versions = {row["version_num"] for row in check_result.data} if check_result.data else set()
|
|
146
329
|
|
|
147
|
-
|
|
148
|
-
|
|
330
|
+
if new_version in applied_versions:
|
|
331
|
+
logger.debug("Version already updated: %s -> %s", old_version, new_version)
|
|
149
332
|
return
|
|
150
333
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
334
|
+
msg = f"Migration {old_version} not found in database for update to {new_version}"
|
|
335
|
+
raise ValueError(msg)
|
|
336
|
+
|
|
337
|
+
driver.commit()
|
|
154
338
|
|
|
155
339
|
|
|
156
340
|
class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTracker["AsyncDriverAdapterBase"]):
|
|
@@ -158,27 +342,98 @@ class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrac
|
|
|
158
342
|
|
|
159
343
|
__slots__ = ()
|
|
160
344
|
|
|
345
|
+
async def _migrate_schema_if_needed(self, driver: "AsyncDriverAdapterBase") -> None:
|
|
346
|
+
"""Check for and add any missing columns to the tracking table.
|
|
347
|
+
|
|
348
|
+
Uses the driver's data dictionary to query existing columns from Oracle's
|
|
349
|
+
USER_TAB_COLUMNS metadata table.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
driver: The database driver to use.
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
columns_data = await driver.data_dictionary.get_columns(driver, self.version_table)
|
|
356
|
+
existing_columns = {str(row["column_name"]).upper() for row in columns_data}
|
|
357
|
+
missing_columns = self._detect_missing_columns(existing_columns)
|
|
358
|
+
|
|
359
|
+
if not missing_columns:
|
|
360
|
+
logger.debug("Migration tracking table schema is up-to-date")
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
console.print(
|
|
364
|
+
f"[cyan]Migrating tracking table schema, adding columns: {', '.join(sorted(missing_columns))}[/]"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
for col_name in sorted(missing_columns):
|
|
368
|
+
await self._add_column(driver, col_name)
|
|
369
|
+
|
|
370
|
+
await driver.commit()
|
|
371
|
+
console.print("[green]Migration tracking table schema updated successfully[/]")
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.warning("Could not check or migrate tracking table schema: %s", e)
|
|
375
|
+
|
|
376
|
+
async def _add_column(self, driver: "AsyncDriverAdapterBase", column_name: str) -> None:
|
|
377
|
+
"""Add a single column to the tracking table.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
driver: The database driver to use.
|
|
381
|
+
column_name: Name of the column to add (lowercase).
|
|
382
|
+
"""
|
|
383
|
+
target_create = self._get_create_table_sql()
|
|
384
|
+
column_def = next((col for col in target_create.columns if col.name.lower() == column_name), None)
|
|
385
|
+
|
|
386
|
+
if not column_def:
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
default_clause = f" DEFAULT {column_def.default}" if column_def.default else ""
|
|
390
|
+
not_null_clause = " NOT NULL" if column_def.not_null else ""
|
|
391
|
+
|
|
392
|
+
alter_sql = f"""
|
|
393
|
+
ALTER TABLE {self.version_table}
|
|
394
|
+
ADD {column_def.name} {column_def.dtype}{default_clause}{not_null_clause}
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
await driver.execute(alter_sql)
|
|
398
|
+
logger.debug("Added column %s to tracking table", column_name)
|
|
399
|
+
|
|
161
400
|
async def ensure_tracking_table(self, driver: "AsyncDriverAdapterBase") -> None:
|
|
162
401
|
"""Create the migration tracking table if it doesn't exist.
|
|
163
402
|
|
|
164
|
-
|
|
403
|
+
Uses a PL/SQL block to make the operation atomic and prevent race conditions.
|
|
404
|
+
Also checks for and adds missing columns to support schema migrations.
|
|
165
405
|
|
|
166
406
|
Args:
|
|
167
407
|
driver: The database driver to use.
|
|
168
408
|
"""
|
|
409
|
+
create_script = f"""
|
|
410
|
+
BEGIN
|
|
411
|
+
EXECUTE IMMEDIATE '
|
|
412
|
+
CREATE TABLE {self.version_table} (
|
|
413
|
+
version_num VARCHAR2(32) PRIMARY KEY,
|
|
414
|
+
version_type VARCHAR2(16),
|
|
415
|
+
execution_sequence INTEGER,
|
|
416
|
+
description VARCHAR2(2000),
|
|
417
|
+
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
418
|
+
execution_time_ms INTEGER,
|
|
419
|
+
checksum VARCHAR2(64),
|
|
420
|
+
applied_by VARCHAR2(255)
|
|
421
|
+
)';
|
|
422
|
+
EXCEPTION
|
|
423
|
+
WHEN OTHERS THEN
|
|
424
|
+
IF SQLCODE = -955 THEN
|
|
425
|
+
NULL; -- Table already exists
|
|
426
|
+
ELSE
|
|
427
|
+
RAISE;
|
|
428
|
+
END IF;
|
|
429
|
+
END;
|
|
430
|
+
"""
|
|
431
|
+
await driver.execute_script(create_script)
|
|
432
|
+
await driver.commit()
|
|
169
433
|
|
|
170
|
-
|
|
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)
|
|
434
|
+
await self._migrate_schema_if_needed(driver)
|
|
180
435
|
|
|
181
|
-
async def get_current_version(self, driver: "AsyncDriverAdapterBase") -> "
|
|
436
|
+
async def get_current_version(self, driver: "AsyncDriverAdapterBase") -> "str | None":
|
|
182
437
|
"""Get the latest applied migration version.
|
|
183
438
|
|
|
184
439
|
Args:
|
|
@@ -188,7 +443,8 @@ class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrac
|
|
|
188
443
|
The current migration version or None if no migrations applied.
|
|
189
444
|
"""
|
|
190
445
|
result = await driver.execute(self._get_current_version_sql())
|
|
191
|
-
|
|
446
|
+
data = result.get_data()
|
|
447
|
+
return data[0]["version_num"] if data else None
|
|
192
448
|
|
|
193
449
|
async def get_applied_migrations(self, driver: "AsyncDriverAdapterBase") -> "list[dict[str, Any]]":
|
|
194
450
|
"""Get all applied migrations in order.
|
|
@@ -197,15 +453,10 @@ class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrac
|
|
|
197
453
|
driver: The database driver to use.
|
|
198
454
|
|
|
199
455
|
Returns:
|
|
200
|
-
List of migration records as dictionaries.
|
|
456
|
+
List of migration records as dictionaries with lowercase keys.
|
|
201
457
|
"""
|
|
202
458
|
result = await driver.execute(self._get_applied_migrations_sql())
|
|
203
|
-
|
|
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)
|
|
459
|
+
return result.get_data()
|
|
209
460
|
|
|
210
461
|
async def record_migration(
|
|
211
462
|
self, driver: "AsyncDriverAdapterBase", version: str, description: str, execution_time_ms: int, checksum: str
|
|
@@ -221,10 +472,18 @@ class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrac
|
|
|
221
472
|
"""
|
|
222
473
|
|
|
223
474
|
applied_by = getpass.getuser()
|
|
475
|
+
parsed_version = parse_version(version)
|
|
476
|
+
version_type = parsed_version.type.value
|
|
477
|
+
|
|
478
|
+
next_seq_result = await driver.execute(self._get_next_execution_sequence_sql())
|
|
479
|
+
seq_data = next_seq_result.get_data()
|
|
480
|
+
execution_sequence = seq_data[0]["next_seq"] if seq_data else 1
|
|
224
481
|
|
|
225
|
-
record_sql = self._get_record_migration_sql(
|
|
482
|
+
record_sql = self._get_record_migration_sql(
|
|
483
|
+
version, version_type, execution_sequence, description, execution_time_ms, checksum, applied_by
|
|
484
|
+
)
|
|
226
485
|
await driver.execute(record_sql)
|
|
227
|
-
await
|
|
486
|
+
await driver.commit()
|
|
228
487
|
|
|
229
488
|
async def remove_migration(self, driver: "AsyncDriverAdapterBase", version: str) -> None:
|
|
230
489
|
"""Remove a migration record.
|
|
@@ -235,23 +494,39 @@ class OracleAsyncMigrationTracker(OracleMigrationTrackerMixin, BaseMigrationTrac
|
|
|
235
494
|
"""
|
|
236
495
|
remove_sql = self._get_remove_migration_sql(version)
|
|
237
496
|
await driver.execute(remove_sql)
|
|
238
|
-
await
|
|
497
|
+
await driver.commit()
|
|
498
|
+
|
|
499
|
+
async def update_version_record(self, driver: "AsyncDriverAdapterBase", old_version: str, new_version: str) -> None:
|
|
500
|
+
"""Update migration version record from timestamp to sequential.
|
|
501
|
+
|
|
502
|
+
Updates version_num and version_type while preserving execution_sequence,
|
|
503
|
+
applied_at, and other tracking metadata. Used during fix command.
|
|
239
504
|
|
|
240
|
-
|
|
241
|
-
|
|
505
|
+
Idempotent: If the version is already updated, logs and continues without error.
|
|
506
|
+
This allows fix command to be safely re-run after pulling changes.
|
|
242
507
|
|
|
243
508
|
Args:
|
|
244
509
|
driver: The database driver to use.
|
|
510
|
+
old_version: Current timestamp version string.
|
|
511
|
+
new_version: New sequential version string.
|
|
512
|
+
|
|
513
|
+
Raises:
|
|
514
|
+
ValueError: If neither old_version nor new_version found in database.
|
|
245
515
|
"""
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
516
|
+
parsed_new_version = parse_version(new_version)
|
|
517
|
+
new_version_type = parsed_new_version.type.value
|
|
518
|
+
|
|
519
|
+
result = await driver.execute(self._get_update_version_sql(old_version, new_version, new_version_type))
|
|
250
520
|
|
|
251
|
-
|
|
252
|
-
|
|
521
|
+
if result.rows_affected == 0:
|
|
522
|
+
check_result = await driver.execute(self._get_applied_migrations_sql())
|
|
523
|
+
applied_versions = {row["version_num"] for row in check_result.data} if check_result.data else set()
|
|
524
|
+
|
|
525
|
+
if new_version in applied_versions:
|
|
526
|
+
logger.debug("Version already updated: %s -> %s", old_version, new_version)
|
|
253
527
|
return
|
|
254
528
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
529
|
+
msg = f"Migration {old_version} not found in database for update to {new_version}"
|
|
530
|
+
raise ValueError(msg)
|
|
531
|
+
|
|
532
|
+
await driver.commit()
|