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
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
import typing
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from typing import Iterable, Optional, TypeVar
|
|
7
|
+
|
|
8
|
+
import redshift_connector
|
|
9
|
+
from strong_typing.inspection import DataclassInstance, is_dataclass_type
|
|
10
|
+
|
|
11
|
+
from pysqlsync.base import (
|
|
12
|
+
BaseConnection,
|
|
13
|
+
BaseContext,
|
|
14
|
+
ClassRef,
|
|
15
|
+
DataSource,
|
|
16
|
+
QueryException,
|
|
17
|
+
RecordType,
|
|
18
|
+
)
|
|
19
|
+
from pysqlsync.formation.object_types import Table
|
|
20
|
+
from pysqlsync.model.id_types import LocalId, SupportsQualifiedId
|
|
21
|
+
from pysqlsync.model.properties import is_identity_type
|
|
22
|
+
from pysqlsync.resultset import resultset_unwrap_object, resultset_unwrap_tuple
|
|
23
|
+
from pysqlsync.util.dispatch import thread_dispatch
|
|
24
|
+
from pysqlsync.util.typing import override
|
|
25
|
+
|
|
26
|
+
D = TypeVar("D", bound=DataclassInstance)
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
|
|
29
|
+
LOGGER = logging.getLogger("pysqlsync.redshift")
|
|
30
|
+
|
|
31
|
+
redshift_connector.paramstyle = "numeric"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RedshiftConnection(BaseConnection):
|
|
35
|
+
native: redshift_connector.Connection
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
@thread_dispatch
|
|
39
|
+
def open(self) -> BaseContext:
|
|
40
|
+
LOGGER.info("connecting to %s", self.params)
|
|
41
|
+
conn = redshift_connector.connect(
|
|
42
|
+
iam=True,
|
|
43
|
+
host=self.params.host,
|
|
44
|
+
port=self.params.port,
|
|
45
|
+
database=self.params.database,
|
|
46
|
+
db_user=self.params.username,
|
|
47
|
+
)
|
|
48
|
+
conn.autocommit = True
|
|
49
|
+
|
|
50
|
+
self.native = conn
|
|
51
|
+
return RedshiftContext(self)
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
@thread_dispatch
|
|
55
|
+
def close(self) -> None:
|
|
56
|
+
self.native.close()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RedshiftContext(BaseContext):
|
|
60
|
+
def __init__(self, connection: RedshiftConnection) -> None:
|
|
61
|
+
super().__init__(connection)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def native_connection(self) -> redshift_connector.Connection:
|
|
65
|
+
return typing.cast(RedshiftConnection, self.connection).native
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
@thread_dispatch
|
|
69
|
+
def _execute(self, statement: str) -> None:
|
|
70
|
+
with self.native_connection.cursor() as cursor:
|
|
71
|
+
# The library `redshift_connector` executes all statements as prepared statements, which locks out
|
|
72
|
+
# the possibility to execute multiple statements in one go. We artificially split SQL scripts into
|
|
73
|
+
# multiple statements to work around this limitation in the connector.
|
|
74
|
+
statement = statement.rstrip("\r\n\t\v ;")
|
|
75
|
+
for s in re.split(r";$", statement, flags=re.MULTILINE):
|
|
76
|
+
try:
|
|
77
|
+
cursor.execute(s)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
raise QueryException(s) from e
|
|
80
|
+
|
|
81
|
+
@override
|
|
82
|
+
async def _execute_all(self, statement: str, source: DataSource) -> None:
|
|
83
|
+
async for batch in source.batches():
|
|
84
|
+
await self._internal_execute_all(statement, batch)
|
|
85
|
+
|
|
86
|
+
@thread_dispatch
|
|
87
|
+
def _internal_execute_all(self, statement: str, records: Iterable[RecordType]) -> None:
|
|
88
|
+
with self.native_connection.cursor() as cursor:
|
|
89
|
+
cursor.executemany(statement, records)
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
@thread_dispatch
|
|
93
|
+
def _query_all(self, signature: type[T], statement: str) -> list[T]:
|
|
94
|
+
with self.native_connection.cursor() as cursor:
|
|
95
|
+
records = cursor.execute(statement).fetchall()
|
|
96
|
+
|
|
97
|
+
if is_dataclass_type(signature):
|
|
98
|
+
return resultset_unwrap_object(signature, records) # type: ignore
|
|
99
|
+
else:
|
|
100
|
+
return resultset_unwrap_tuple(signature, records)
|
|
101
|
+
|
|
102
|
+
@override
|
|
103
|
+
@thread_dispatch
|
|
104
|
+
def insert_data(self, table: type[D], data: Iterable[D]) -> None:
|
|
105
|
+
if not is_dataclass_type(table):
|
|
106
|
+
raise TypeError(f"expected dataclass type, got: {table}")
|
|
107
|
+
generator = self.connection.generator
|
|
108
|
+
table_name = generator.get_qualified_id(ClassRef(table))
|
|
109
|
+
columns = [LocalId(field.name) for field in dataclasses.fields(table) if not is_identity_type(field.type)]
|
|
110
|
+
records = generator.get_dataclasses_as_records(table, data, skip_identity=True)
|
|
111
|
+
self._insert_copy_stream(table_name, columns, records)
|
|
112
|
+
|
|
113
|
+
@override
|
|
114
|
+
async def _insert_rows(
|
|
115
|
+
self,
|
|
116
|
+
table: Table,
|
|
117
|
+
source: DataSource,
|
|
118
|
+
*,
|
|
119
|
+
field_types: tuple[type, ...],
|
|
120
|
+
field_names: Optional[tuple[str, ...]] = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
order = tuple(name for name in field_names if name) if field_names else None
|
|
123
|
+
columns = [col.name for col in table.get_columns(order)]
|
|
124
|
+
record_generator = await self._generate_records(table, source, field_types=field_types, field_names=field_names)
|
|
125
|
+
async for batch in record_generator.batches():
|
|
126
|
+
await self._insert_copy_stream_async(table.name, columns, batch)
|
|
127
|
+
|
|
128
|
+
@thread_dispatch
|
|
129
|
+
def _insert_copy_stream_async(
|
|
130
|
+
self,
|
|
131
|
+
table_name: SupportsQualifiedId,
|
|
132
|
+
columns: list[LocalId],
|
|
133
|
+
records: Iterable[RecordType],
|
|
134
|
+
) -> None:
|
|
135
|
+
self._insert_copy_stream(table_name, columns, records)
|
|
136
|
+
|
|
137
|
+
def _insert_copy_stream(
|
|
138
|
+
self,
|
|
139
|
+
table_name: SupportsQualifiedId,
|
|
140
|
+
columns: list[LocalId],
|
|
141
|
+
records: Iterable[RecordType],
|
|
142
|
+
) -> None:
|
|
143
|
+
column_list = ", ".join(str(column) for column in columns)
|
|
144
|
+
copy_query = f"COPY {table_name} ({column_list}) FROM STDIN"
|
|
145
|
+
|
|
146
|
+
with BytesIO() as f:
|
|
147
|
+
for record in records:
|
|
148
|
+
f.write(b"|".join(str(value).encode("utf-8") for value in record))
|
|
149
|
+
f.write(b"\n")
|
|
150
|
+
stream_data = f.getvalue()
|
|
151
|
+
|
|
152
|
+
cursor = self.native_connection.cursor()
|
|
153
|
+
try:
|
|
154
|
+
cursor.execute(copy_query, stream=BytesIO(stream_data))
|
|
155
|
+
finally:
|
|
156
|
+
cursor.close()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from pysqlsync.model.data_types import SqlVariableBinaryType
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RedshiftVariableBinaryType(SqlVariableBinaryType):
|
|
5
|
+
def __str__(self) -> str:
|
|
6
|
+
if self.storage is not None:
|
|
7
|
+
storage = f"({self.storage})"
|
|
8
|
+
else:
|
|
9
|
+
storage = ""
|
|
10
|
+
return f"varbyte{storage}"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pysqlsync: Synchronize schema and large volumes of data.
|
|
3
|
+
|
|
4
|
+
This module defines dependencies required for AWS Redshift.
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/instructure-internal/pysqlsync
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import redshift_connector # pyright: ignore[reportUnusedImport] # noqa: F401
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pysqlsync.base import BaseConnection, BaseEngine, BaseGenerator, Explorer
|
|
2
|
+
|
|
3
|
+
from ..postgresql.discovery import PostgreSQLExplorer
|
|
4
|
+
from .connection import RedshiftConnection
|
|
5
|
+
from .generator import RedshiftGenerator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RedshiftEngine(BaseEngine):
|
|
9
|
+
@property
|
|
10
|
+
def name(self) -> str:
|
|
11
|
+
return "redshift"
|
|
12
|
+
|
|
13
|
+
def get_generator_type(self) -> type[BaseGenerator]:
|
|
14
|
+
return RedshiftGenerator
|
|
15
|
+
|
|
16
|
+
def get_connection_type(self) -> type[BaseConnection]:
|
|
17
|
+
return RedshiftConnection
|
|
18
|
+
|
|
19
|
+
def get_explorer_type(self) -> type[Explorer]:
|
|
20
|
+
return PostgreSQLExplorer
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from strong_typing.core import JsonType
|
|
6
|
+
|
|
7
|
+
from pysqlsync.base import BaseGenerator, GeneratorOptions
|
|
8
|
+
from pysqlsync.formation.inspection import is_ip_address_type
|
|
9
|
+
from pysqlsync.formation.object_types import Column
|
|
10
|
+
from pysqlsync.formation.py_to_sql import (
|
|
11
|
+
ArrayMode,
|
|
12
|
+
DataclassConverter,
|
|
13
|
+
DataclassConverterOptions,
|
|
14
|
+
EnumMode,
|
|
15
|
+
NamespaceMapping,
|
|
16
|
+
StructMode,
|
|
17
|
+
)
|
|
18
|
+
from pysqlsync.model.data_types import SqlVariableCharacterType
|
|
19
|
+
from pysqlsync.util.typing import override
|
|
20
|
+
|
|
21
|
+
from ..postgresql.mutation import PostgreSQLMutator
|
|
22
|
+
from ..postgresql.object_types import PostgreSQLObjectFactory
|
|
23
|
+
from .data_types import RedshiftVariableBinaryType
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RedshiftGenerator(BaseGenerator):
|
|
27
|
+
"Generator for AWS Redshift."
|
|
28
|
+
|
|
29
|
+
converter: DataclassConverter
|
|
30
|
+
|
|
31
|
+
def __init__(self, options: GeneratorOptions) -> None:
|
|
32
|
+
super().__init__(
|
|
33
|
+
options,
|
|
34
|
+
PostgreSQLObjectFactory(),
|
|
35
|
+
PostgreSQLMutator(options.synchronization),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
self.check_enum_mode(matches=EnumMode.CHECK)
|
|
39
|
+
self.check_struct_mode(matches=StructMode.JSON)
|
|
40
|
+
self.check_array_mode(include=[ArrayMode.JSON, ArrayMode.RELATION])
|
|
41
|
+
|
|
42
|
+
self.converter = DataclassConverter(
|
|
43
|
+
options=DataclassConverterOptions(
|
|
44
|
+
enum_mode=options.enum_mode or EnumMode.RELATION,
|
|
45
|
+
struct_mode=options.struct_mode or StructMode.JSON,
|
|
46
|
+
array_mode=options.array_mode or ArrayMode.JSON,
|
|
47
|
+
namespaces=NamespaceMapping(options.namespaces),
|
|
48
|
+
foreign_constraints=False,
|
|
49
|
+
initialize_tables=options.initialize_tables,
|
|
50
|
+
substitutions={
|
|
51
|
+
uuid.UUID: RedshiftVariableBinaryType(16),
|
|
52
|
+
JsonType: SqlVariableCharacterType(),
|
|
53
|
+
ipaddress.IPv4Address: RedshiftVariableBinaryType(4),
|
|
54
|
+
ipaddress.IPv6Address: RedshiftVariableBinaryType(16),
|
|
55
|
+
},
|
|
56
|
+
factory=self.factory,
|
|
57
|
+
skip_annotations=options.skip_annotations,
|
|
58
|
+
auto_default=options.auto_default,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
def placeholder(self, index: int) -> str:
|
|
64
|
+
return f":{index}"
|
|
65
|
+
|
|
66
|
+
@override
|
|
67
|
+
def get_field_extractor(self, column: Column, field_name: str, field_type: type) -> Callable[[Any], Any]:
|
|
68
|
+
if field_type is uuid.UUID:
|
|
69
|
+
return lambda obj: getattr(obj, field_name).bytes
|
|
70
|
+
elif is_ip_address_type(field_type):
|
|
71
|
+
return lambda obj: getattr(obj, field_name).packed
|
|
72
|
+
|
|
73
|
+
return super().get_field_extractor(column, field_name, field_type)
|
|
74
|
+
|
|
75
|
+
@override
|
|
76
|
+
def get_value_transformer(self, column: Column, field_type: type) -> Optional[Callable[[Any], Any]]:
|
|
77
|
+
if field_type is uuid.UUID:
|
|
78
|
+
return lambda field: field.bytes
|
|
79
|
+
elif is_ip_address_type(field_type):
|
|
80
|
+
return lambda field: field.packed
|
|
81
|
+
|
|
82
|
+
return super().get_value_transformer(column, field_type)
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pysqlsync.model.data_types import SqlJsonType, SqlTimestampType
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SnowflakeDateTimeType(SqlTimestampType):
|
|
5
|
+
"Timestamp without time zone, equivalent to TIMESTAMP_NTZ."
|
|
6
|
+
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self.precision = 9
|
|
9
|
+
|
|
10
|
+
def __str__(self) -> str:
|
|
11
|
+
return "datetime"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SnowflakeJsonType(SqlJsonType):
|
|
15
|
+
"Represents JSON data extracted with PARSE_JSON."
|
|
16
|
+
|
|
17
|
+
def __str__(self) -> str:
|
|
18
|
+
return "variant"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pysqlsync.base import BaseConnection, BaseEngine, BaseGenerator, Explorer
|
|
2
|
+
|
|
3
|
+
from .generator import SnowflakeGenerator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SnowflakeEngine(BaseEngine):
|
|
7
|
+
@property
|
|
8
|
+
def name(self) -> str:
|
|
9
|
+
return "snowflake"
|
|
10
|
+
|
|
11
|
+
def get_generator_type(self) -> type[BaseGenerator]:
|
|
12
|
+
return SnowflakeGenerator
|
|
13
|
+
|
|
14
|
+
def get_connection_type(self) -> type[BaseConnection]:
|
|
15
|
+
raise NotImplementedError()
|
|
16
|
+
|
|
17
|
+
def get_explorer_type(self) -> type[Explorer]:
|
|
18
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import ipaddress
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from strong_typing.core import JsonType
|
|
6
|
+
|
|
7
|
+
from pysqlsync.base import BaseGenerator, GeneratorOptions
|
|
8
|
+
from pysqlsync.formation.mutation import Mutator
|
|
9
|
+
from pysqlsync.formation.py_to_sql import (
|
|
10
|
+
ArrayMode,
|
|
11
|
+
DataclassConverter,
|
|
12
|
+
DataclassConverterOptions,
|
|
13
|
+
EnumMode,
|
|
14
|
+
NamespaceMapping,
|
|
15
|
+
StructMode,
|
|
16
|
+
)
|
|
17
|
+
from pysqlsync.model.data_types import SqlFixedBinaryType
|
|
18
|
+
from pysqlsync.util.typing import override
|
|
19
|
+
|
|
20
|
+
from .data_types import SnowflakeDateTimeType, SnowflakeJsonType
|
|
21
|
+
from .object_types import SnowflakeObjectFactory
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SnowflakeGenerator(BaseGenerator):
|
|
25
|
+
"Generator for Snowflake."
|
|
26
|
+
|
|
27
|
+
converter: DataclassConverter
|
|
28
|
+
|
|
29
|
+
def __init__(self, options: GeneratorOptions) -> None:
|
|
30
|
+
super().__init__(
|
|
31
|
+
options,
|
|
32
|
+
SnowflakeObjectFactory(),
|
|
33
|
+
Mutator(options.synchronization),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.check_enum_mode(matches=EnumMode.CHECK)
|
|
37
|
+
self.check_struct_mode(matches=StructMode.JSON)
|
|
38
|
+
self.check_array_mode(include=[ArrayMode.JSON, ArrayMode.RELATION])
|
|
39
|
+
|
|
40
|
+
self.converter = DataclassConverter(
|
|
41
|
+
options=DataclassConverterOptions(
|
|
42
|
+
enum_mode=options.enum_mode or EnumMode.CHECK,
|
|
43
|
+
struct_mode=options.struct_mode or StructMode.JSON,
|
|
44
|
+
array_mode=options.array_mode or ArrayMode.JSON,
|
|
45
|
+
namespaces=NamespaceMapping(options.namespaces),
|
|
46
|
+
check_constraints=False,
|
|
47
|
+
foreign_constraints=False,
|
|
48
|
+
initialize_tables=options.initialize_tables,
|
|
49
|
+
substitutions={
|
|
50
|
+
datetime.datetime: SnowflakeDateTimeType(),
|
|
51
|
+
uuid.UUID: SqlFixedBinaryType(16),
|
|
52
|
+
JsonType: SnowflakeJsonType(),
|
|
53
|
+
ipaddress.IPv4Address: SqlFixedBinaryType(4),
|
|
54
|
+
ipaddress.IPv6Address: SqlFixedBinaryType(16),
|
|
55
|
+
},
|
|
56
|
+
factory=self.factory,
|
|
57
|
+
skip_annotations=options.skip_annotations,
|
|
58
|
+
auto_default=options.auto_default,
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
def placeholder(self, index: int) -> str:
|
|
64
|
+
return f":{index}"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pysqlsync.formation.object_types import Column, ObjectFactory, Table
|
|
5
|
+
from pysqlsync.model.data_types import SqlTimestampType
|
|
6
|
+
from pysqlsync.model.id_types import LocalId
|
|
7
|
+
|
|
8
|
+
_sql_quoted_str_table = str.maketrans(
|
|
9
|
+
{
|
|
10
|
+
"\\": "\\\\",
|
|
11
|
+
"'": "\\'",
|
|
12
|
+
'"': '\\"',
|
|
13
|
+
"\0": "\\0",
|
|
14
|
+
"\b": "\\b",
|
|
15
|
+
"\f": "\\f",
|
|
16
|
+
"\n": "\\n",
|
|
17
|
+
"\r": "\\r",
|
|
18
|
+
"\t": "\\t",
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sql_quoted_string(text: str) -> str:
|
|
24
|
+
if re.search(r"[\\\0\b\f\n\r\t]", text):
|
|
25
|
+
text = text.translate(_sql_quoted_str_table)
|
|
26
|
+
elif "'" in text:
|
|
27
|
+
text = text.replace("'", "''")
|
|
28
|
+
return f"'{text}'"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SnowflakeTable(Table):
|
|
32
|
+
def create_stmt(self) -> str:
|
|
33
|
+
defs: list[str] = []
|
|
34
|
+
defs.extend(str(c) for c in self.columns.values())
|
|
35
|
+
defs.append(self.create_keys())
|
|
36
|
+
definitions = ",\n".join(defs)
|
|
37
|
+
comment = f"\nCOMMENT = {sql_quoted_string(self.description)}" if self.description else ""
|
|
38
|
+
return f"CREATE TABLE {self.name} (\n{definitions}\n){comment};"
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def primary_key_constraint_id(self) -> LocalId:
|
|
42
|
+
return LocalId(f"pk_{self.name.local_id.replace('.', '_')}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SnowflakeColumn(Column):
|
|
46
|
+
@property
|
|
47
|
+
def default_expr(self) -> str:
|
|
48
|
+
if self.default is None:
|
|
49
|
+
raise ValueError("default value is NULL")
|
|
50
|
+
|
|
51
|
+
if isinstance(self.data_type, SqlTimestampType):
|
|
52
|
+
m = re.match(
|
|
53
|
+
r"^'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}) (?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})'$",
|
|
54
|
+
self.default,
|
|
55
|
+
)
|
|
56
|
+
if m:
|
|
57
|
+
return f"TIMESTAMP {self.default}"
|
|
58
|
+
|
|
59
|
+
return self.default
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def data_spec(self) -> str:
|
|
63
|
+
nullable = " NOT NULL" if not self.nullable and not self.identity else ""
|
|
64
|
+
default = f" DEFAULT {self.default_expr}" if self.default is not None else ""
|
|
65
|
+
identity = " IDENTITY" if self.identity else ""
|
|
66
|
+
description = f" COMMENT {self.comment}" if self.description is not None else ""
|
|
67
|
+
return f"{self.data_type}{nullable}{default}{identity}{description}"
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def comment(self) -> Optional[str]:
|
|
71
|
+
if self.description is not None:
|
|
72
|
+
return sql_quoted_string(self.description)
|
|
73
|
+
else:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SnowflakeObjectFactory(ObjectFactory):
|
|
78
|
+
@property
|
|
79
|
+
def column_class(self) -> type[Column]:
|
|
80
|
+
return SnowflakeColumn
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def table_class(self) -> type[Table]:
|
|
84
|
+
return SnowflakeTable
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pysqlsync: Synchronize schema and large volumes of data.
|
|
3
|
+
|
|
4
|
+
This module is only required for integration tests.
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/instructure-internal/pysqlsync
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# a fake import to trigger a ModuleNotFoundError exception
|
|
10
|
+
import module_not_found # pyright: ignore[reportMissingImports, reportUnusedImport] # noqa: F401
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from typing import Iterable, Optional, TypeVar
|
|
3
|
+
|
|
4
|
+
import aiotrino
|
|
5
|
+
from strong_typing.inspection import is_dataclass_type
|
|
6
|
+
|
|
7
|
+
from pysqlsync.base import BaseConnection, BaseContext, DataSource
|
|
8
|
+
from pysqlsync.formation.object_types import Table
|
|
9
|
+
from pysqlsync.resultset import resultset_unwrap_tuple
|
|
10
|
+
from pysqlsync.util.typing import override
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TrinoConnection(BaseConnection):
|
|
16
|
+
native: aiotrino.dbapi.Connection
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
async def open(self) -> BaseContext:
|
|
20
|
+
self.native = aiotrino.dbapi.connect(
|
|
21
|
+
host=self.params.host,
|
|
22
|
+
port=self.params.port,
|
|
23
|
+
user=self.params.username,
|
|
24
|
+
auth=aiotrino.auth.BasicAuthentication(self.params.username, self.params.password),
|
|
25
|
+
http_scheme="https",
|
|
26
|
+
catalog=self.params.database,
|
|
27
|
+
)
|
|
28
|
+
return TrinoContext(self)
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
async def close(self) -> None:
|
|
32
|
+
await self.native.close()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TrinoContext(BaseContext):
|
|
36
|
+
def __init__(self, connection: TrinoConnection) -> None:
|
|
37
|
+
super().__init__(connection)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def native_connection(self) -> aiotrino.dbapi.Connection:
|
|
41
|
+
return typing.cast(TrinoConnection, self.connection).native
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
async def _execute(self, statement: str) -> None:
|
|
45
|
+
cur = await self.native_connection.cursor()
|
|
46
|
+
await cur.execute(statement)
|
|
47
|
+
|
|
48
|
+
@override
|
|
49
|
+
async def _execute_all(self, statement: str, source: DataSource) -> None:
|
|
50
|
+
raise NotImplementedError("operation not supported for Trino")
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
async def _query_all(self, signature: type[T], statement: str) -> list[T]:
|
|
54
|
+
if is_dataclass_type(signature):
|
|
55
|
+
raise NotImplementedError()
|
|
56
|
+
|
|
57
|
+
cur = await self.native_connection.cursor()
|
|
58
|
+
await cur.execute(statement)
|
|
59
|
+
records = await cur.fetchall()
|
|
60
|
+
return resultset_unwrap_tuple(signature, records)
|
|
61
|
+
|
|
62
|
+
@override
|
|
63
|
+
async def insert_data(self, table: type[T], data: Iterable[T]) -> None:
|
|
64
|
+
raise NotImplementedError("operation not supported for Trino")
|
|
65
|
+
|
|
66
|
+
@override
|
|
67
|
+
async def _insert_rows(
|
|
68
|
+
self,
|
|
69
|
+
table: Table,
|
|
70
|
+
records: DataSource,
|
|
71
|
+
*,
|
|
72
|
+
field_types: tuple[type, ...],
|
|
73
|
+
field_names: Optional[tuple[str, ...]] = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
raise NotImplementedError("operation not supported for Trino")
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
async def _upsert_rows(
|
|
79
|
+
self,
|
|
80
|
+
table: Table,
|
|
81
|
+
records: DataSource,
|
|
82
|
+
*,
|
|
83
|
+
field_types: tuple[type, ...],
|
|
84
|
+
field_names: Optional[tuple[str, ...]] = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
raise NotImplementedError("operation not supported for Trino")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pysqlsync: Synchronize schema and large volumes of data.
|
|
3
|
+
|
|
4
|
+
This module defines dependencies required to support the query engine Trino.
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/instructure-internal/pysqlsync
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import aiotrino # pyright: ignore[reportUnusedImport] # noqa: F401
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pysqlsync.base import BaseContext
|
|
2
|
+
from pysqlsync.formation.data_types import SqlDiscovery, SqlDiscoveryOptions
|
|
3
|
+
from pysqlsync.formation.discovery import AnsiExplorer
|
|
4
|
+
from pysqlsync.formation.object_types import ObjectFactory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TrinoExplorer(AnsiExplorer):
|
|
8
|
+
"""
|
|
9
|
+
Discovers objects in a database exposed via Trino.
|
|
10
|
+
|
|
11
|
+
We recommend setting the following configuration options in Trino:
|
|
12
|
+
* postgresql.array-mapping = AS_ARRAY
|
|
13
|
+
* postgresql.include-system-tables = true
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, conn: BaseContext) -> None:
|
|
17
|
+
super().__init__(
|
|
18
|
+
conn,
|
|
19
|
+
SqlDiscovery(SqlDiscoveryOptions(substitutions={})),
|
|
20
|
+
ObjectFactory(),
|
|
21
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pysqlsync.base import BaseConnection, BaseEngine, BaseGenerator, Explorer
|
|
2
|
+
|
|
3
|
+
from .connection import TrinoConnection
|
|
4
|
+
from .discovery import TrinoExplorer
|
|
5
|
+
from .generator import TrinoGenerator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TrinoEngine(BaseEngine):
|
|
9
|
+
@property
|
|
10
|
+
def name(self) -> str:
|
|
11
|
+
return "trino"
|
|
12
|
+
|
|
13
|
+
def get_generator_type(self) -> type[BaseGenerator]:
|
|
14
|
+
return TrinoGenerator
|
|
15
|
+
|
|
16
|
+
def get_connection_type(self) -> type[BaseConnection]:
|
|
17
|
+
return TrinoConnection
|
|
18
|
+
|
|
19
|
+
def get_explorer_type(self) -> type[Explorer]:
|
|
20
|
+
return TrinoExplorer
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pysqlsync.base import BaseGenerator, GeneratorOptions
|
|
2
|
+
from pysqlsync.formation.py_to_sql import (
|
|
3
|
+
ArrayMode,
|
|
4
|
+
DataclassConverter,
|
|
5
|
+
DataclassConverterOptions,
|
|
6
|
+
EnumMode,
|
|
7
|
+
NamespaceMapping,
|
|
8
|
+
StructMode,
|
|
9
|
+
)
|
|
10
|
+
from pysqlsync.util.typing import override
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TrinoGenerator(BaseGenerator):
|
|
14
|
+
def __init__(self, options: GeneratorOptions) -> None:
|
|
15
|
+
super().__init__(options)
|
|
16
|
+
self.converter = DataclassConverter(
|
|
17
|
+
options=DataclassConverterOptions(
|
|
18
|
+
enum_mode=options.enum_mode or EnumMode.CHECK,
|
|
19
|
+
struct_mode=options.struct_mode or StructMode.JSON,
|
|
20
|
+
array_mode=options.array_mode or ArrayMode.JSON,
|
|
21
|
+
namespaces=NamespaceMapping(options.namespaces),
|
|
22
|
+
foreign_constraints=options.foreign_constraints,
|
|
23
|
+
initialize_tables=options.initialize_tables,
|
|
24
|
+
skip_annotations=options.skip_annotations,
|
|
25
|
+
auto_default=options.auto_default,
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
def placeholder(self, index: int) -> str:
|
|
31
|
+
raise NotImplementedError("operation not supported for Trino")
|