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
pysqlsync/factory.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pysqlsync: Synchronize schema and large volumes of data.
|
|
3
|
+
|
|
4
|
+
This module helps discover and register database dialects.
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/instructure-internal/pysqlsync
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
import importlib.resources
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import typing
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from urllib.parse import parse_qs, unquote, urlparse
|
|
16
|
+
|
|
17
|
+
from strong_typing.inspection import get_module_classes
|
|
18
|
+
|
|
19
|
+
from .base import BaseConnection, BaseEngine, BaseGenerator, Explorer
|
|
20
|
+
from .connection import ConnectionParameters, ConnectionSSLMode
|
|
21
|
+
|
|
22
|
+
LOGGER = logging.getLogger("pysqlsync")
|
|
23
|
+
|
|
24
|
+
_engines: dict[str, BaseEngine] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def register_dialect(engine_name: str, engine_factory: BaseEngine) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Dynamically registers a new database engine dialect.
|
|
30
|
+
|
|
31
|
+
Connections strings to registered dialects are automatically recognized by pysqlsync.
|
|
32
|
+
|
|
33
|
+
:param engine_name: The dialect name such as `postgres` or `mysql`.
|
|
34
|
+
:param engine_factory: The engine factory to register, which creates connections, explorers and generators.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if engine_name in _engines:
|
|
38
|
+
raise ValueError(f"engine already registered: {engine_name}")
|
|
39
|
+
|
|
40
|
+
_engines[engine_name] = engine_factory
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def unregister_dialect(engine_name: str) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Dynamically removes a database engine dialect.
|
|
46
|
+
|
|
47
|
+
:param engine_name: The dialect name such as `postgres` or `mysql`.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_engines.pop(engine_name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_dialect(engine_name: str) -> BaseEngine:
|
|
54
|
+
"Looks up a database engine dialect based on its reference name."
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
engine_factory = _engines[engine_name]
|
|
58
|
+
except KeyError:
|
|
59
|
+
raise ValueError(f"unrecognized dialect: {engine_name}") from None
|
|
60
|
+
else:
|
|
61
|
+
return engine_factory
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_parameters(url: str) -> tuple[str, ConnectionParameters]:
|
|
65
|
+
"""
|
|
66
|
+
Extracts dialect and connection parameters from a connection string URL.
|
|
67
|
+
|
|
68
|
+
The connection string URL has the following structure:
|
|
69
|
+
```
|
|
70
|
+
dialect://username:password@hostname:port/database
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For example,
|
|
74
|
+
```
|
|
75
|
+
postgresql://root:%3C%3FYour%3AStrong%40Pass%2Fw0rd%3E@server.example.com:5432/public
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
In the example above, dialect is `postgresql`, username is `root`, password is `<?Your:Strong@Pass/w0rd>`,
|
|
79
|
+
hostname is `server.example.com`, port is `5432` and the database name is `public`. Note how components are
|
|
80
|
+
URL-encoded (a.k.a. percent-encoded) to comply with RFC 3986.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
parts = urlparse(url, allow_fragments=False)
|
|
84
|
+
|
|
85
|
+
ssl: Optional[ConnectionSSLMode] = None
|
|
86
|
+
if parts.query:
|
|
87
|
+
query = parse_qs(parts.query, strict_parsing=True)
|
|
88
|
+
if "ssl" in query:
|
|
89
|
+
if len(query["ssl"]) != 1:
|
|
90
|
+
raise ValueError("only a single `ssl` parameter is permitted in a connection string")
|
|
91
|
+
ssl_mode = query["ssl"][0]
|
|
92
|
+
for v in ConnectionSSLMode.__members__.values():
|
|
93
|
+
if ssl_mode == v.value:
|
|
94
|
+
ssl = v
|
|
95
|
+
break
|
|
96
|
+
else:
|
|
97
|
+
raise ValueError(f"unsupported SSL mode: {ssl_mode}")
|
|
98
|
+
|
|
99
|
+
return parts.scheme, ConnectionParameters(
|
|
100
|
+
host=parts.hostname,
|
|
101
|
+
port=parts.port,
|
|
102
|
+
username=unquote(parts.username) if parts.username else None,
|
|
103
|
+
password=unquote(parts.password) if parts.password else None,
|
|
104
|
+
database=parts.path.lstrip("/") if parts.path else None,
|
|
105
|
+
ssl=ssl,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class UnavailableEngine(BaseEngine):
|
|
110
|
+
_name: str
|
|
111
|
+
_module: str
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def name(self) -> str:
|
|
115
|
+
return self._name
|
|
116
|
+
|
|
117
|
+
def __init__(self, name: str, module: str) -> None:
|
|
118
|
+
self._name = name
|
|
119
|
+
self._module = module
|
|
120
|
+
|
|
121
|
+
def get_generator_type(self) -> type[BaseGenerator]:
|
|
122
|
+
self._raise_error()
|
|
123
|
+
|
|
124
|
+
def get_connection_type(self) -> type[BaseConnection]:
|
|
125
|
+
self._raise_error()
|
|
126
|
+
|
|
127
|
+
def get_explorer_type(self) -> type[Explorer]:
|
|
128
|
+
self._raise_error()
|
|
129
|
+
|
|
130
|
+
def _raise_error(self) -> typing.NoReturn:
|
|
131
|
+
raise RuntimeError(f"missing dependency: `{self._module}`; you may need to run `pip install pysqlsync[{self._name}]`")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def discover_dialects() -> None:
|
|
135
|
+
"Discovers database engine dialects bundled with this package."
|
|
136
|
+
|
|
137
|
+
resources = importlib.resources.files(__package__).joinpath("dialect").iterdir()
|
|
138
|
+
for resource in resources:
|
|
139
|
+
if resource.name.startswith((".", "__")) or not resource.is_dir():
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
# run a preliminary check importing only the module `dependency`;
|
|
143
|
+
# if the check fails, it indicates that required dependencies have not been installed (e.g. database driver)
|
|
144
|
+
try:
|
|
145
|
+
module = importlib.import_module(f".dialect.{resource.name}.dependency", package=__package__)
|
|
146
|
+
except ModuleNotFoundError as e:
|
|
147
|
+
LOGGER.debug(
|
|
148
|
+
"skipping dialect `%s`: missing dependency: `%s`; you may need to run `pip install pysqlsync[%s]`",
|
|
149
|
+
resource.name,
|
|
150
|
+
e.name,
|
|
151
|
+
resource.name,
|
|
152
|
+
)
|
|
153
|
+
register_dialect(resource.name, UnavailableEngine(resource.name, e.name or ""))
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
# import the module `engine`, which acts as an entry point to connector, generator and explorer functionality
|
|
157
|
+
module = importlib.import_module(f".dialect.{resource.name}.engine", package=__package__)
|
|
158
|
+
classes = [cls for cls in get_module_classes(module) if re.match(r"^\w+Engine$", cls.__name__)]
|
|
159
|
+
engine_type = typing.cast(type[BaseEngine], classes.pop())
|
|
160
|
+
engine_factory = engine_type()
|
|
161
|
+
LOGGER.info(
|
|
162
|
+
"found dialect `%s` defined by `%s`",
|
|
163
|
+
engine_factory.name,
|
|
164
|
+
engine_type.__name__,
|
|
165
|
+
)
|
|
166
|
+
register_dialect(engine_factory.name, engine_factory)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
discover_dialects()
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from ..model.id_types import LocalId, SupportsQualifiedId
|
|
4
|
+
from .object_types import ConstraintReference, ForeignConstraint, UniqueConstraint
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UniqueFactory:
|
|
8
|
+
"Builds a list of unique constraints from a table of constraint names and columns."
|
|
9
|
+
|
|
10
|
+
name_to_columns: dict[str, list[LocalId]]
|
|
11
|
+
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self.name_to_columns = {}
|
|
14
|
+
|
|
15
|
+
def add(self, constraint_name: str, column: LocalId) -> None:
|
|
16
|
+
self.name_to_columns.setdefault(constraint_name, []).append(column)
|
|
17
|
+
|
|
18
|
+
def fetch(self) -> list[UniqueConstraint]:
|
|
19
|
+
return [UniqueConstraint(LocalId(name), tuple(columns)) for name, columns in self.name_to_columns.items()]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ColumnPair:
|
|
24
|
+
source: LocalId
|
|
25
|
+
target: LocalId
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ConstraintLink:
|
|
30
|
+
target_table: SupportsQualifiedId
|
|
31
|
+
column_pairs: list[ColumnPair]
|
|
32
|
+
|
|
33
|
+
def __init__(self, target_table: SupportsQualifiedId) -> None:
|
|
34
|
+
self.target_table = target_table
|
|
35
|
+
self.column_pairs = []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ForeignFactory:
|
|
39
|
+
"Builds a list of foreign key constraints from a table of constraint names, source and target columns."
|
|
40
|
+
|
|
41
|
+
name_to_constraint: dict[str, ConstraintLink]
|
|
42
|
+
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
self.target_table = None
|
|
45
|
+
self.name_to_constraint = {}
|
|
46
|
+
|
|
47
|
+
def add(
|
|
48
|
+
self,
|
|
49
|
+
constraint_name: str,
|
|
50
|
+
source_column: LocalId,
|
|
51
|
+
target_table: SupportsQualifiedId,
|
|
52
|
+
target_column: LocalId,
|
|
53
|
+
) -> None:
|
|
54
|
+
link = self.name_to_constraint.get(constraint_name)
|
|
55
|
+
if link is not None:
|
|
56
|
+
if target_table != link.target_table:
|
|
57
|
+
raise ValueError("inconsistent target table for foreign constraint")
|
|
58
|
+
else:
|
|
59
|
+
link = ConstraintLink(target_table)
|
|
60
|
+
self.name_to_constraint[constraint_name] = link
|
|
61
|
+
|
|
62
|
+
link.column_pairs.append(ColumnPair(source_column, target_column))
|
|
63
|
+
|
|
64
|
+
def fetch(self) -> list[ForeignConstraint]:
|
|
65
|
+
if not self.name_to_constraint:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
return [
|
|
69
|
+
ForeignConstraint(
|
|
70
|
+
LocalId(name),
|
|
71
|
+
tuple(item.source for item in link.column_pairs),
|
|
72
|
+
ConstraintReference(
|
|
73
|
+
link.target_table,
|
|
74
|
+
tuple(item.target for item in link.column_pairs),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
for name, link in self.name_to_constraint.items()
|
|
78
|
+
]
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import dataclasses
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from ..model.data_types import (
|
|
7
|
+
SqlBooleanType,
|
|
8
|
+
SqlDataType,
|
|
9
|
+
SqlDateType,
|
|
10
|
+
SqlDecimalType,
|
|
11
|
+
SqlDoubleType,
|
|
12
|
+
SqlEnumType,
|
|
13
|
+
SqlFixedBinaryType,
|
|
14
|
+
SqlFixedCharacterType,
|
|
15
|
+
SqlFloatType,
|
|
16
|
+
SqlIntegerType,
|
|
17
|
+
SqlJsonType,
|
|
18
|
+
SqlRealType,
|
|
19
|
+
SqlTimestampType,
|
|
20
|
+
SqlTimeType,
|
|
21
|
+
SqlUserDefinedType,
|
|
22
|
+
SqlUuidType,
|
|
23
|
+
SqlVariableBinaryType,
|
|
24
|
+
SqlVariableCharacterType,
|
|
25
|
+
)
|
|
26
|
+
from ..model.id_types import QualifiedId
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclasses.dataclass
|
|
30
|
+
class SqlDiscoveryOptions:
|
|
31
|
+
substitutions: dict[str, SqlDataType] = dataclasses.field(default_factory=dict[str, SqlDataType])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SqlDiscovery:
|
|
35
|
+
options: SqlDiscoveryOptions
|
|
36
|
+
|
|
37
|
+
def __init__(self, options: Optional[SqlDiscoveryOptions] = None) -> None:
|
|
38
|
+
self.options = options if options is not None else SqlDiscoveryOptions()
|
|
39
|
+
|
|
40
|
+
def sql_data_type_from_name(
|
|
41
|
+
self,
|
|
42
|
+
type_name: str,
|
|
43
|
+
type_schema: Optional[str] = None,
|
|
44
|
+
) -> Optional[SqlDataType]:
|
|
45
|
+
name = type_name.lower()
|
|
46
|
+
if name in ["bool", "boolean"]:
|
|
47
|
+
return SqlBooleanType()
|
|
48
|
+
elif name in ["tinyint", "tinyint(1)", "int1"]:
|
|
49
|
+
return SqlIntegerType(1)
|
|
50
|
+
elif name in ["smallint", "int2"]:
|
|
51
|
+
return SqlIntegerType(2)
|
|
52
|
+
elif name in ["integer", "int", "int4"]:
|
|
53
|
+
return SqlIntegerType(4)
|
|
54
|
+
elif name in ["bigint", "int8"]:
|
|
55
|
+
return SqlIntegerType(8)
|
|
56
|
+
elif name in ["decimal", "number", "numeric"]:
|
|
57
|
+
return SqlDecimalType()
|
|
58
|
+
elif name in ["real", "float4"]:
|
|
59
|
+
return SqlRealType()
|
|
60
|
+
elif name in ["double", "double precision", "float8"]:
|
|
61
|
+
return SqlDoubleType()
|
|
62
|
+
elif name == "float":
|
|
63
|
+
return SqlFloatType()
|
|
64
|
+
elif name in ["timestamp", "timestamp without time zone"]:
|
|
65
|
+
return SqlTimestampType(None, False)
|
|
66
|
+
elif name == "timestamp with time zone":
|
|
67
|
+
return SqlTimestampType(None, True)
|
|
68
|
+
elif name == "datetime":
|
|
69
|
+
return SqlTimestampType()
|
|
70
|
+
elif name == "date":
|
|
71
|
+
return SqlDateType()
|
|
72
|
+
elif name in ["time", "time without time zone"]:
|
|
73
|
+
return SqlTimeType(None, False)
|
|
74
|
+
elif name == "time with time zone":
|
|
75
|
+
return SqlTimeType(None, True)
|
|
76
|
+
elif name in ["char", "character"]:
|
|
77
|
+
return SqlFixedCharacterType()
|
|
78
|
+
elif name in ["varchar", "character varying", "text"]:
|
|
79
|
+
return SqlVariableCharacterType()
|
|
80
|
+
elif name == "binary":
|
|
81
|
+
return SqlFixedBinaryType()
|
|
82
|
+
elif name in ["varbinary", "binary varying", "bytea"]:
|
|
83
|
+
return SqlVariableBinaryType()
|
|
84
|
+
elif name in ["json", "jsonb"]: # PostgreSQL-specific
|
|
85
|
+
return SqlJsonType()
|
|
86
|
+
elif name == "uuid": # PostgreSQL-specific
|
|
87
|
+
return SqlUuidType()
|
|
88
|
+
|
|
89
|
+
if type_schema is not None:
|
|
90
|
+
return SqlUserDefinedType(QualifiedId(type_schema, type_name))
|
|
91
|
+
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def sql_data_type_from_def(self, type_def: str) -> Optional[SqlDataType]:
|
|
95
|
+
m = re.fullmatch(r"^(?:decimal|number|numeric)[(](\d+),\s*(\d+)[)]$", type_def, re.IGNORECASE)
|
|
96
|
+
if m is not None:
|
|
97
|
+
return SqlDecimalType(int(m.group(1)), int(m.group(2)))
|
|
98
|
+
|
|
99
|
+
m = re.fullmatch(
|
|
100
|
+
r"^timestamp[(](\d+)[)]\s*(with(?:out)? time zone)?$",
|
|
101
|
+
type_def,
|
|
102
|
+
re.IGNORECASE,
|
|
103
|
+
)
|
|
104
|
+
if m is not None:
|
|
105
|
+
if m.group(2) == "with time zone":
|
|
106
|
+
return SqlTimestampType(int(m.group(1)), True)
|
|
107
|
+
else:
|
|
108
|
+
return SqlTimestampType(int(m.group(1)), False)
|
|
109
|
+
|
|
110
|
+
m = re.fullmatch(r"^char(?:acter)?[(](\d+)[)]$", type_def, re.IGNORECASE)
|
|
111
|
+
if m is not None:
|
|
112
|
+
return SqlFixedCharacterType(int(m.group(1)))
|
|
113
|
+
|
|
114
|
+
m = re.fullmatch(r"^(?:varchar|character varying)[(](\d+)[)]$", type_def, re.IGNORECASE)
|
|
115
|
+
if m is not None:
|
|
116
|
+
return SqlVariableCharacterType(int(m.group(1)))
|
|
117
|
+
|
|
118
|
+
m = re.fullmatch(r"^binary[(](\d+)[)]$", type_def, re.IGNORECASE)
|
|
119
|
+
if m is not None:
|
|
120
|
+
return SqlFixedBinaryType(int(m.group(1)))
|
|
121
|
+
|
|
122
|
+
m = re.fullmatch(r"^(?:varbinary|binary varying)[(](\d+)[)]$", type_def, re.IGNORECASE)
|
|
123
|
+
if m is not None:
|
|
124
|
+
return SqlVariableBinaryType(int(m.group(1)))
|
|
125
|
+
|
|
126
|
+
# MySQL and Oracle
|
|
127
|
+
m = re.fullmatch(r"^enum[(](.+)[)]$", type_def, re.IGNORECASE)
|
|
128
|
+
if m is not None:
|
|
129
|
+
value_list = m.group(1)
|
|
130
|
+
values = [value for value in re.findall(r"'((?:[^']+|'')*)'(?:,|$)", value_list)]
|
|
131
|
+
return SqlEnumType(values)
|
|
132
|
+
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def sql_data_type_from_spec(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
type_name: str,
|
|
139
|
+
type_schema: Optional[str] = None,
|
|
140
|
+
type_def: Optional[str] = None,
|
|
141
|
+
character_maximum_length: Optional[int] = None,
|
|
142
|
+
numeric_precision: Optional[int] = None,
|
|
143
|
+
numeric_scale: Optional[int] = None,
|
|
144
|
+
datetime_precision: Optional[int] = None,
|
|
145
|
+
) -> SqlDataType:
|
|
146
|
+
"Determines the column type from SQL column attribute data extracted from the information schema table."
|
|
147
|
+
|
|
148
|
+
sql_type: Optional[SqlDataType] = None
|
|
149
|
+
if type_schema is None or type_schema == "pg_catalog" or type_schema == "SYS":
|
|
150
|
+
sql_type = copy.copy(self.options.substitutions.get(type_name))
|
|
151
|
+
|
|
152
|
+
if sql_type is None and type_def is not None:
|
|
153
|
+
sql_type = self.sql_data_type_from_def(type_def)
|
|
154
|
+
|
|
155
|
+
if sql_type is None:
|
|
156
|
+
sql_type = self.sql_data_type_from_name(type_name, type_schema)
|
|
157
|
+
|
|
158
|
+
if sql_type is None:
|
|
159
|
+
if type_schema is not None:
|
|
160
|
+
name = f"{type_schema}.{type_name}"
|
|
161
|
+
else:
|
|
162
|
+
name = type_name
|
|
163
|
+
raise TypeError(f"unrecognized SQL type: {name}")
|
|
164
|
+
|
|
165
|
+
if isinstance(sql_type, SqlDecimalType):
|
|
166
|
+
if numeric_precision is not None:
|
|
167
|
+
sql_type.precision = numeric_precision # precision in base 10
|
|
168
|
+
if numeric_scale is not None:
|
|
169
|
+
sql_type.scale = numeric_scale
|
|
170
|
+
elif isinstance(sql_type, SqlFloatType):
|
|
171
|
+
if numeric_precision is not None:
|
|
172
|
+
sql_type.precision = numeric_precision # precision in base 2
|
|
173
|
+
elif isinstance(sql_type, SqlTimestampType):
|
|
174
|
+
if datetime_precision is not None and datetime_precision > 0:
|
|
175
|
+
sql_type.precision = datetime_precision
|
|
176
|
+
elif isinstance(sql_type, SqlTimeType):
|
|
177
|
+
if datetime_precision is not None and datetime_precision > 0:
|
|
178
|
+
sql_type.precision = datetime_precision
|
|
179
|
+
elif isinstance(sql_type, SqlFixedCharacterType):
|
|
180
|
+
if character_maximum_length is not None:
|
|
181
|
+
sql_type.limit = character_maximum_length
|
|
182
|
+
elif isinstance(sql_type, SqlVariableCharacterType):
|
|
183
|
+
if character_maximum_length is not None and character_maximum_length > 0:
|
|
184
|
+
sql_type.limit = character_maximum_length
|
|
185
|
+
elif isinstance(sql_type, SqlFixedBinaryType):
|
|
186
|
+
if character_maximum_length is not None:
|
|
187
|
+
sql_type.storage = character_maximum_length
|
|
188
|
+
elif isinstance(sql_type, SqlVariableBinaryType):
|
|
189
|
+
if character_maximum_length is not None and character_maximum_length > 0:
|
|
190
|
+
sql_type.storage = character_maximum_length
|
|
191
|
+
|
|
192
|
+
return sql_type
|