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.
Files changed (102) hide show
  1. instructure_pysqlsync-0.8.5.dev0.dist-info/METADATA +244 -0
  2. instructure_pysqlsync-0.8.5.dev0.dist-info/RECORD +102 -0
  3. instructure_pysqlsync-0.8.5.dev0.dist-info/WHEEL +5 -0
  4. instructure_pysqlsync-0.8.5.dev0.dist-info/licenses/LICENSE +21 -0
  5. instructure_pysqlsync-0.8.5.dev0.dist-info/top_level.txt +1 -0
  6. instructure_pysqlsync-0.8.5.dev0.dist-info/zip-safe +1 -0
  7. pysqlsync/__init__.py +15 -0
  8. pysqlsync/base.py +1386 -0
  9. pysqlsync/connection.py +101 -0
  10. pysqlsync/data/__init__.py +0 -0
  11. pysqlsync/data/exchange.py +184 -0
  12. pysqlsync/data/generator.py +305 -0
  13. pysqlsync/dialect/README.md +35 -0
  14. pysqlsync/dialect/__init__.py +0 -0
  15. pysqlsync/dialect/delta/__init__.py +0 -0
  16. pysqlsync/dialect/delta/data_types.py +51 -0
  17. pysqlsync/dialect/delta/dependency.py +7 -0
  18. pysqlsync/dialect/delta/engine.py +18 -0
  19. pysqlsync/dialect/delta/generator.py +74 -0
  20. pysqlsync/dialect/delta/object_types.py +87 -0
  21. pysqlsync/dialect/mssql/README.md +23 -0
  22. pysqlsync/dialect/mssql/__init__.py +0 -0
  23. pysqlsync/dialect/mssql/connection.py +146 -0
  24. pysqlsync/dialect/mssql/data_types.py +113 -0
  25. pysqlsync/dialect/mssql/dependency.py +9 -0
  26. pysqlsync/dialect/mssql/discovery.py +99 -0
  27. pysqlsync/dialect/mssql/engine.py +20 -0
  28. pysqlsync/dialect/mssql/generator.py +88 -0
  29. pysqlsync/dialect/mssql/mutation.py +46 -0
  30. pysqlsync/dialect/mssql/object_types.py +135 -0
  31. pysqlsync/dialect/mysql/__init__.py +0 -0
  32. pysqlsync/dialect/mysql/connection.py +126 -0
  33. pysqlsync/dialect/mysql/data_types.py +40 -0
  34. pysqlsync/dialect/mysql/dependency.py +10 -0
  35. pysqlsync/dialect/mysql/discovery.py +145 -0
  36. pysqlsync/dialect/mysql/engine.py +20 -0
  37. pysqlsync/dialect/mysql/generator.py +140 -0
  38. pysqlsync/dialect/mysql/mutation.py +65 -0
  39. pysqlsync/dialect/mysql/object_types.py +75 -0
  40. pysqlsync/dialect/oracle/README.md +22 -0
  41. pysqlsync/dialect/oracle/__init__.py +0 -0
  42. pysqlsync/dialect/oracle/connection.py +141 -0
  43. pysqlsync/dialect/oracle/data_types.py +104 -0
  44. pysqlsync/dialect/oracle/dependency.py +9 -0
  45. pysqlsync/dialect/oracle/discovery.py +231 -0
  46. pysqlsync/dialect/oracle/engine.py +20 -0
  47. pysqlsync/dialect/oracle/generator.py +93 -0
  48. pysqlsync/dialect/oracle/mutation.py +37 -0
  49. pysqlsync/dialect/oracle/object_types.py +59 -0
  50. pysqlsync/dialect/postgresql/__init__.py +0 -0
  51. pysqlsync/dialect/postgresql/connection.py +133 -0
  52. pysqlsync/dialect/postgresql/data_types.py +6 -0
  53. pysqlsync/dialect/postgresql/dependency.py +9 -0
  54. pysqlsync/dialect/postgresql/discovery.py +392 -0
  55. pysqlsync/dialect/postgresql/engine.py +20 -0
  56. pysqlsync/dialect/postgresql/generator.py +99 -0
  57. pysqlsync/dialect/postgresql/mutation.py +82 -0
  58. pysqlsync/dialect/postgresql/object_types.py +99 -0
  59. pysqlsync/dialect/redshift/__init__.py +0 -0
  60. pysqlsync/dialect/redshift/connection.py +156 -0
  61. pysqlsync/dialect/redshift/data_types.py +10 -0
  62. pysqlsync/dialect/redshift/dependency.py +9 -0
  63. pysqlsync/dialect/redshift/engine.py +20 -0
  64. pysqlsync/dialect/redshift/generator.py +82 -0
  65. pysqlsync/dialect/snowflake/__init__.py +0 -0
  66. pysqlsync/dialect/snowflake/data_types.py +18 -0
  67. pysqlsync/dialect/snowflake/dependency.py +9 -0
  68. pysqlsync/dialect/snowflake/engine.py +18 -0
  69. pysqlsync/dialect/snowflake/generator.py +64 -0
  70. pysqlsync/dialect/snowflake/object_types.py +84 -0
  71. pysqlsync/dialect/test/__init__.py +0 -0
  72. pysqlsync/dialect/test/dependency.py +10 -0
  73. pysqlsync/dialect/trino/__init__.py +0 -0
  74. pysqlsync/dialect/trino/connection.py +86 -0
  75. pysqlsync/dialect/trino/dependency.py +9 -0
  76. pysqlsync/dialect/trino/discovery.py +21 -0
  77. pysqlsync/dialect/trino/engine.py +20 -0
  78. pysqlsync/dialect/trino/generator.py +31 -0
  79. pysqlsync/factory.py +169 -0
  80. pysqlsync/formation/__init__.py +0 -0
  81. pysqlsync/formation/constraints.py +78 -0
  82. pysqlsync/formation/data_types.py +192 -0
  83. pysqlsync/formation/discovery.py +390 -0
  84. pysqlsync/formation/inspection.py +179 -0
  85. pysqlsync/formation/mutation.py +320 -0
  86. pysqlsync/formation/object_dict.py +79 -0
  87. pysqlsync/formation/object_types.py +872 -0
  88. pysqlsync/formation/py_to_sql.py +1134 -0
  89. pysqlsync/formation/sql_to_py.py +194 -0
  90. pysqlsync/model/__init__.py +0 -0
  91. pysqlsync/model/data_types.py +397 -0
  92. pysqlsync/model/entity_types.py +68 -0
  93. pysqlsync/model/id_types.py +193 -0
  94. pysqlsync/model/key_types.py +38 -0
  95. pysqlsync/model/properties.py +234 -0
  96. pysqlsync/py.typed +0 -0
  97. pysqlsync/python_types.py +192 -0
  98. pysqlsync/resultset.py +106 -0
  99. pysqlsync/util/__init__.py +0 -0
  100. pysqlsync/util/dataclasses.py +80 -0
  101. pysqlsync/util/dispatch.py +28 -0
  102. 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,9 @@
1
+ """
2
+ pysqlsync: Synchronize schema and large volumes of data.
3
+
4
+ This module defines dependencies required for Snowflake.
5
+
6
+ :see: https://github.com/instructure-internal/pysqlsync
7
+ """
8
+
9
+ import snowflake # pyright: ignore[reportUnusedImport] # noqa: F401
@@ -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")