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,194 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime
|
|
3
|
+
import decimal
|
|
4
|
+
import keyword
|
|
5
|
+
import sys
|
|
6
|
+
import types
|
|
7
|
+
import typing
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from io import StringIO
|
|
10
|
+
from typing import Annotated, Any, Optional, Union
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from strong_typing.auxiliary import Length, MaxLength, Precision, TimePrecision, float32, float64, int16, int32, int64
|
|
14
|
+
from strong_typing.core import JsonType
|
|
15
|
+
from strong_typing.inspection import DataclassInstance, TypeLike
|
|
16
|
+
|
|
17
|
+
from ..model.data_types import (
|
|
18
|
+
SqlArrayType,
|
|
19
|
+
SqlBooleanType,
|
|
20
|
+
SqlDataType,
|
|
21
|
+
SqlDateType,
|
|
22
|
+
SqlDecimalType,
|
|
23
|
+
SqlDoubleType,
|
|
24
|
+
SqlFixedCharacterType,
|
|
25
|
+
SqlFloatType,
|
|
26
|
+
SqlIntegerType,
|
|
27
|
+
SqlJsonType,
|
|
28
|
+
SqlRealType,
|
|
29
|
+
SqlTimestampType,
|
|
30
|
+
SqlTimeType,
|
|
31
|
+
SqlUserDefinedType,
|
|
32
|
+
SqlUuidType,
|
|
33
|
+
SqlVariableCharacterType,
|
|
34
|
+
)
|
|
35
|
+
from ..model.id_types import SupportsQualifiedId
|
|
36
|
+
from ..model.key_types import PrimaryKey
|
|
37
|
+
from .object_types import Column, DiscriminatedConstraint, ForeignConstraint, ReferenceConstraint, Table
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sql_type_to_python(sql_type: SqlDataType) -> TypeLike:
|
|
41
|
+
if isinstance(sql_type, SqlBooleanType):
|
|
42
|
+
return bool
|
|
43
|
+
elif isinstance(sql_type, SqlIntegerType):
|
|
44
|
+
if sql_type.width == 2:
|
|
45
|
+
return int16
|
|
46
|
+
elif sql_type.width == 4:
|
|
47
|
+
return int32
|
|
48
|
+
elif sql_type.width == 8:
|
|
49
|
+
return int64
|
|
50
|
+
return int
|
|
51
|
+
elif isinstance(sql_type, SqlRealType):
|
|
52
|
+
return float32
|
|
53
|
+
elif isinstance(sql_type, SqlDoubleType):
|
|
54
|
+
return float64
|
|
55
|
+
elif isinstance(sql_type, SqlFloatType):
|
|
56
|
+
if sql_type.precision is not None:
|
|
57
|
+
return Annotated[float, Precision(sql_type.precision)]
|
|
58
|
+
return float
|
|
59
|
+
elif isinstance(sql_type, SqlDecimalType):
|
|
60
|
+
return decimal.Decimal
|
|
61
|
+
elif isinstance(sql_type, SqlTimestampType):
|
|
62
|
+
if sql_type.precision is not None:
|
|
63
|
+
return Annotated[datetime.datetime, TimePrecision(sql_type.precision)]
|
|
64
|
+
return datetime.datetime
|
|
65
|
+
elif isinstance(sql_type, SqlDateType):
|
|
66
|
+
return datetime.date
|
|
67
|
+
elif isinstance(sql_type, SqlTimeType):
|
|
68
|
+
if sql_type.precision is not None:
|
|
69
|
+
return Annotated[datetime.time, TimePrecision(sql_type.precision)]
|
|
70
|
+
return datetime.time
|
|
71
|
+
elif isinstance(sql_type, SqlFixedCharacterType):
|
|
72
|
+
if sql_type.limit is not None:
|
|
73
|
+
return Annotated[str, Length(sql_type.limit)]
|
|
74
|
+
return str
|
|
75
|
+
elif isinstance(sql_type, SqlVariableCharacterType):
|
|
76
|
+
if sql_type.limit is not None:
|
|
77
|
+
return Annotated[str, MaxLength(sql_type.limit)]
|
|
78
|
+
return str
|
|
79
|
+
elif isinstance(sql_type, SqlUuidType):
|
|
80
|
+
return UUID
|
|
81
|
+
elif isinstance(sql_type, SqlJsonType):
|
|
82
|
+
return JsonType
|
|
83
|
+
elif isinstance(sql_type, SqlUserDefinedType):
|
|
84
|
+
return sql_type.ref.compact_id
|
|
85
|
+
elif isinstance(sql_type, SqlArrayType):
|
|
86
|
+
item_type = (sql_type_to_python(sql_type.element_type),)
|
|
87
|
+
return list[item_type] # type: ignore
|
|
88
|
+
|
|
89
|
+
raise TypeError(f"unrecognized SQL type: {sql_type}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def safe_id(id: str) -> str:
|
|
93
|
+
"Apply PEP 8: single trailing underscore to avoid conflicts with Python keyword"
|
|
94
|
+
|
|
95
|
+
return f"{id}_" if keyword.iskeyword(id) else id
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class SqlConverterOptions:
|
|
100
|
+
namespaces: dict[Optional[str], types.ModuleType]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SqlConverter:
|
|
104
|
+
options: SqlConverterOptions
|
|
105
|
+
|
|
106
|
+
def __init__(self, options: SqlConverterOptions) -> None:
|
|
107
|
+
self.options = options
|
|
108
|
+
|
|
109
|
+
def qual_to_module(self, id: SupportsQualifiedId) -> str:
|
|
110
|
+
return f"{self.options.namespaces[id.scope_id].__name__}.{safe_id(id.local_id)}"
|
|
111
|
+
|
|
112
|
+
def column_to_field(self, table: Table, column: Column) -> tuple[str, type, dataclasses.Field[Any]]:
|
|
113
|
+
"""
|
|
114
|
+
Generates a dataclass field corresponding to a table column.
|
|
115
|
+
|
|
116
|
+
:param column: The database column from which to produce a dataclass field.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
field_name = safe_id(column.name.local_id)
|
|
120
|
+
|
|
121
|
+
default: Any = dataclasses.MISSING
|
|
122
|
+
if column.default is not None:
|
|
123
|
+
default = column.default
|
|
124
|
+
|
|
125
|
+
field_type = sql_type_to_python(column.data_type)
|
|
126
|
+
if (column.name,) == table.primary_key:
|
|
127
|
+
field_type = PrimaryKey[(field_type,)] # type: ignore
|
|
128
|
+
elif column.nullable:
|
|
129
|
+
field_type = Optional[field_type]
|
|
130
|
+
|
|
131
|
+
for c in table.constraints.values():
|
|
132
|
+
if not isinstance(c, ReferenceConstraint):
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if column.name not in c.foreign_columns:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if isinstance(c, ForeignConstraint):
|
|
139
|
+
field_type = self.qual_to_module(c.reference.table)
|
|
140
|
+
elif isinstance(c, DiscriminatedConstraint):
|
|
141
|
+
union_types = tuple(self.qual_to_module(r.table) for r in c.references)
|
|
142
|
+
field_type = Union[union_types]
|
|
143
|
+
|
|
144
|
+
# use cast to ensure compatibility with signature of `make_dataclass`
|
|
145
|
+
data_type = typing.cast(type, field_type)
|
|
146
|
+
|
|
147
|
+
return (field_name, data_type, dataclasses.field(default=default))
|
|
148
|
+
|
|
149
|
+
def table_to_dataclass(self, table: Table) -> type[DataclassInstance]:
|
|
150
|
+
"""
|
|
151
|
+
Generates a dataclass type corresponding to a table schema.
|
|
152
|
+
|
|
153
|
+
:param table: The database table from which to produce a dataclass.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
fields = [self.column_to_field(table, column) for column in table.columns.values()]
|
|
157
|
+
class_name = safe_id(table.name.local_id)
|
|
158
|
+
|
|
159
|
+
# default arguments must follow non-default arguments
|
|
160
|
+
fields.sort(key=lambda f: f[2].default is not dataclasses.MISSING)
|
|
161
|
+
|
|
162
|
+
# look up target module
|
|
163
|
+
module = self.options.namespaces[table.name.scope_id]
|
|
164
|
+
|
|
165
|
+
# produce class definition with docstring
|
|
166
|
+
if sys.version_info >= (3, 12):
|
|
167
|
+
typ = dataclasses.make_dataclass(class_name, fields, module=module.__name__)
|
|
168
|
+
else:
|
|
169
|
+
typ = dataclasses.make_dataclass(class_name, fields, namespace={"__module__": module.__name__})
|
|
170
|
+
with StringIO() as out:
|
|
171
|
+
for field in dataclasses.fields(typ):
|
|
172
|
+
description = field.metadata.get("description")
|
|
173
|
+
if description is not None:
|
|
174
|
+
print(f":param {field.name}: {description}", file=out)
|
|
175
|
+
paramstring = out.getvalue()
|
|
176
|
+
with StringIO() as out:
|
|
177
|
+
if table.description:
|
|
178
|
+
out.write(table.description)
|
|
179
|
+
if table.description and paramstring:
|
|
180
|
+
out.write("\n\n")
|
|
181
|
+
if paramstring:
|
|
182
|
+
out.write(paramstring)
|
|
183
|
+
docstring = out.getvalue()
|
|
184
|
+
typ.__doc__ = docstring
|
|
185
|
+
|
|
186
|
+
# assign the newly created type to the target module
|
|
187
|
+
setattr(sys.modules[module.__name__], class_name, typ)
|
|
188
|
+
|
|
189
|
+
return typ
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def table_to_dataclass(table: Table, options: SqlConverterOptions) -> type[DataclassInstance]:
|
|
193
|
+
converter = SqlConverter(options)
|
|
194
|
+
return converter.table_to_dataclass(table)
|
|
File without changes
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import decimal
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from strong_typing.auxiliary import IntegerRange, MaxLength, Precision, Signed, Storage, TimePrecision
|
|
8
|
+
from strong_typing.inspection import dataclass_fields, is_dataclass_instance
|
|
9
|
+
|
|
10
|
+
from .id_types import LocalId, SupportsQualifiedId
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def quote(s: str) -> str:
|
|
14
|
+
"Quotes a string to be embedded in an SQL statement."
|
|
15
|
+
|
|
16
|
+
return "'" + s.replace("'", "''") + "'"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def constant(v: Any) -> str:
|
|
20
|
+
"Outputs a constant value."
|
|
21
|
+
|
|
22
|
+
if isinstance(v, str):
|
|
23
|
+
return quote(v)
|
|
24
|
+
elif isinstance(v, bool):
|
|
25
|
+
return "TRUE" if v else "FALSE"
|
|
26
|
+
elif isinstance(v, (int, float)):
|
|
27
|
+
return str(v)
|
|
28
|
+
elif isinstance(v, decimal.Decimal):
|
|
29
|
+
return str(v)
|
|
30
|
+
elif isinstance(v, datetime.datetime):
|
|
31
|
+
if v.tzinfo is not None:
|
|
32
|
+
timestamp = v.astimezone(tz=datetime.timezone.utc).replace(tzinfo=None)
|
|
33
|
+
else:
|
|
34
|
+
timestamp = v
|
|
35
|
+
return quote(timestamp.isoformat(sep=" "))
|
|
36
|
+
elif isinstance(v, tuple):
|
|
37
|
+
values = ", ".join(constant(value) for value in v) # pyright: ignore[reportUnknownVariableType]
|
|
38
|
+
return f"({values})"
|
|
39
|
+
elif is_dataclass_instance(v):
|
|
40
|
+
values = ", ".join(constant(getattr(v, field.name)) for field in dataclass_fields(type(v)))
|
|
41
|
+
return f"({values})"
|
|
42
|
+
else:
|
|
43
|
+
raise NotImplementedError(f"unknown constant representation for value (of type): {v} ({type(v)})")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def escape_like(value: str, escape_char: str) -> str:
|
|
47
|
+
"Escapes a string to be embedded in an SQL LIKE '...' ESCAPE '...' expression."
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
value.replace("'", "''")
|
|
51
|
+
.replace(escape_char, f"{escape_char}{escape_char}")
|
|
52
|
+
.replace("_", f"{escape_char}_")
|
|
53
|
+
.replace("%", f"{escape_char}%")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class SqlDataType:
|
|
59
|
+
def parse_meta(self, meta: Any) -> None:
|
|
60
|
+
raise TypeError(f"unrecognized Python type annotation for {type(self).__name__}: {meta}")
|
|
61
|
+
|
|
62
|
+
def value_to_sql_literal(self, value: Any) -> str:
|
|
63
|
+
return constant(value)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class SqlArrayType(SqlDataType):
|
|
68
|
+
element_type: SqlDataType
|
|
69
|
+
|
|
70
|
+
def __str__(self) -> str:
|
|
71
|
+
return f"{self.element_type} ARRAY"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class SqlUuidType(SqlDataType):
|
|
76
|
+
def __str__(self) -> str:
|
|
77
|
+
return "uuid"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class SqlBooleanType(SqlDataType):
|
|
82
|
+
def __str__(self) -> str:
|
|
83
|
+
return "boolean"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class SqlIntegerType(SqlDataType):
|
|
88
|
+
width: int
|
|
89
|
+
signed: bool = True
|
|
90
|
+
minimum: Optional[int] = None
|
|
91
|
+
maximum: Optional[int] = None
|
|
92
|
+
|
|
93
|
+
def __str__(self) -> str:
|
|
94
|
+
if self.width == 1:
|
|
95
|
+
return "tinyint"
|
|
96
|
+
elif self.width == 2:
|
|
97
|
+
return "smallint"
|
|
98
|
+
elif self.width == 4:
|
|
99
|
+
return "integer"
|
|
100
|
+
elif self.width == 8:
|
|
101
|
+
return "bigint"
|
|
102
|
+
|
|
103
|
+
raise TypeError(f"invalid integer width: {self.width}")
|
|
104
|
+
|
|
105
|
+
def parse_meta(self, meta: Any) -> None:
|
|
106
|
+
if isinstance(meta, IntegerRange):
|
|
107
|
+
self.minimum = meta.minimum
|
|
108
|
+
self.maximum = meta.maximum
|
|
109
|
+
elif isinstance(meta, Signed):
|
|
110
|
+
self.signed = meta.is_signed
|
|
111
|
+
elif isinstance(meta, Storage):
|
|
112
|
+
self.width = meta.bytes
|
|
113
|
+
else:
|
|
114
|
+
super().parse_meta(meta)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class SqlFloatType(SqlDataType):
|
|
119
|
+
"""
|
|
120
|
+
Floating-point numeric type.
|
|
121
|
+
|
|
122
|
+
:param precision: Numeric precision in base 2.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
precision: Optional[int] = None
|
|
126
|
+
|
|
127
|
+
def __str__(self) -> str:
|
|
128
|
+
if self.precision is not None:
|
|
129
|
+
precision = self.precision
|
|
130
|
+
else:
|
|
131
|
+
precision = 53
|
|
132
|
+
|
|
133
|
+
return f"float({precision})"
|
|
134
|
+
|
|
135
|
+
def parse_meta(self, meta: Any) -> None:
|
|
136
|
+
if isinstance(meta, Precision):
|
|
137
|
+
self.precision = meta.significant_digits
|
|
138
|
+
else:
|
|
139
|
+
super().parse_meta(meta)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class SqlRealType(SqlDataType):
|
|
144
|
+
def __str__(self) -> str:
|
|
145
|
+
return "real"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class SqlDoubleType(SqlDataType):
|
|
150
|
+
def __str__(self) -> str:
|
|
151
|
+
return "double precision"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass
|
|
155
|
+
class SqlDecimalType(SqlDataType):
|
|
156
|
+
"""
|
|
157
|
+
Fixed-point numeric type.
|
|
158
|
+
|
|
159
|
+
:param precision: Numeric precision in base 10.
|
|
160
|
+
:param scale: Scale in base 10.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
precision: Optional[int] = None
|
|
164
|
+
scale: Optional[int] = None
|
|
165
|
+
|
|
166
|
+
def __str__(self) -> str:
|
|
167
|
+
if self.precision is not None and self.scale is not None:
|
|
168
|
+
return f"decimal({self.precision}, {self.scale})"
|
|
169
|
+
elif self.precision is not None:
|
|
170
|
+
return f"decimal({self.precision})"
|
|
171
|
+
else:
|
|
172
|
+
return "decimal"
|
|
173
|
+
|
|
174
|
+
def parse_meta(self, meta: Any) -> None:
|
|
175
|
+
if isinstance(meta, Precision):
|
|
176
|
+
self.precision = meta.significant_digits
|
|
177
|
+
self.scale = meta.decimal_digits
|
|
178
|
+
else:
|
|
179
|
+
super().parse_meta(meta)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class SqlFixedBinaryType(SqlDataType):
|
|
184
|
+
storage: Optional[int] = None
|
|
185
|
+
|
|
186
|
+
def __str__(self) -> str:
|
|
187
|
+
storage = f"({self.storage})" if self.storage is not None else ""
|
|
188
|
+
return f"binary{storage}"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass
|
|
192
|
+
class SqlVariableBinaryType(SqlDataType):
|
|
193
|
+
storage: Optional[int] = None
|
|
194
|
+
|
|
195
|
+
def __str__(self) -> str:
|
|
196
|
+
if self.storage is not None:
|
|
197
|
+
return f"varbinary({self.storage})"
|
|
198
|
+
else:
|
|
199
|
+
return "blob"
|
|
200
|
+
|
|
201
|
+
def parse_meta(self, meta: Any) -> None:
|
|
202
|
+
if isinstance(meta, Storage):
|
|
203
|
+
self.storage = meta.bytes
|
|
204
|
+
else:
|
|
205
|
+
super().parse_meta(meta)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass
|
|
209
|
+
class SqlFixedCharacterType(SqlDataType):
|
|
210
|
+
limit: Optional[int] = None
|
|
211
|
+
|
|
212
|
+
def __str__(self) -> str:
|
|
213
|
+
limit = f"({self.limit})" if self.limit is not None else ""
|
|
214
|
+
return f"char{limit}"
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class SqlVariableCharacterType(SqlDataType):
|
|
219
|
+
limit: Optional[int] = None
|
|
220
|
+
|
|
221
|
+
def __str__(self) -> str:
|
|
222
|
+
if self.limit is not None:
|
|
223
|
+
return f"varchar({self.limit})"
|
|
224
|
+
else:
|
|
225
|
+
return "text"
|
|
226
|
+
|
|
227
|
+
def parse_meta(self, meta: Any) -> None:
|
|
228
|
+
if isinstance(meta, MaxLength):
|
|
229
|
+
self.limit = meta.value
|
|
230
|
+
else:
|
|
231
|
+
super().parse_meta(meta)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@dataclass
|
|
235
|
+
class SqlTimestampType(SqlDataType):
|
|
236
|
+
precision: Optional[int] = None
|
|
237
|
+
with_time_zone: bool = False
|
|
238
|
+
|
|
239
|
+
def __str__(self) -> str:
|
|
240
|
+
if self.precision is not None:
|
|
241
|
+
precision = f"({self.precision})"
|
|
242
|
+
else:
|
|
243
|
+
precision = ""
|
|
244
|
+
if self.with_time_zone:
|
|
245
|
+
time_zone = " with time zone"
|
|
246
|
+
else:
|
|
247
|
+
time_zone = "" # PostgreSQL: " without time zone"
|
|
248
|
+
return f"timestamp{precision}{time_zone}"
|
|
249
|
+
|
|
250
|
+
def parse_meta(self, meta: Any) -> None:
|
|
251
|
+
if isinstance(meta, TimePrecision):
|
|
252
|
+
self.precision = meta.decimal_digits
|
|
253
|
+
else:
|
|
254
|
+
super().parse_meta(meta)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class SqlDateType(SqlDataType):
|
|
259
|
+
def __str__(self) -> str:
|
|
260
|
+
return "date"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@dataclass
|
|
264
|
+
class SqlTimeType(SqlDataType):
|
|
265
|
+
precision: Optional[int] = None
|
|
266
|
+
with_time_zone: bool = False
|
|
267
|
+
|
|
268
|
+
def __str__(self) -> str:
|
|
269
|
+
if self.precision is not None:
|
|
270
|
+
precision = f"({self.precision})"
|
|
271
|
+
else:
|
|
272
|
+
precision = ""
|
|
273
|
+
if self.with_time_zone:
|
|
274
|
+
time_zone = " with time zone"
|
|
275
|
+
else:
|
|
276
|
+
time_zone = "" # PostgreSQL: " without time zone"
|
|
277
|
+
return f"time{precision}{time_zone}"
|
|
278
|
+
|
|
279
|
+
def parse_meta(self, meta: Any) -> None:
|
|
280
|
+
if isinstance(meta, TimePrecision):
|
|
281
|
+
self.precision = meta.decimal_digits
|
|
282
|
+
else:
|
|
283
|
+
super().parse_meta(meta)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass
|
|
287
|
+
class SqlIntervalType(SqlDataType):
|
|
288
|
+
def __str__(self) -> str:
|
|
289
|
+
return "interval"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@dataclass
|
|
293
|
+
class SqlJsonType(SqlDataType):
|
|
294
|
+
def __str__(self) -> str:
|
|
295
|
+
return "json"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@dataclass
|
|
299
|
+
class SqlEnumType(SqlDataType):
|
|
300
|
+
values: list[str]
|
|
301
|
+
|
|
302
|
+
def __str__(self) -> str:
|
|
303
|
+
values = ", ".join(quote(val) for val in self.values)
|
|
304
|
+
return f"ENUM ({values})"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass
|
|
308
|
+
class SqlStructMember:
|
|
309
|
+
name: LocalId
|
|
310
|
+
data_type: SqlDataType
|
|
311
|
+
nullable: bool
|
|
312
|
+
|
|
313
|
+
def __str__(self) -> str:
|
|
314
|
+
nullable = " NOT NULL" if not self.nullable else ""
|
|
315
|
+
return f"{self.name} {self.data_type}{nullable}"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@dataclass
|
|
319
|
+
class SqlStructType(SqlDataType):
|
|
320
|
+
fields: list[SqlStructMember]
|
|
321
|
+
|
|
322
|
+
def __str__(self) -> str:
|
|
323
|
+
field_list = ", ".join(str(f) for f in self.fields)
|
|
324
|
+
return f"STRUCT <{field_list}>"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@dataclass
|
|
328
|
+
class SqlUserDefinedType(SqlDataType):
|
|
329
|
+
ref: SupportsQualifiedId
|
|
330
|
+
|
|
331
|
+
def __str__(self) -> str:
|
|
332
|
+
return self.ref.quoted_id
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class CompatibilityError(TypeError):
|
|
336
|
+
"Raised when a list of types cannot be reduced into a single common type."
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def compatible_type(data_types: list[SqlDataType]) -> SqlDataType:
|
|
340
|
+
"Returns a single common type that all elements in a list of types can be converted into without data loss."
|
|
341
|
+
|
|
342
|
+
return reduce(_compatible_type, data_types)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def max_or_none(left: Optional[int], right: Optional[int]) -> Optional[int]:
|
|
346
|
+
if left is None or right is None:
|
|
347
|
+
return None
|
|
348
|
+
else:
|
|
349
|
+
return max(left, right)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _compatible_type(left: SqlDataType, right: SqlDataType) -> SqlDataType:
|
|
353
|
+
"""
|
|
354
|
+
Returns a common type that can represent instances of both source types without data loss.
|
|
355
|
+
|
|
356
|
+
The implementation makes use of `type(...)` to ensure the most specific class is instantiated when
|
|
357
|
+
database-specific data types are created with inheritance.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
if left == right:
|
|
361
|
+
return left
|
|
362
|
+
|
|
363
|
+
# integer types
|
|
364
|
+
if isinstance(left, SqlIntegerType) and isinstance(right, SqlIntegerType):
|
|
365
|
+
return type(left)(width=max(left.width, right.width))
|
|
366
|
+
|
|
367
|
+
# floating-point types
|
|
368
|
+
if isinstance(left, SqlRealType):
|
|
369
|
+
if isinstance(right, SqlRealType):
|
|
370
|
+
return type(left)()
|
|
371
|
+
elif isinstance(right, SqlDoubleType):
|
|
372
|
+
return type(right)()
|
|
373
|
+
elif isinstance(left, SqlDoubleType):
|
|
374
|
+
if isinstance(right, (SqlRealType, SqlDoubleType)):
|
|
375
|
+
return type(left)()
|
|
376
|
+
|
|
377
|
+
# character types
|
|
378
|
+
if isinstance(left, SqlFixedCharacterType):
|
|
379
|
+
if isinstance(right, SqlFixedCharacterType):
|
|
380
|
+
return type(left)(limit=max_or_none(left.limit, right.limit))
|
|
381
|
+
elif isinstance(right, SqlVariableCharacterType):
|
|
382
|
+
return type(right)(limit=max_or_none(left.limit, right.limit))
|
|
383
|
+
elif isinstance(left, SqlVariableCharacterType):
|
|
384
|
+
if isinstance(right, (SqlFixedCharacterType, SqlVariableCharacterType)):
|
|
385
|
+
return type(left)(limit=max_or_none(left.limit, right.limit))
|
|
386
|
+
|
|
387
|
+
# binary types
|
|
388
|
+
if isinstance(left, SqlFixedBinaryType):
|
|
389
|
+
if isinstance(right, SqlFixedBinaryType):
|
|
390
|
+
return type(left)(storage=max_or_none(left.storage, right.storage))
|
|
391
|
+
elif isinstance(right, SqlVariableBinaryType):
|
|
392
|
+
return type(right)(storage=max_or_none(left.storage, right.storage))
|
|
393
|
+
elif isinstance(left, SqlVariableBinaryType):
|
|
394
|
+
if isinstance(right, (SqlFixedBinaryType, SqlVariableBinaryType)):
|
|
395
|
+
return type(left)(storage=max_or_none(left.storage, right.storage))
|
|
396
|
+
|
|
397
|
+
raise CompatibilityError()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from strong_typing.inspection import DataclassField, DataclassInstance, dataclass_fields
|
|
6
|
+
|
|
7
|
+
from .key_types import PrimaryKey
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def key_value_fields(cls: type[DataclassInstance], key: str) -> tuple[DataclassField, list[DataclassField]]:
|
|
11
|
+
"""
|
|
12
|
+
Separates the key and the value fields in a data-class definition.
|
|
13
|
+
|
|
14
|
+
:param cls: The data-class type to transform.
|
|
15
|
+
:param key: The field to be come the primary key.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
source_fields = dataclass_fields(cls)
|
|
19
|
+
key_field: Optional[DataclassField] = None
|
|
20
|
+
value_fields: list[DataclassField] = []
|
|
21
|
+
for source_field in source_fields:
|
|
22
|
+
if source_field.name == key:
|
|
23
|
+
key_field = source_field
|
|
24
|
+
else:
|
|
25
|
+
value_fields.append(source_field)
|
|
26
|
+
|
|
27
|
+
if key_field is None:
|
|
28
|
+
raise ValueError(f"key field not found: {key}")
|
|
29
|
+
|
|
30
|
+
return key_field, value_fields
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def make_entity(cls: type[DataclassInstance], key: str) -> type[DataclassInstance]:
|
|
34
|
+
"""
|
|
35
|
+
Transforms a regular data-class type into an entity type.
|
|
36
|
+
|
|
37
|
+
:param cls: The data-class type to transform.
|
|
38
|
+
:param key: The field to become the primary key.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
key_field, value_fields = key_value_fields(cls, key)
|
|
42
|
+
key_type = key_field.type
|
|
43
|
+
|
|
44
|
+
primary_key_type = PrimaryKey[key_type] # type: ignore
|
|
45
|
+
target_fields: list[tuple[str, Any, dataclasses.Field[Any]]] = [
|
|
46
|
+
(key_field.name, primary_key_type, dataclasses.field(default=key_field.default))
|
|
47
|
+
]
|
|
48
|
+
for value_field in value_fields:
|
|
49
|
+
target_fields.append(
|
|
50
|
+
(
|
|
51
|
+
value_field.name,
|
|
52
|
+
value_field.type,
|
|
53
|
+
dataclasses.field(default=value_field.default),
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
class_name = cls.__name__
|
|
58
|
+
class_module = cls.__module__
|
|
59
|
+
class_doc = cls.__doc__
|
|
60
|
+
|
|
61
|
+
if sys.version_info >= (3, 12):
|
|
62
|
+
class_type = dataclasses.make_dataclass(class_name, target_fields, module=class_module)
|
|
63
|
+
else:
|
|
64
|
+
class_type = dataclasses.make_dataclass(class_name, target_fields, namespace={"__module__": class_module})
|
|
65
|
+
|
|
66
|
+
class_type.__doc__ = class_doc
|
|
67
|
+
setattr(sys.modules[class_module], class_name, class_type)
|
|
68
|
+
return class_type
|