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
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