instructure-pysqlsync 0.8.5.dev0__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.
- instructure_pysqlsync-0.8.5.dev0.dist-info/METADATA +244 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/RECORD +102 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/WHEEL +5 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/licenses/LICENSE +21 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/top_level.txt +1 -0
- instructure_pysqlsync-0.8.5.dev0.dist-info/zip-safe +1 -0
- pysqlsync/__init__.py +15 -0
- pysqlsync/base.py +1386 -0
- pysqlsync/connection.py +101 -0
- pysqlsync/data/__init__.py +0 -0
- pysqlsync/data/exchange.py +184 -0
- pysqlsync/data/generator.py +305 -0
- pysqlsync/dialect/README.md +35 -0
- pysqlsync/dialect/__init__.py +0 -0
- pysqlsync/dialect/delta/__init__.py +0 -0
- pysqlsync/dialect/delta/data_types.py +51 -0
- pysqlsync/dialect/delta/dependency.py +7 -0
- pysqlsync/dialect/delta/engine.py +18 -0
- pysqlsync/dialect/delta/generator.py +74 -0
- pysqlsync/dialect/delta/object_types.py +87 -0
- pysqlsync/dialect/mssql/README.md +23 -0
- pysqlsync/dialect/mssql/__init__.py +0 -0
- pysqlsync/dialect/mssql/connection.py +146 -0
- pysqlsync/dialect/mssql/data_types.py +113 -0
- pysqlsync/dialect/mssql/dependency.py +9 -0
- pysqlsync/dialect/mssql/discovery.py +99 -0
- pysqlsync/dialect/mssql/engine.py +20 -0
- pysqlsync/dialect/mssql/generator.py +88 -0
- pysqlsync/dialect/mssql/mutation.py +46 -0
- pysqlsync/dialect/mssql/object_types.py +135 -0
- pysqlsync/dialect/mysql/__init__.py +0 -0
- pysqlsync/dialect/mysql/connection.py +126 -0
- pysqlsync/dialect/mysql/data_types.py +40 -0
- pysqlsync/dialect/mysql/dependency.py +10 -0
- pysqlsync/dialect/mysql/discovery.py +145 -0
- pysqlsync/dialect/mysql/engine.py +20 -0
- pysqlsync/dialect/mysql/generator.py +140 -0
- pysqlsync/dialect/mysql/mutation.py +65 -0
- pysqlsync/dialect/mysql/object_types.py +75 -0
- pysqlsync/dialect/oracle/README.md +22 -0
- pysqlsync/dialect/oracle/__init__.py +0 -0
- pysqlsync/dialect/oracle/connection.py +141 -0
- pysqlsync/dialect/oracle/data_types.py +104 -0
- pysqlsync/dialect/oracle/dependency.py +9 -0
- pysqlsync/dialect/oracle/discovery.py +231 -0
- pysqlsync/dialect/oracle/engine.py +20 -0
- pysqlsync/dialect/oracle/generator.py +93 -0
- pysqlsync/dialect/oracle/mutation.py +37 -0
- pysqlsync/dialect/oracle/object_types.py +59 -0
- pysqlsync/dialect/postgresql/__init__.py +0 -0
- pysqlsync/dialect/postgresql/connection.py +133 -0
- pysqlsync/dialect/postgresql/data_types.py +6 -0
- pysqlsync/dialect/postgresql/dependency.py +9 -0
- pysqlsync/dialect/postgresql/discovery.py +392 -0
- pysqlsync/dialect/postgresql/engine.py +20 -0
- pysqlsync/dialect/postgresql/generator.py +99 -0
- pysqlsync/dialect/postgresql/mutation.py +82 -0
- pysqlsync/dialect/postgresql/object_types.py +99 -0
- pysqlsync/dialect/redshift/__init__.py +0 -0
- pysqlsync/dialect/redshift/connection.py +156 -0
- pysqlsync/dialect/redshift/data_types.py +10 -0
- pysqlsync/dialect/redshift/dependency.py +9 -0
- pysqlsync/dialect/redshift/engine.py +20 -0
- pysqlsync/dialect/redshift/generator.py +82 -0
- pysqlsync/dialect/snowflake/__init__.py +0 -0
- pysqlsync/dialect/snowflake/data_types.py +18 -0
- pysqlsync/dialect/snowflake/dependency.py +9 -0
- pysqlsync/dialect/snowflake/engine.py +18 -0
- pysqlsync/dialect/snowflake/generator.py +64 -0
- pysqlsync/dialect/snowflake/object_types.py +84 -0
- pysqlsync/dialect/test/__init__.py +0 -0
- pysqlsync/dialect/test/dependency.py +10 -0
- pysqlsync/dialect/trino/__init__.py +0 -0
- pysqlsync/dialect/trino/connection.py +86 -0
- pysqlsync/dialect/trino/dependency.py +9 -0
- pysqlsync/dialect/trino/discovery.py +21 -0
- pysqlsync/dialect/trino/engine.py +20 -0
- pysqlsync/dialect/trino/generator.py +31 -0
- pysqlsync/factory.py +169 -0
- pysqlsync/formation/__init__.py +0 -0
- pysqlsync/formation/constraints.py +78 -0
- pysqlsync/formation/data_types.py +192 -0
- pysqlsync/formation/discovery.py +390 -0
- pysqlsync/formation/inspection.py +179 -0
- pysqlsync/formation/mutation.py +320 -0
- pysqlsync/formation/object_dict.py +79 -0
- pysqlsync/formation/object_types.py +872 -0
- pysqlsync/formation/py_to_sql.py +1134 -0
- pysqlsync/formation/sql_to_py.py +194 -0
- pysqlsync/model/__init__.py +0 -0
- pysqlsync/model/data_types.py +397 -0
- pysqlsync/model/entity_types.py +68 -0
- pysqlsync/model/id_types.py +193 -0
- pysqlsync/model/key_types.py +38 -0
- pysqlsync/model/properties.py +234 -0
- pysqlsync/py.typed +0 -0
- pysqlsync/python_types.py +192 -0
- pysqlsync/resultset.py +106 -0
- pysqlsync/util/__init__.py +0 -0
- pysqlsync/util/dataclasses.py +80 -0
- pysqlsync/util/dispatch.py +28 -0
- pysqlsync/util/typing.py +13 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pysqlsync.formation.mutation import Mutator
|
|
5
|
+
from pysqlsync.formation.object_types import Column, StatementList, Table, deleted, join_or_none
|
|
6
|
+
from pysqlsync.model.data_types import SqlEnumType, SqlVariableCharacterType, quote
|
|
7
|
+
from pysqlsync.model.id_types import LocalId
|
|
8
|
+
|
|
9
|
+
from .object_types import MySQLColumn, MySQLTable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MySQLMutator(Mutator):
|
|
13
|
+
def migrate_column_stmt(self, source_table: Table, source: Column, target_table: Table, target: Column) -> Optional[str]:
|
|
14
|
+
statements: list[str] = []
|
|
15
|
+
ref = target_table.get_constraint(target.name)
|
|
16
|
+
if isinstance(source.data_type, SqlEnumType):
|
|
17
|
+
enum_values = ", ".join(f"({quote(v)})" for v in source.data_type.values)
|
|
18
|
+
statements.append(f'INSERT INTO {ref.table} ("value") VALUES {enum_values}\nON DUPLICATE KEY UPDATE "value" = "value";')
|
|
19
|
+
elif isinstance(source.data_type, SqlVariableCharacterType):
|
|
20
|
+
statements.append(
|
|
21
|
+
f'INSERT INTO {ref.table} ("value") SELECT DISTINCT {LocalId(deleted(source.name.id))} FROM {source_table.name}\n'
|
|
22
|
+
'ON DUPLICATE KEY UPDATE "value" = "value";'
|
|
23
|
+
)
|
|
24
|
+
statements.append(
|
|
25
|
+
f"UPDATE {source_table.name} data_table\n"
|
|
26
|
+
f'JOIN {ref.table} enum_table ON data_table.{LocalId(deleted(source.name.id))} = enum_table."value"\n'
|
|
27
|
+
f'SET data_table.{target.name} = enum_table."id";'
|
|
28
|
+
)
|
|
29
|
+
return "\n".join(statements)
|
|
30
|
+
|
|
31
|
+
def mutate_table_stmt(self, source: Table, target: Table) -> Optional[str]:
|
|
32
|
+
source = typing.cast(MySQLTable, source)
|
|
33
|
+
target = typing.cast(MySQLTable, target)
|
|
34
|
+
|
|
35
|
+
statements: StatementList = StatementList()
|
|
36
|
+
statements.append(super().mutate_table_stmt(source, target))
|
|
37
|
+
|
|
38
|
+
source_desc = source.short_description
|
|
39
|
+
target_desc = target.short_description
|
|
40
|
+
|
|
41
|
+
if source_desc is None:
|
|
42
|
+
if target_desc is not None:
|
|
43
|
+
statements.append(f"ALTER TABLE {target.name} COMMENT = {quote(target_desc)};")
|
|
44
|
+
else:
|
|
45
|
+
if target_desc is None:
|
|
46
|
+
statements.append(f"ALTER TABLE {target.name} COMMENT = {quote('')};")
|
|
47
|
+
elif source_desc != target_desc:
|
|
48
|
+
statements.append(f"ALTER TABLE {target.name} COMMENT = {quote(target_desc)};")
|
|
49
|
+
|
|
50
|
+
return join_or_none(statements)
|
|
51
|
+
|
|
52
|
+
def mutate_column_stmt(self, source: Column, target: Column) -> Optional[str]:
|
|
53
|
+
source = typing.cast(MySQLColumn, source)
|
|
54
|
+
target = typing.cast(MySQLColumn, target)
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
source.data_type != target.data_type
|
|
58
|
+
or source.nullable != target.nullable
|
|
59
|
+
or source.default != target.default
|
|
60
|
+
or source.identity != target.identity
|
|
61
|
+
or source.comment != target.comment
|
|
62
|
+
):
|
|
63
|
+
return f"MODIFY COLUMN {source.name} {target.data_spec}"
|
|
64
|
+
else:
|
|
65
|
+
return None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pysqlsync.formation.object_types import (
|
|
5
|
+
Column,
|
|
6
|
+
EnumTable,
|
|
7
|
+
FormationError,
|
|
8
|
+
ObjectFactory,
|
|
9
|
+
Table,
|
|
10
|
+
)
|
|
11
|
+
from pysqlsync.model.data_types import SqlEnumType, quote
|
|
12
|
+
from pysqlsync.model.id_types import LocalId
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MySQLTable(Table):
|
|
16
|
+
@property
|
|
17
|
+
def short_description(self) -> Optional[str]:
|
|
18
|
+
if self.description is None:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
return self.description if "\n" not in self.description else self.description[: self.description.index("\n")]
|
|
22
|
+
|
|
23
|
+
def create_stmt(self) -> str:
|
|
24
|
+
defs: list[str] = []
|
|
25
|
+
defs.extend(str(c) for c in self.columns.values())
|
|
26
|
+
primary_columns = ", ".join(str(key) for key in self.primary_key)
|
|
27
|
+
defs.append(f"CONSTRAINT {self.primary_key_constraint_id} PRIMARY KEY ({primary_columns})")
|
|
28
|
+
definition = ",\n".join(defs)
|
|
29
|
+
comment = f"\nCOMMENT = {quote(self.short_description)}" if self.short_description else ""
|
|
30
|
+
return f"CREATE TABLE {self.name} (\n{definition}\n){comment};"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MySQLEnumTable(EnumTable, MySQLTable):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(eq=True)
|
|
38
|
+
class MySQLColumn(Column):
|
|
39
|
+
@property
|
|
40
|
+
def data_spec(self) -> str:
|
|
41
|
+
charset = " CHARACTER SET ascii COLLATE ascii_bin" if isinstance(self.data_type, SqlEnumType) else ""
|
|
42
|
+
nullable = " NOT NULL" if not self.nullable else ""
|
|
43
|
+
default = f" DEFAULT {self.default}" if self.default is not None else ""
|
|
44
|
+
identity = " AUTO_INCREMENT" if self.identity else ""
|
|
45
|
+
description = f" COMMENT {self.comment}" if self.description is not None else ""
|
|
46
|
+
return f"{self.data_type}{charset}{nullable}{default}{identity}{description}"
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def comment(self) -> Optional[str]:
|
|
50
|
+
if self.description is not None:
|
|
51
|
+
description = self.description if "\n" not in self.description else self.description[: self.description.index("\n")]
|
|
52
|
+
|
|
53
|
+
if len(description) > 1024:
|
|
54
|
+
raise FormationError(f"comment for column {self.name} too long, expected: maximum 1024; got: {len(description)}")
|
|
55
|
+
|
|
56
|
+
return quote(description)
|
|
57
|
+
else:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def rename_stmt(self, name: str) -> str:
|
|
61
|
+
return f"CHANGE {self.name} {LocalId(name)} {self.data_spec}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MySQLObjectFactory(ObjectFactory):
|
|
65
|
+
@property
|
|
66
|
+
def column_class(self) -> type[Column]:
|
|
67
|
+
return MySQLColumn
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def table_class(self) -> type[Table]:
|
|
71
|
+
return MySQLTable
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def enum_table_class(self) -> type[EnumTable]:
|
|
75
|
+
return MySQLEnumTable
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Oracle dialect for pysqlsync
|
|
2
|
+
|
|
3
|
+
## Testing
|
|
4
|
+
|
|
5
|
+
Refer to [Oracle Database Free Release Quick Start](https://www.oracle.com/database/free/get-started/) for setting up Oracle in Docker.
|
|
6
|
+
|
|
7
|
+
Specifically, pull the Docker image with the command:
|
|
8
|
+
```sh
|
|
9
|
+
docker pull container-registry.oracle.com/database/free:latest
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Next, start the Docker container with the command:
|
|
13
|
+
```sh
|
|
14
|
+
docker run -d --name oracle-db -e "ORACLE_PWD=<?YourStrong@Passw0rd>" -p 1521:1521 container-registry.oracle.com/database/free:latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
In order to inspect database startup logs, run:
|
|
18
|
+
```sh
|
|
19
|
+
docker logs oracle-db
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Finally, run unit and integration tests.
|
|
File without changes
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
import typing
|
|
4
|
+
from typing import Iterable, Optional, TypeVar
|
|
5
|
+
|
|
6
|
+
import oracledb
|
|
7
|
+
from strong_typing.inspection import DataclassInstance, is_dataclass_type
|
|
8
|
+
|
|
9
|
+
from pysqlsync.base import BaseConnection, BaseContext, DataSource, QueryException, RecordType
|
|
10
|
+
from pysqlsync.formation.object_types import Table
|
|
11
|
+
from pysqlsync.model.data_types import escape_like
|
|
12
|
+
from pysqlsync.model.id_types import LocalId
|
|
13
|
+
from pysqlsync.resultset import resultset_unwrap_tuple
|
|
14
|
+
from pysqlsync.util.dispatch import thread_dispatch
|
|
15
|
+
from pysqlsync.util.typing import override
|
|
16
|
+
|
|
17
|
+
from .data_types import sql_to_oracle_type
|
|
18
|
+
|
|
19
|
+
D = TypeVar("D", bound=DataclassInstance)
|
|
20
|
+
T = TypeVar("T")
|
|
21
|
+
|
|
22
|
+
LOGGER = logging.getLogger("pysqlsync.oracle")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OracleConnection(BaseConnection):
|
|
26
|
+
native: oracledb.Connection
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
@thread_dispatch
|
|
30
|
+
def open(self) -> BaseContext:
|
|
31
|
+
LOGGER.info("connecting to %s", self.params)
|
|
32
|
+
|
|
33
|
+
host = self.params.host or "localhost"
|
|
34
|
+
port = self.params.port or 1521
|
|
35
|
+
database = self.params.database or "FREEPDB1"
|
|
36
|
+
|
|
37
|
+
conn = oracledb.connect(
|
|
38
|
+
user=self.params.username,
|
|
39
|
+
password=self.params.password,
|
|
40
|
+
dsn=f"{host}:{port}/{database}",
|
|
41
|
+
)
|
|
42
|
+
conn.autocommit = True
|
|
43
|
+
with conn.cursor() as cur:
|
|
44
|
+
cur.execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'")
|
|
45
|
+
|
|
46
|
+
self.native = conn
|
|
47
|
+
return OracleContext(self)
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
@thread_dispatch
|
|
51
|
+
def close(self) -> None:
|
|
52
|
+
self.native.close()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class OracleContext(BaseContext):
|
|
56
|
+
def __init__(self, connection: OracleConnection) -> None:
|
|
57
|
+
super().__init__(connection)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def native_connection(self) -> oracledb.Connection:
|
|
61
|
+
return typing.cast(OracleConnection, self.connection).native
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
@thread_dispatch
|
|
65
|
+
def _execute(self, statement: str) -> None:
|
|
66
|
+
with self.native_connection.cursor() as cur:
|
|
67
|
+
if statement.startswith("BEGIN"):
|
|
68
|
+
cur.execute(statement)
|
|
69
|
+
else:
|
|
70
|
+
statement = statement.rstrip("\r\n\t\v ;")
|
|
71
|
+
for s in re.split(r";$", statement, flags=re.MULTILINE):
|
|
72
|
+
try:
|
|
73
|
+
cur.execute(s)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise QueryException(s) from e
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
async def _execute_all(self, statement: str, source: DataSource) -> None:
|
|
79
|
+
async for batch in source.batches():
|
|
80
|
+
await self._internal_execute_all(statement, batch)
|
|
81
|
+
|
|
82
|
+
@override
|
|
83
|
+
async def _execute_typed(
|
|
84
|
+
self,
|
|
85
|
+
statement: str,
|
|
86
|
+
source: DataSource,
|
|
87
|
+
table: Table,
|
|
88
|
+
order: Optional[tuple[str, ...]] = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
async for batch in source.batches():
|
|
91
|
+
await self._internal_execute_typed(statement, batch, table, order)
|
|
92
|
+
|
|
93
|
+
@thread_dispatch
|
|
94
|
+
def _internal_execute_all(self, statement: str, records: Iterable[RecordType]) -> None:
|
|
95
|
+
statement = statement.rstrip("\r\n\t\v ;")
|
|
96
|
+
with self.native_connection.cursor() as cur:
|
|
97
|
+
cur.executemany(statement, list(records))
|
|
98
|
+
|
|
99
|
+
@thread_dispatch
|
|
100
|
+
def _internal_execute_typed(
|
|
101
|
+
self,
|
|
102
|
+
statement: str,
|
|
103
|
+
records: Iterable[RecordType],
|
|
104
|
+
table: Table,
|
|
105
|
+
order: Optional[tuple[str, ...]] = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
statement = statement.rstrip("\r\n\t\v ;")
|
|
108
|
+
with self.native_connection.cursor() as cur:
|
|
109
|
+
cur.setinputsizes(*tuple(sql_to_oracle_type(cur, column.data_type) for column in table.get_columns(order)))
|
|
110
|
+
cur.executemany(statement, list(records))
|
|
111
|
+
|
|
112
|
+
@override
|
|
113
|
+
@thread_dispatch
|
|
114
|
+
def _query_all(self, signature: type[T], statement: str) -> list[T]:
|
|
115
|
+
with self.native_connection.cursor() as cur:
|
|
116
|
+
cur.execute(statement)
|
|
117
|
+
if is_dataclass_type(signature):
|
|
118
|
+
cur.rowfactory = lambda *args: signature(*args)
|
|
119
|
+
|
|
120
|
+
records = cur.fetchall()
|
|
121
|
+
|
|
122
|
+
if is_dataclass_type(signature):
|
|
123
|
+
return typing.cast(list[T], records)
|
|
124
|
+
else:
|
|
125
|
+
return resultset_unwrap_tuple(signature, records)
|
|
126
|
+
|
|
127
|
+
@override
|
|
128
|
+
async def current_schema(self) -> Optional[str]:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
@override
|
|
132
|
+
async def drop_schema(self, namespace: LocalId) -> None:
|
|
133
|
+
LOGGER.debug("drop schema: %s", namespace)
|
|
134
|
+
condition = f"table_name LIKE '{escape_like(namespace.id, '~')}~_~_%' ESCAPE '~'"
|
|
135
|
+
tables = await self.query_all(
|
|
136
|
+
str,
|
|
137
|
+
f"SELECT table_name FROM dba_tables WHERE {condition}",
|
|
138
|
+
)
|
|
139
|
+
statement = "\n".join(f"DROP TABLE {LocalId(table)} CASCADE CONSTRAINTS PURGE;" for table in tables)
|
|
140
|
+
if statement:
|
|
141
|
+
await self.execute(statement)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import oracledb
|
|
2
|
+
|
|
3
|
+
from pysqlsync.model.data_types import (
|
|
4
|
+
SqlBooleanType,
|
|
5
|
+
SqlDataType,
|
|
6
|
+
SqlDateType,
|
|
7
|
+
SqlDecimalType,
|
|
8
|
+
SqlDoubleType,
|
|
9
|
+
SqlFixedCharacterType,
|
|
10
|
+
SqlIntegerType,
|
|
11
|
+
SqlRealType,
|
|
12
|
+
SqlTimestampType,
|
|
13
|
+
SqlTimeType,
|
|
14
|
+
SqlVariableBinaryType,
|
|
15
|
+
SqlVariableCharacterType,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OracleIntegerType(SqlIntegerType):
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
super().__init__(8)
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
return "number"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OracleTimestampType(SqlTimestampType):
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
if self.precision is not None and self.precision != 6:
|
|
30
|
+
precision = f"({self.precision})"
|
|
31
|
+
else:
|
|
32
|
+
precision = ""
|
|
33
|
+
if self.with_time_zone:
|
|
34
|
+
time_zone = " with time zone"
|
|
35
|
+
else:
|
|
36
|
+
time_zone = ""
|
|
37
|
+
return f"timestamp{precision}{time_zone}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OracleTimeType(SqlTimeType):
|
|
41
|
+
def __str__(self) -> str:
|
|
42
|
+
return "interval day to second"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class OracleVariableCharacterType(SqlVariableCharacterType):
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
if self.limit is not None and self.limit <= 4000:
|
|
48
|
+
return f"varchar2({self.limit})"
|
|
49
|
+
else:
|
|
50
|
+
return "clob"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OracleVariableBinaryType(SqlVariableBinaryType):
|
|
54
|
+
def __str__(self) -> str:
|
|
55
|
+
if self.storage is not None and self.storage <= 2000:
|
|
56
|
+
return f"raw({self.storage})"
|
|
57
|
+
else:
|
|
58
|
+
return "blob"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def sql_to_oracle_type(cur: oracledb.Cursor, data_type: SqlDataType) -> oracledb.Var:
|
|
62
|
+
"""
|
|
63
|
+
Returns the Oracle data type associated with the SQL data type.
|
|
64
|
+
|
|
65
|
+
Passing the right data types to `setinputsizes` eliminates data-based guessing, and can speed up `executemany`
|
|
66
|
+
by a significant factor.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
if isinstance(data_type, SqlBooleanType):
|
|
70
|
+
return cur.var(oracledb.DB_TYPE_BOOLEAN)
|
|
71
|
+
elif isinstance(data_type, SqlRealType):
|
|
72
|
+
return cur.var(oracledb.DB_TYPE_BINARY_FLOAT)
|
|
73
|
+
elif isinstance(data_type, SqlDoubleType):
|
|
74
|
+
return cur.var(oracledb.DB_TYPE_BINARY_DOUBLE)
|
|
75
|
+
elif isinstance(data_type, SqlIntegerType):
|
|
76
|
+
if data_type.width > 4:
|
|
77
|
+
return cur.var(oracledb.DB_TYPE_NUMBER)
|
|
78
|
+
else:
|
|
79
|
+
return cur.var(oracledb.DB_TYPE_BINARY_INTEGER, data_type.width)
|
|
80
|
+
elif isinstance(data_type, SqlVariableBinaryType):
|
|
81
|
+
if data_type.storage is not None:
|
|
82
|
+
return cur.var(oracledb.DB_TYPE_RAW, data_type.storage)
|
|
83
|
+
else:
|
|
84
|
+
return cur.var(oracledb.DB_TYPE_BLOB)
|
|
85
|
+
elif isinstance(data_type, SqlFixedCharacterType):
|
|
86
|
+
if data_type.limit is not None:
|
|
87
|
+
return cur.var(oracledb.DB_TYPE_CHAR, data_type.limit)
|
|
88
|
+
else:
|
|
89
|
+
return cur.var(oracledb.DB_TYPE_CHAR, 4000)
|
|
90
|
+
elif isinstance(data_type, SqlVariableCharacterType):
|
|
91
|
+
if data_type.limit is not None:
|
|
92
|
+
return cur.var(oracledb.DB_TYPE_VARCHAR, data_type.limit)
|
|
93
|
+
else:
|
|
94
|
+
return cur.var(oracledb.DB_TYPE_CLOB)
|
|
95
|
+
elif isinstance(data_type, SqlDateType):
|
|
96
|
+
return cur.var(oracledb.DB_TYPE_DATE)
|
|
97
|
+
elif isinstance(data_type, SqlTimeType):
|
|
98
|
+
return cur.var(oracledb.DB_TYPE_INTERVAL_DS)
|
|
99
|
+
elif isinstance(data_type, SqlDecimalType):
|
|
100
|
+
return cur.var(oracledb.DB_TYPE_NUMBER)
|
|
101
|
+
elif isinstance(data_type, SqlTimestampType):
|
|
102
|
+
return cur.var(oracledb.DB_TYPE_TIMESTAMP)
|
|
103
|
+
|
|
104
|
+
return cur.var(oracledb.DB_TYPE_UNKNOWN)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from pysqlsync.base import BaseContext, Explorer
|
|
7
|
+
from pysqlsync.formation.constraints import ForeignFactory, UniqueFactory
|
|
8
|
+
from pysqlsync.formation.data_types import SqlDiscovery, SqlDiscoveryOptions
|
|
9
|
+
from pysqlsync.formation.object_types import Column, Constraint, Namespace, Table
|
|
10
|
+
from pysqlsync.model.data_types import escape_like, quote
|
|
11
|
+
from pysqlsync.model.id_types import (
|
|
12
|
+
LocalId,
|
|
13
|
+
PrefixedId,
|
|
14
|
+
QualifiedId,
|
|
15
|
+
SupportsQualifiedId,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .data_types import (
|
|
19
|
+
OracleIntegerType,
|
|
20
|
+
OracleTimestampType,
|
|
21
|
+
OracleTimeType,
|
|
22
|
+
OracleVariableBinaryType,
|
|
23
|
+
OracleVariableCharacterType,
|
|
24
|
+
)
|
|
25
|
+
from .object_types import OracleObjectFactory
|
|
26
|
+
|
|
27
|
+
LOGGER = logging.getLogger("pysqlsync.oracle")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class OracleColumnMeta:
|
|
32
|
+
column_name: str
|
|
33
|
+
data_type: str
|
|
34
|
+
data_type_owner: str
|
|
35
|
+
data_length: int
|
|
36
|
+
data_precision: int
|
|
37
|
+
data_scale: int
|
|
38
|
+
is_nullable: bool
|
|
39
|
+
data_default: str
|
|
40
|
+
char_length: int
|
|
41
|
+
is_identity: bool
|
|
42
|
+
comments: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class OracleConstraintMeta:
|
|
47
|
+
constraint_name: str
|
|
48
|
+
constraint_type: str
|
|
49
|
+
source_column: str
|
|
50
|
+
target_table: str
|
|
51
|
+
target_column: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class OracleExplorer(Explorer):
|
|
55
|
+
discovery: SqlDiscovery
|
|
56
|
+
factory: OracleObjectFactory
|
|
57
|
+
|
|
58
|
+
def __init__(self, conn: BaseContext) -> None:
|
|
59
|
+
super().__init__(conn)
|
|
60
|
+
self.discovery = SqlDiscovery(
|
|
61
|
+
SqlDiscoveryOptions(
|
|
62
|
+
substitutions={
|
|
63
|
+
"BLOB": OracleVariableBinaryType(),
|
|
64
|
+
"CLOB": OracleVariableCharacterType(),
|
|
65
|
+
"INTERVAL DAY": OracleTimeType(),
|
|
66
|
+
"LONG": OracleVariableCharacterType(),
|
|
67
|
+
"NUMBER": OracleIntegerType(),
|
|
68
|
+
"RAW": OracleVariableBinaryType(),
|
|
69
|
+
"TIMESTAMP": OracleTimestampType(),
|
|
70
|
+
"VARCHAR2": OracleVariableCharacterType(),
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
self.factory = OracleObjectFactory()
|
|
75
|
+
|
|
76
|
+
def get_qualified_id(self, namespace: Optional[str], id: str) -> SupportsQualifiedId:
|
|
77
|
+
return PrefixedId(namespace, id)
|
|
78
|
+
|
|
79
|
+
def split_composite_id(self, name: str) -> SupportsQualifiedId:
|
|
80
|
+
if "__" in name:
|
|
81
|
+
parts = name.split("__", 1)
|
|
82
|
+
return PrefixedId(parts[0], parts[1])
|
|
83
|
+
else:
|
|
84
|
+
return PrefixedId(None, name)
|
|
85
|
+
|
|
86
|
+
async def get_table_names(self) -> list[QualifiedId]:
|
|
87
|
+
raise NotImplementedError()
|
|
88
|
+
|
|
89
|
+
async def has_table(self, table_id: SupportsQualifiedId) -> bool:
|
|
90
|
+
raise NotImplementedError()
|
|
91
|
+
|
|
92
|
+
async def has_column(self, table_id: SupportsQualifiedId, column_id: LocalId) -> bool:
|
|
93
|
+
raise NotImplementedError()
|
|
94
|
+
|
|
95
|
+
async def get_table(self, table_id: SupportsQualifiedId) -> Table:
|
|
96
|
+
condition = f"t.table_name = {quote(table_id.local_id)}"
|
|
97
|
+
|
|
98
|
+
table_comments = await self.conn.query_all(
|
|
99
|
+
str,
|
|
100
|
+
"SELECT tc.comments\n"
|
|
101
|
+
"FROM dba_tables t JOIN dba_tab_comments tc ON t.owner = tc.owner AND t.table_name = tc.table_name\n"
|
|
102
|
+
f"WHERE t.owner != 'SYS' AND {condition}\n AND t.tablespace_name != 'SYSAUX'\n"
|
|
103
|
+
" AND t.status = 'VALID' AND t.temporary = 'N' AND t.secondary = 'N' AND t.dropped = 'NO' AND t.read_only = 'NO'",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
column_records = await self.conn.query_all(
|
|
107
|
+
OracleColumnMeta,
|
|
108
|
+
"SELECT\n"
|
|
109
|
+
" t.column_name,\n"
|
|
110
|
+
" t.data_type,\n"
|
|
111
|
+
" t.data_type_owner,\n"
|
|
112
|
+
" t.data_length,\n"
|
|
113
|
+
" t.data_precision,\n"
|
|
114
|
+
" t.data_scale,\n"
|
|
115
|
+
" t.nullable != 'N' AS is_nullable,\n"
|
|
116
|
+
" t.data_default,\n"
|
|
117
|
+
" t.char_length,\n"
|
|
118
|
+
" t.identity_column = 'YES' AS is_identity,\n"
|
|
119
|
+
" tc.comments\n"
|
|
120
|
+
"FROM dba_tab_columns t\n"
|
|
121
|
+
" JOIN dba_col_comments tc ON t.owner = tc.owner AND t.table_name = tc.table_name AND t.column_name = tc.column_name\n"
|
|
122
|
+
f"WHERE {condition}\n"
|
|
123
|
+
"ORDER BY column_id",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
columns: list[Column] = []
|
|
127
|
+
for col in column_records:
|
|
128
|
+
char_length: Optional[int]
|
|
129
|
+
if col.data_type in ["BLOB", "CLOB"]:
|
|
130
|
+
char_length = None
|
|
131
|
+
else:
|
|
132
|
+
char_length = col.char_length or col.data_length
|
|
133
|
+
m = re.match("^([^()]+)", col.data_type)
|
|
134
|
+
if m is not None:
|
|
135
|
+
data_type = m.group(1)
|
|
136
|
+
else:
|
|
137
|
+
data_type = col.data_type
|
|
138
|
+
|
|
139
|
+
data_type = self.discovery.sql_data_type_from_spec(
|
|
140
|
+
type_name=data_type,
|
|
141
|
+
type_schema=col.data_type_owner,
|
|
142
|
+
type_def=col.data_type,
|
|
143
|
+
character_maximum_length=char_length,
|
|
144
|
+
numeric_precision=col.data_precision,
|
|
145
|
+
numeric_scale=col.data_scale,
|
|
146
|
+
)
|
|
147
|
+
nullable = bool(col.is_nullable)
|
|
148
|
+
identity = bool(col.is_identity)
|
|
149
|
+
columns.append(
|
|
150
|
+
self.factory.column_class(
|
|
151
|
+
LocalId(col.column_name),
|
|
152
|
+
data_type,
|
|
153
|
+
nullable,
|
|
154
|
+
identity=identity,
|
|
155
|
+
default=col.data_default if not identity else None,
|
|
156
|
+
description=col.comments,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
constraint_records = await self.conn.query_all(
|
|
161
|
+
OracleConstraintMeta,
|
|
162
|
+
f"SELECT\n"
|
|
163
|
+
" t.constraint_name AS constraint_name,\n"
|
|
164
|
+
" t.constraint_type AS constraint_type,\n"
|
|
165
|
+
" tc.column_name AS source_column,\n"
|
|
166
|
+
" r.table_name AS target_table,\n"
|
|
167
|
+
" rc.column_name AS target_column\n"
|
|
168
|
+
"FROM dba_constraints t\n"
|
|
169
|
+
" JOIN dba_cons_columns tc ON t.owner = tc.owner AND t.constraint_name = tc.constraint_name\n"
|
|
170
|
+
" LEFT JOIN dba_constraints r ON t.r_owner = r.owner AND t.r_constraint_name = r.constraint_name\n"
|
|
171
|
+
" LEFT JOIN dba_cons_columns rc ON r.owner = rc.owner AND r.constraint_name = rc.constraint_name\n"
|
|
172
|
+
f"WHERE {condition}\n"
|
|
173
|
+
"ORDER BY tc.position",
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
primary: list[LocalId] = []
|
|
177
|
+
unique = UniqueFactory()
|
|
178
|
+
foreign = ForeignFactory()
|
|
179
|
+
for con in constraint_records:
|
|
180
|
+
if con.constraint_type == "P":
|
|
181
|
+
primary.append(LocalId(con.source_column))
|
|
182
|
+
elif con.constraint_type == "U":
|
|
183
|
+
unique.add(con.constraint_name, LocalId(con.source_column))
|
|
184
|
+
elif con.constraint_type == "R":
|
|
185
|
+
foreign.add(
|
|
186
|
+
con.constraint_name,
|
|
187
|
+
LocalId(con.source_column),
|
|
188
|
+
self.split_composite_id(con.target_table),
|
|
189
|
+
LocalId(con.target_column),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
constraints: list[Constraint] = []
|
|
193
|
+
constraints.extend(unique.fetch())
|
|
194
|
+
constraints.extend(foreign.fetch())
|
|
195
|
+
|
|
196
|
+
return self.factory.table_class(
|
|
197
|
+
name=table_id,
|
|
198
|
+
columns=columns,
|
|
199
|
+
primary_key=tuple(primary),
|
|
200
|
+
constraints=constraints or None,
|
|
201
|
+
description=table_comments[0] if table_comments else None,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
async def get_namespace_current(self) -> Namespace:
|
|
205
|
+
return await self._get_namespace()
|
|
206
|
+
|
|
207
|
+
async def get_namespace(self, namespace_id: LocalId) -> Namespace:
|
|
208
|
+
return await self._get_namespace(namespace_id)
|
|
209
|
+
|
|
210
|
+
async def _get_namespace(self, namespace_id: Optional[LocalId] = None) -> Namespace:
|
|
211
|
+
if namespace_id is not None:
|
|
212
|
+
condition = f"table_name LIKE '{escape_like(namespace_id.id, '~')}~_~_%' ESCAPE '~'"
|
|
213
|
+
else:
|
|
214
|
+
condition = "table_name NOT LIKE '%~_~_%' ESCAPE '~'"
|
|
215
|
+
|
|
216
|
+
table_names = await self.conn.query_all(
|
|
217
|
+
str,
|
|
218
|
+
"SELECT table_name FROM dba_tables\n"
|
|
219
|
+
f"WHERE owner != 'SYS' AND {condition}\n AND table_name NOT LIKE '%$%' AND tablespace_name != 'SYSAUX'\n"
|
|
220
|
+
" AND status = 'VALID' AND temporary = 'N' AND secondary = 'N' AND dropped = 'NO' AND read_only = 'NO'",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
tables: list[Table] = []
|
|
224
|
+
for table_name in table_names:
|
|
225
|
+
table = await self.get_table(self.split_composite_id(table_name))
|
|
226
|
+
tables.append(table)
|
|
227
|
+
|
|
228
|
+
if tables:
|
|
229
|
+
return self.factory.namespace_class(enums=[], structs=[], tables=tables)
|
|
230
|
+
else:
|
|
231
|
+
return self.factory.namespace_class()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pysqlsync.base import BaseConnection, BaseEngine, BaseGenerator, Explorer
|
|
2
|
+
|
|
3
|
+
from .connection import OracleConnection
|
|
4
|
+
from .discovery import OracleExplorer
|
|
5
|
+
from .generator import OracleGenerator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OracleEngine(BaseEngine):
|
|
9
|
+
@property
|
|
10
|
+
def name(self) -> str:
|
|
11
|
+
return "oracle"
|
|
12
|
+
|
|
13
|
+
def get_generator_type(self) -> type[BaseGenerator]:
|
|
14
|
+
return OracleGenerator
|
|
15
|
+
|
|
16
|
+
def get_connection_type(self) -> type[BaseConnection]:
|
|
17
|
+
return OracleConnection
|
|
18
|
+
|
|
19
|
+
def get_explorer_type(self) -> type[Explorer]:
|
|
20
|
+
return OracleExplorer
|