sql-dialect-master 1.1.1__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.
- backend/__init__.py +1 -0
- backend/api/__init__.py +1 -0
- backend/api/main.py +732 -0
- backend/api/middleware.py +274 -0
- backend/api/rate_limit_store.py +105 -0
- backend/api/readiness.py +178 -0
- backend/core/__init__.py +57 -0
- backend/core/cache.py +261 -0
- backend/core/column_hint_validation.py +22 -0
- backend/core/config.py +546 -0
- backend/core/constants.py +84 -0
- backend/core/exceptions.py +124 -0
- backend/core/function_call_scanner.py +64 -0
- backend/core/function_semantics.py +148 -0
- backend/core/functions_db.json +601 -0
- backend/core/functions_lookup.py +332 -0
- backend/core/metadata.py +71 -0
- backend/core/nl2sql.py +85 -0
- backend/core/nl2sql_components/__init__.py +72 -0
- backend/core/nl2sql_components/aggregations.py +35 -0
- backend/core/nl2sql_components/boolean_conditions.py +279 -0
- backend/core/nl2sql_components/core_extract.py +106 -0
- backend/core/nl2sql_components/evidence.py +122 -0
- backend/core/nl2sql_components/mappings.py +185 -0
- backend/core/nl2sql_components/relations.py +180 -0
- backend/core/nl2sql_components/templates.py +130 -0
- backend/core/nl2sql_components/tokenizer.py +187 -0
- backend/core/nl2sql_legacy.py +22 -0
- backend/core/nl2sql_pipeline/__init__.py +13 -0
- backend/core/nl2sql_pipeline/aggregations.py +26 -0
- backend/core/nl2sql_pipeline/clause_extract.py +97 -0
- backend/core/nl2sql_pipeline/dialect.py +139 -0
- backend/core/nl2sql_pipeline/intent.py +85 -0
- backend/core/nl2sql_pipeline/ir.py +70 -0
- backend/core/nl2sql_pipeline/models.py +30 -0
- backend/core/nl2sql_pipeline/orchestrator.py +576 -0
- backend/core/nl2sql_pipeline/predicates.py +182 -0
- backend/core/nl2sql_pipeline/renderer.py +248 -0
- backend/core/nl2sql_pipeline/resolution.py +69 -0
- backend/core/nl2sql_pipeline/suggestions.py +30 -0
- backend/core/nl2sql_pipeline/templates.py +371 -0
- backend/core/nl2sql_pipeline/validation.py +28 -0
- backend/core/p1_sql_scanner.py +196 -0
- backend/core/p2_data_validation.py +72 -0
- backend/core/parser.py +280 -0
- backend/core/post_processor.py +129 -0
- backend/core/rules.py +808 -0
- backend/core/semantic_diff.py +385 -0
- backend/core/transpiler.py +791 -0
- backend/core/type_mapping.json +593 -0
- backend/core/type_mapping.py +765 -0
- backend/utils/__init__.py +2 -0
- backend/utils/validators.py +407 -0
- frontend/__init__.py +10 -0
- frontend/app_context_v2.py +201 -0
- frontend/core/__init__.py +33 -0
- frontend/core/design_tokens.py +127 -0
- frontend/core/escaping.py +13 -0
- frontend/core/navigation.py +135 -0
- frontend/core/state.py +612 -0
- frontend/core/styles.py +523 -0
- frontend/core/themes.py +69 -0
- frontend/core/viewmodels.py +499 -0
- frontend/pages/__init__.py +0 -0
- frontend/pages/convert.py +283 -0
- frontend/pages/diff.py +157 -0
- frontend/pages/functions.py +100 -0
- frontend/pages/history.py +144 -0
- frontend/pages/lineage.py +168 -0
- frontend/pages/nl2sql.py +305 -0
- frontend/pages/query_analysis.py +120 -0
- frontend/pages/runtime.py +66 -0
- frontend/pages/settings.py +60 -0
- frontend/pages/templates.py +67 -0
- frontend/pages/types.py +110 -0
- frontend/templates_v2.py +53 -0
- frontend/ui/__init__.py +0 -0
- frontend/ui/command_palette.py +153 -0
- frontend/ui/diff.py +275 -0
- frontend/ui/editor.py +102 -0
- frontend/ui/status.py +255 -0
- sql_dialect_master-1.1.1.dist-info/METADATA +360 -0
- sql_dialect_master-1.1.1.dist-info/RECORD +86 -0
- sql_dialect_master-1.1.1.dist-info/WHEEL +5 -0
- sql_dialect_master-1.1.1.dist-info/licenses/LICENSE +21 -0
- sql_dialect_master-1.1.1.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Type Mapper - Map SQL data types between different database dialects.
|
|
3
|
+
|
|
4
|
+
Provides comprehensive data type mapping with:
|
|
5
|
+
- 36 data types across 7 categories
|
|
6
|
+
- 12 database dialects support
|
|
7
|
+
- Precision and compatibility warnings
|
|
8
|
+
- Type conversion suggestions
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Any, Dict, List
|
|
15
|
+
|
|
16
|
+
from .config import SUPPORTED_DIALECTS, TYPE_CATEGORIES, TypeMappingInfo
|
|
17
|
+
from .exceptions import ConfigurationError
|
|
18
|
+
from .p2_data_validation import _validate_type_mapping_entry
|
|
19
|
+
|
|
20
|
+
# Configure module logger
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TypeMapper:
|
|
25
|
+
"""SQL Type Mapper with 36 types × 12 dialects matrix support.
|
|
26
|
+
|
|
27
|
+
Features:
|
|
28
|
+
- 36 data types (STRING, VARCHAR, INT, DECIMAL, DATE, ARRAY, JSON, etc.)
|
|
29
|
+
- 12 database dialects
|
|
30
|
+
- Precision warnings for DECIMAL, TIMESTAMP, VARCHAR
|
|
31
|
+
- Type conversion suggestions
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
DIALECTS = SUPPORTED_DIALECTS
|
|
35
|
+
TYPE_CATEGORIES = TYPE_CATEGORIES
|
|
36
|
+
# Explicit aliases preserve deterministic resolution without fuzzy matching.
|
|
37
|
+
TYPE_ALIASES = {
|
|
38
|
+
"INTEGER": "INT",
|
|
39
|
+
"VARCHAR2": "VARCHAR",
|
|
40
|
+
"NVARCHAR2": "NVARCHAR",
|
|
41
|
+
"NVARCHAR": "VARCHAR",
|
|
42
|
+
"NCHAR": "CHAR",
|
|
43
|
+
}
|
|
44
|
+
# Regex to extract base type and optional precision from parameterized type strings.
|
|
45
|
+
# Matches: VARCHAR2(2000), VARCHAR(100), NVARCHAR(MAX), CHAR(), VARCHAR(MAX), etc.
|
|
46
|
+
PARAMETERIZED_TYPE_RE = re.compile(
|
|
47
|
+
r"^([A-Z_][A-Z0-9_]*)\s*(?:\(\s*(.*?)\s*\))?$", re.IGNORECASE
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def __init__(self, data_path: Optional[Path] = None):
|
|
51
|
+
"""Initialize mapper with type mapping data.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
data_path: Path to type_mapping.json. Uses default if None.
|
|
55
|
+
"""
|
|
56
|
+
if data_path is None:
|
|
57
|
+
data_path = Path(__file__).parent / "type_mapping.json"
|
|
58
|
+
self.data_path = data_path
|
|
59
|
+
self.data = self._load_data()
|
|
60
|
+
self.mappings: Dict[str, TypeMappingInfo] = self.data.get("mappings", {})
|
|
61
|
+
self.precision_warnings: Dict[str, str] = self.data.get("precision_warnings", {})
|
|
62
|
+
|
|
63
|
+
def _load_data(self) -> dict:
|
|
64
|
+
"""Load and validate type mapping data from JSON file."""
|
|
65
|
+
try:
|
|
66
|
+
with open(self.data_path, 'r', encoding='utf-8') as f:
|
|
67
|
+
data = json.load(f)
|
|
68
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
69
|
+
raise ConfigurationError(
|
|
70
|
+
f"Unable to load type mapping data from {self.data_path}: {exc}",
|
|
71
|
+
details={"data_path": str(self.data_path)},
|
|
72
|
+
) from exc
|
|
73
|
+
|
|
74
|
+
if not isinstance(data, dict):
|
|
75
|
+
raise ConfigurationError(
|
|
76
|
+
f"Invalid type mapping data in {self.data_path}: top-level JSON value must be an object",
|
|
77
|
+
details={"data_path": str(self.data_path)},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
mappings = data.get("mappings", {})
|
|
81
|
+
if not isinstance(mappings, dict):
|
|
82
|
+
raise ConfigurationError(
|
|
83
|
+
f"Invalid type mapping data in {self.data_path}: 'mappings' must be an object",
|
|
84
|
+
details={"data_path": str(self.data_path)},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
precision_warnings = data.get("precision_warnings", {})
|
|
88
|
+
if not isinstance(precision_warnings, dict):
|
|
89
|
+
raise ConfigurationError(
|
|
90
|
+
f"Invalid type mapping data in {self.data_path}: 'precision_warnings' must be an object",
|
|
91
|
+
details={"data_path": str(self.data_path)},
|
|
92
|
+
)
|
|
93
|
+
for name, entry in mappings.items():
|
|
94
|
+
_validate_type_mapping_entry(self.data_path, name, entry)
|
|
95
|
+
for name, warning in precision_warnings.items():
|
|
96
|
+
if not isinstance(name, str) or not name.strip() or not isinstance(warning, str):
|
|
97
|
+
raise ConfigurationError(
|
|
98
|
+
f"Invalid runtime data in {self.data_path}: precision_warnings must map non-empty strings to strings",
|
|
99
|
+
details={"data_path": str(self.data_path)},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
def _parse_parameterized_type(self, type_name: str) -> tuple:
|
|
105
|
+
"""Parse a potentially parameterized type string into (base_type, precision).
|
|
106
|
+
|
|
107
|
+
Handles cases like 'VARCHAR2(2000)', 'NVARCHAR(MAX)', 'CHAR()', 'VARCHAR'.
|
|
108
|
+
Uses a minimal regex parser (not sqlglot) to avoid adding a new dependency;
|
|
109
|
+
the regex is simple enough to be reliable for SQL type declarations.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
type_name: Raw type string, e.g. 'VARCHAR2(2000)' or 'NVARCHAR(MAX)'
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Tuple of (canonical_base_type, precision_value_or_None)
|
|
116
|
+
- canonical_base_type: the resolved type name in mappings (e.g. 'VARCHAR')
|
|
117
|
+
- precision_value: the numeric or string precision (e.g. 2000, 'MAX'), or None
|
|
118
|
+
"""
|
|
119
|
+
# First try: is it an exact canonical match (no parens)?
|
|
120
|
+
upper = type_name.upper().strip()
|
|
121
|
+
if upper in self.mappings:
|
|
122
|
+
return (upper, None)
|
|
123
|
+
|
|
124
|
+
# Try alias match
|
|
125
|
+
alias = self.TYPE_ALIASES.get(upper)
|
|
126
|
+
if alias and alias in self.mappings:
|
|
127
|
+
return (alias, None)
|
|
128
|
+
|
|
129
|
+
# Try parameterized match via regex
|
|
130
|
+
m = self.PARAMETERIZED_TYPE_RE.match(upper)
|
|
131
|
+
if not m:
|
|
132
|
+
return (upper, None) # Cannot parse — treat as unknown
|
|
133
|
+
|
|
134
|
+
base_candidate = m.group(1)
|
|
135
|
+
precision_str = m.group(2) # None if no parens, '' if empty parens
|
|
136
|
+
|
|
137
|
+
# Resolve base candidate through aliases
|
|
138
|
+
canonical_key = base_candidate # default to the regex-extracted name
|
|
139
|
+
if base_candidate in self.mappings:
|
|
140
|
+
canonical_key = base_candidate
|
|
141
|
+
elif base_candidate in self.TYPE_ALIASES:
|
|
142
|
+
canonical_key = self.TYPE_ALIASES[base_candidate]
|
|
143
|
+
if canonical_key not in self.mappings:
|
|
144
|
+
return (base_candidate, None) # Unknown base type
|
|
145
|
+
|
|
146
|
+
# Parse precision
|
|
147
|
+
precision = None
|
|
148
|
+
if precision_str is not None:
|
|
149
|
+
# Empty parens: VARCHAR2() → no precision (treat as unbounded)
|
|
150
|
+
if precision_str.strip() == "":
|
|
151
|
+
precision = None
|
|
152
|
+
elif precision_str.strip().upper() == "MAX":
|
|
153
|
+
precision = "MAX"
|
|
154
|
+
else:
|
|
155
|
+
# Try to parse as integer
|
|
156
|
+
try:
|
|
157
|
+
precision = int(precision_str.strip())
|
|
158
|
+
except ValueError:
|
|
159
|
+
# Non-numeric, non-MAX value — treat as literal string
|
|
160
|
+
precision = precision_str.strip()
|
|
161
|
+
|
|
162
|
+
return (canonical_key, precision)
|
|
163
|
+
|
|
164
|
+
def map_type(self, type_name: str, source: str, target: str) -> Dict[str, Any]:
|
|
165
|
+
"""Map a type from source dialect to target dialect.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
type_name: Data type name (may be parameterized, e.g. 'VARCHAR2(2000)')
|
|
169
|
+
source: Source dialect
|
|
170
|
+
target: Target dialect
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dictionary with mapping result
|
|
174
|
+
"""
|
|
175
|
+
source = source.lower()
|
|
176
|
+
target = target.lower()
|
|
177
|
+
|
|
178
|
+
invalid_dialects = [
|
|
179
|
+
dialect for dialect in (source, target) if dialect not in SUPPORTED_DIALECTS
|
|
180
|
+
]
|
|
181
|
+
if invalid_dialects:
|
|
182
|
+
return {
|
|
183
|
+
"success": False,
|
|
184
|
+
"error": (
|
|
185
|
+
f"Unsupported dialect(s): {', '.join(invalid_dialects)}. "
|
|
186
|
+
f"Supported: {SUPPORTED_DIALECTS}"
|
|
187
|
+
),
|
|
188
|
+
"source_type": type_name.upper(),
|
|
189
|
+
"target_type": None,
|
|
190
|
+
"source_dialect": source,
|
|
191
|
+
"target_dialect": target,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Parse parameterized type: extract canonical base + precision
|
|
195
|
+
canonical_type_name, precision = self._parse_parameterized_type(type_name)
|
|
196
|
+
|
|
197
|
+
type_info = self.mappings.get(canonical_type_name)
|
|
198
|
+
if not type_info:
|
|
199
|
+
return {
|
|
200
|
+
"success": False,
|
|
201
|
+
"error": f"Type {type_name} not found in mappings",
|
|
202
|
+
"source_type": type_name.upper(),
|
|
203
|
+
"target_type": None,
|
|
204
|
+
"source_dialect": source,
|
|
205
|
+
"target_dialect": target
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
source_type = type_info.get(source, "N/A")
|
|
209
|
+
target_type = type_info.get(target, "N/A")
|
|
210
|
+
notes = type_info.get("notes", "")
|
|
211
|
+
|
|
212
|
+
# Check for precision warnings
|
|
213
|
+
warnings = []
|
|
214
|
+
if canonical_type_name in self.precision_warnings:
|
|
215
|
+
warnings.append(self.precision_warnings[canonical_type_name])
|
|
216
|
+
|
|
217
|
+
# Add precision-aware warnings for parameterized types
|
|
218
|
+
warnings.extend(self._get_precision_aware_warnings(canonical_type_name, precision, source, target))
|
|
219
|
+
warnings.extend(self._get_type_specific_warnings(canonical_type_name, source, target))
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"success": True,
|
|
223
|
+
"type_name": type_name.upper(),
|
|
224
|
+
"source_dialect": source,
|
|
225
|
+
"target_dialect": target,
|
|
226
|
+
"source_type": source_type,
|
|
227
|
+
"target_type": target_type,
|
|
228
|
+
"notes": notes,
|
|
229
|
+
"warnings": warnings,
|
|
230
|
+
"category": self.get_type_category(canonical_type_name),
|
|
231
|
+
"precision": precision,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def _get_type_specific_warnings(
|
|
235
|
+
self,
|
|
236
|
+
type_name: str,
|
|
237
|
+
source: str,
|
|
238
|
+
target: str
|
|
239
|
+
) -> List[str]:
|
|
240
|
+
"""Get type-specific warnings for a conversion.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
type_name: Data type name
|
|
244
|
+
source: Source dialect
|
|
245
|
+
target: Target dialect
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of warning messages
|
|
249
|
+
"""
|
|
250
|
+
warnings = []
|
|
251
|
+
|
|
252
|
+
# Array/Map/Struct warnings
|
|
253
|
+
if type_name in ["ARRAY", "MAP", "STRUCT"]:
|
|
254
|
+
if target in ["mysql", "oracle", "tsql"]:
|
|
255
|
+
warnings.append(
|
|
256
|
+
f"{type_name} is stored as JSON in {target} - "
|
|
257
|
+
"native array operations not available"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# UUID warnings
|
|
261
|
+
if type_name == "UUID":
|
|
262
|
+
if target not in ["postgres", "tsql", "clickhouse", "duckdb"]:
|
|
263
|
+
warnings.append(
|
|
264
|
+
f"UUID stored as CHAR(36) in {target} - "
|
|
265
|
+
"consider indexing implications"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# JSON warnings
|
|
269
|
+
if type_name in ["JSON", "JSONB"]:
|
|
270
|
+
if target == "snowflake":
|
|
271
|
+
warnings.append("Snowflake uses VARIANT type for JSON data")
|
|
272
|
+
if target in ["hive", "spark"]:
|
|
273
|
+
warnings.append("JSON stored as STRING - use JSON functions for access")
|
|
274
|
+
|
|
275
|
+
# Timestamp precision
|
|
276
|
+
if type_name in ["TIMESTAMP", "TIMESTAMP_TZ"]:
|
|
277
|
+
if source == "oracle" and target in ["mysql", "postgres"]:
|
|
278
|
+
warnings.append(
|
|
279
|
+
"Oracle TIMESTAMP has 9 fractional digits, "
|
|
280
|
+
f"{target} has 6 - potential precision loss"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Boolean
|
|
284
|
+
if type_name == "BOOLEAN":
|
|
285
|
+
if target in ["mysql", "oracle"]:
|
|
286
|
+
warnings.append(
|
|
287
|
+
f"BOOLEAN stored as numeric in {target} - "
|
|
288
|
+
"use 0/1 for false/true"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return warnings
|
|
292
|
+
|
|
293
|
+
def _get_precision_aware_warnings(
|
|
294
|
+
self,
|
|
295
|
+
type_name: str,
|
|
296
|
+
precision,
|
|
297
|
+
source: str,
|
|
298
|
+
target: str
|
|
299
|
+
) -> List[str]:
|
|
300
|
+
"""Get precision-aware warnings for parameterized types.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
type_name: Canonical type name (e.g. 'VARCHAR', 'NVARCHAR')
|
|
304
|
+
precision: Numeric precision value or 'MAX' or None
|
|
305
|
+
source: Source dialect
|
|
306
|
+
target: Target dialect
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
List of precision-specific warning messages
|
|
310
|
+
"""
|
|
311
|
+
warnings = []
|
|
312
|
+
|
|
313
|
+
# VARCHAR/VARCHAR2/NVARCHAR/NVARCHAR2 precision warnings.
|
|
314
|
+
#
|
|
315
|
+
# 'NVARCHAR' is itself a canonical mappings key (NVARCHAR2 aliases
|
|
316
|
+
# to it), so guarding this branch on type_name == "VARCHAR" alone
|
|
317
|
+
# skipped it for every NVARCHAR input — and the nested
|
|
318
|
+
# `type_name == "NVARCHAR"` T-SQL rule below could never be true.
|
|
319
|
+
# Widening the branch to both canonical character types makes the
|
|
320
|
+
# T-SQL NVARCHAR rule reachable again. Rules whose messages are
|
|
321
|
+
# VARCHAR-specific (VARCHAR2/VARCHAR text, MAX notes) keep an
|
|
322
|
+
# explicit `type_name == "VARCHAR"` guard so their behavior is
|
|
323
|
+
# unchanged for every input.
|
|
324
|
+
if type_name in ("VARCHAR", "NVARCHAR"):
|
|
325
|
+
oracle_max = 4000
|
|
326
|
+
oracle_extended = 32767
|
|
327
|
+
tsql_varchar_max = 8000
|
|
328
|
+
tsql_nvarchar_max = 4000
|
|
329
|
+
|
|
330
|
+
if precision is not None:
|
|
331
|
+
if isinstance(precision, int):
|
|
332
|
+
# Oracle source: warn if precision exceeds Oracle's hard limit
|
|
333
|
+
if (
|
|
334
|
+
source == "oracle"
|
|
335
|
+
and type_name == "VARCHAR"
|
|
336
|
+
and precision > oracle_max
|
|
337
|
+
):
|
|
338
|
+
if precision > oracle_extended:
|
|
339
|
+
warnings.append(
|
|
340
|
+
f"VARCHAR2({precision}) exceeds Oracle's extended "
|
|
341
|
+
f"MAX_STRING_SIZE limit of {oracle_extended}; "
|
|
342
|
+
f"will be truncated or require CLOB"
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
warnings.append(
|
|
346
|
+
f"VARCHAR2({precision}) exceeds Oracle's default "
|
|
347
|
+
f"VARCHAR2 limit of {oracle_max}; ensure "
|
|
348
|
+
f"MAX_STRING_SIZE=EXTENDED"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# TSQL source NVARCHAR: warn if exceeds limit. Reachable
|
|
352
|
+
# now that the outer branch covers canonical NVARCHAR.
|
|
353
|
+
if source == "tsql" and type_name == "NVARCHAR":
|
|
354
|
+
if precision > tsql_nvarchar_max:
|
|
355
|
+
warnings.append(
|
|
356
|
+
f"NVARCHAR({precision}) exceeds TSQL NVARCHAR "
|
|
357
|
+
f"limit of {tsql_nvarchar_max}; consider "
|
|
358
|
+
f"NVARCHAR(MAX) or different type"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Target-specific warnings
|
|
362
|
+
if target == "oracle" and type_name == "VARCHAR":
|
|
363
|
+
if precision > oracle_max:
|
|
364
|
+
if precision > oracle_extended:
|
|
365
|
+
warnings.append(
|
|
366
|
+
f"Target Oracle cannot store VARCHAR2({precision}); "
|
|
367
|
+
f"use CLOB for lengths > {oracle_extended}"
|
|
368
|
+
)
|
|
369
|
+
elif target == "tsql" and type_name == "VARCHAR":
|
|
370
|
+
if precision > tsql_varchar_max:
|
|
371
|
+
warnings.append(
|
|
372
|
+
f"Target TSQL VARCHAR({precision}) exceeds "
|
|
373
|
+
f"limit of {tsql_varchar_max}; use VARCHAR(MAX)"
|
|
374
|
+
)
|
|
375
|
+
elif target == "mysql" and type_name == "VARCHAR":
|
|
376
|
+
# MySQL's 65535 is a row-size soft limit, not per-column
|
|
377
|
+
if precision > 21845:
|
|
378
|
+
warnings.append(
|
|
379
|
+
f"VARCHAR({precision}) may exceed MySQL row size "
|
|
380
|
+
f"limit when combined with other columns; "
|
|
381
|
+
f"consider TEXT"
|
|
382
|
+
)
|
|
383
|
+
elif precision == "MAX" and type_name == "VARCHAR":
|
|
384
|
+
# Unbounded — suggest checking if CLOB/TEXT is more appropriate
|
|
385
|
+
if target == "oracle":
|
|
386
|
+
warnings.append(
|
|
387
|
+
"VARCHAR2(MAX) in TSQL maps to unbounded length; "
|
|
388
|
+
"Oracle uses CLOB for large text data"
|
|
389
|
+
)
|
|
390
|
+
elif target == "mysql":
|
|
391
|
+
warnings.append(
|
|
392
|
+
"VARCHAR(MAX) may exceed MySQL row size limits; "
|
|
393
|
+
"consider MEDIUMTEXT or LONGTEXT"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return warnings
|
|
397
|
+
|
|
398
|
+
def get_matrix(
|
|
399
|
+
self,
|
|
400
|
+
source: str = None,
|
|
401
|
+
target: str = None
|
|
402
|
+
) -> Dict[str, Any]:
|
|
403
|
+
"""Get full or filtered type mapping matrix.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
source: Filter by source dialect
|
|
407
|
+
target: Filter by target dialect
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Type mapping matrix
|
|
411
|
+
"""
|
|
412
|
+
if source is None and target is None:
|
|
413
|
+
return self.mappings
|
|
414
|
+
|
|
415
|
+
result = {}
|
|
416
|
+
for type_name, type_map in self.mappings.items():
|
|
417
|
+
entry = {"type": type_name}
|
|
418
|
+
if source:
|
|
419
|
+
entry["source"] = type_map.get(source.lower(), "N/A")
|
|
420
|
+
if target:
|
|
421
|
+
entry["target"] = type_map.get(target.lower(), "N/A")
|
|
422
|
+
if "notes" in type_map:
|
|
423
|
+
entry["notes"] = type_map["notes"]
|
|
424
|
+
entry["category"] = self.get_type_category(type_name)
|
|
425
|
+
result[type_name] = entry
|
|
426
|
+
|
|
427
|
+
return result
|
|
428
|
+
|
|
429
|
+
def get_all_types(self) -> List[str]:
|
|
430
|
+
"""Get list of all supported type names.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
List of type names
|
|
434
|
+
"""
|
|
435
|
+
return list(self.mappings.keys())
|
|
436
|
+
|
|
437
|
+
def get_dialect_types(self, dialect: str) -> Dict[str, str]:
|
|
438
|
+
"""Get all type mappings for a specific dialect.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
dialect: Dialect name
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
Dictionary of type_name -> dialect_syntax
|
|
445
|
+
"""
|
|
446
|
+
dialect = dialect.lower()
|
|
447
|
+
result = {}
|
|
448
|
+
for type_name, type_map in self.mappings.items():
|
|
449
|
+
if dialect in type_map:
|
|
450
|
+
result[type_name] = type_map[dialect]
|
|
451
|
+
return result
|
|
452
|
+
|
|
453
|
+
def compare_types(self, type_name: str) -> Dict[str, Any]:
|
|
454
|
+
"""Compare a type across all dialects.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
type_name: Type name to compare
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Comparison dictionary
|
|
461
|
+
"""
|
|
462
|
+
type_info = self.mappings.get(type_name.upper())
|
|
463
|
+
if not type_info:
|
|
464
|
+
return {"error": f"Type {type_name} not found"}
|
|
465
|
+
|
|
466
|
+
comparison = {
|
|
467
|
+
"type": type_name,
|
|
468
|
+
"category": self.get_type_category(type_name),
|
|
469
|
+
"dialects": {},
|
|
470
|
+
"notes": type_info.get("notes", "")
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
for dialect in self.DIALECTS:
|
|
474
|
+
comparison["dialects"][dialect] = type_info.get(dialect, "N/A")
|
|
475
|
+
|
|
476
|
+
# Add availability summary
|
|
477
|
+
available = [d for d in self.DIALECTS if comparison["dialects"].get(d, "N/A") != "N/A"]
|
|
478
|
+
comparison["availability"] = {
|
|
479
|
+
"supported": available,
|
|
480
|
+
"coverage": f"{len(available)}/{len(self.DIALECTS)}"
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return comparison
|
|
484
|
+
|
|
485
|
+
def get_precision_warnings(self) -> Dict[str, str]:
|
|
486
|
+
"""Get all precision and compatibility warnings.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Dictionary of type_name -> warning_message
|
|
490
|
+
"""
|
|
491
|
+
return self.precision_warnings
|
|
492
|
+
|
|
493
|
+
def suggest_type(
|
|
494
|
+
self,
|
|
495
|
+
source_type: str,
|
|
496
|
+
source: str,
|
|
497
|
+
target: str
|
|
498
|
+
) -> Dict[str, Any]:
|
|
499
|
+
"""Suggest best target type with notes and recommendations.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
source_type: Source data type
|
|
503
|
+
source: Source dialect
|
|
504
|
+
target: Target dialect
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Dictionary with suggestion and recommendations
|
|
508
|
+
"""
|
|
509
|
+
result = self.map_type(source_type, source, target)
|
|
510
|
+
|
|
511
|
+
# Add suggestions based on common patterns
|
|
512
|
+
suggestions = []
|
|
513
|
+
source_upper = source_type.upper()
|
|
514
|
+
target_lower = target.lower()
|
|
515
|
+
|
|
516
|
+
if "VARCHAR" in source_upper and target_lower == "oracle":
|
|
517
|
+
suggestions.append(
|
|
518
|
+
"Consider VARCHAR2 with MAX_STRING_SIZE=EXTENDED for >4000 chars"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if "DECIMAL" in source_upper:
|
|
522
|
+
suggestions.append(
|
|
523
|
+
"Verify precision (p) and scale (s) are within target limits"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
if "TIMESTAMP" in source_upper:
|
|
527
|
+
suggestions.append(
|
|
528
|
+
"Check timezone handling: WITH TIME ZONE vs WITHOUT"
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
if "ARRAY" in source_upper and target_lower in ["mysql", "oracle", "tsql"]:
|
|
532
|
+
suggestions.append("Consider JSON serialization for array data")
|
|
533
|
+
|
|
534
|
+
if "JSON" in source_upper and target_lower == "snowflake":
|
|
535
|
+
suggestions.append("Snowflake uses VARIANT type for JSON data")
|
|
536
|
+
|
|
537
|
+
if "UUID" in source_upper and target_lower not in ["postgres", "tsql", "clickhouse", "duckdb"]:
|
|
538
|
+
suggestions.append(
|
|
539
|
+
"UUID stored as CHAR(36) or RAW(16) - consider indexing implications"
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
if "TEXT" in source_upper and target_lower == "oracle":
|
|
543
|
+
suggestions.append("Oracle uses CLOB for unlimited text - consider VARCHAR2 for smaller data")
|
|
544
|
+
|
|
545
|
+
if "SERIAL" in source_upper:
|
|
546
|
+
suggestions.append("Auto-increment syntax varies significantly - verify DDL")
|
|
547
|
+
|
|
548
|
+
result["suggestions"] = suggestions
|
|
549
|
+
return result
|
|
550
|
+
|
|
551
|
+
def get_type_category(self, type_name: str) -> Optional[str]:
|
|
552
|
+
"""Get the category for a type.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
type_name: Type name
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
Category name or None
|
|
559
|
+
"""
|
|
560
|
+
type_upper = type_name.upper()
|
|
561
|
+
for category, types in self.TYPE_CATEGORIES.items():
|
|
562
|
+
if type_upper in types:
|
|
563
|
+
return category
|
|
564
|
+
return None
|
|
565
|
+
|
|
566
|
+
def get_types_by_category(self, category: str) -> List[str]:
|
|
567
|
+
"""Get all types in a category.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
category: Category name
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
List of types in a category
|
|
574
|
+
"""
|
|
575
|
+
return self.TYPE_CATEGORIES.get(category, [])
|
|
576
|
+
|
|
577
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
578
|
+
"""Get comprehensive type mapping statistics.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
Dictionary with statistics
|
|
582
|
+
"""
|
|
583
|
+
# Count types by dialect availability
|
|
584
|
+
dialect_coverage = {d: 0 for d in self.DIALECTS}
|
|
585
|
+
for type_map in self.mappings.values():
|
|
586
|
+
for dialect in self.DIALECTS:
|
|
587
|
+
if type_map.get(dialect) and type_map.get(dialect) != "N/A":
|
|
588
|
+
dialect_coverage[dialect] += 1
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
"total_types": len(self.mappings),
|
|
592
|
+
"total_dialects": len(self.DIALECTS),
|
|
593
|
+
"categories": len(self.TYPE_CATEGORIES),
|
|
594
|
+
"by_category": {cat: len(types) for cat, types in self.TYPE_CATEGORIES.items()},
|
|
595
|
+
"precision_warnings": len(self.precision_warnings),
|
|
596
|
+
"dialect_coverage": dialect_coverage
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
def find_compatible_types(
|
|
600
|
+
self,
|
|
601
|
+
type_name: str,
|
|
602
|
+
target: str
|
|
603
|
+
) -> List[Dict[str, Any]]:
|
|
604
|
+
"""Find compatible alternative types in target dialect.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
type_name: Source type name
|
|
608
|
+
target: Target dialect
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
List of alternative types
|
|
612
|
+
"""
|
|
613
|
+
type_upper = type_name.upper()
|
|
614
|
+
target_lower = target.lower()
|
|
615
|
+
|
|
616
|
+
# Get the category of the source type
|
|
617
|
+
category = self.get_type_category(type_upper)
|
|
618
|
+
if not category:
|
|
619
|
+
return []
|
|
620
|
+
|
|
621
|
+
# Find all types in the same category
|
|
622
|
+
alternatives = []
|
|
623
|
+
for alt_type in self.TYPE_CATEGORIES.get(category, []):
|
|
624
|
+
if alt_type != type_upper and alt_type in self.mappings:
|
|
625
|
+
target_syntax = self.mappings[alt_type].get(target_lower)
|
|
626
|
+
if target_syntax and target_syntax != "N/A":
|
|
627
|
+
alternatives.append({
|
|
628
|
+
"type": alt_type,
|
|
629
|
+
"syntax": target_syntax,
|
|
630
|
+
"notes": self.mappings[alt_type].get("notes", ""),
|
|
631
|
+
"category": category
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
return alternatives
|
|
635
|
+
|
|
636
|
+
def get_conversion_path(
|
|
637
|
+
self,
|
|
638
|
+
source_type: str,
|
|
639
|
+
source: str,
|
|
640
|
+
target: str
|
|
641
|
+
) -> Dict[str, Any]:
|
|
642
|
+
"""Get detailed conversion path with all considerations.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
source_type: Source type name
|
|
646
|
+
source: Source dialect
|
|
647
|
+
target: Target dialect
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
Detailed conversion information
|
|
651
|
+
"""
|
|
652
|
+
mapping = self.map_type(source_type, source, target)
|
|
653
|
+
suggestion = self.suggest_type(source_type, source, target)
|
|
654
|
+
alternatives = self.find_compatible_types(source_type, target)
|
|
655
|
+
|
|
656
|
+
return {
|
|
657
|
+
"mapping": mapping,
|
|
658
|
+
"suggestions": suggestion.get("suggestions", []),
|
|
659
|
+
"alternatives": alternatives,
|
|
660
|
+
"precision_warning": self.precision_warnings.get(source_type.upper()),
|
|
661
|
+
"category": self.get_type_category(source_type)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
def find_source_type(
|
|
665
|
+
self,
|
|
666
|
+
target_syntax: str,
|
|
667
|
+
target_dialect: str
|
|
668
|
+
) -> List[Dict[str, Any]]:
|
|
669
|
+
"""Reverse lookup: find source types that map to a target syntax.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
target_syntax: Target type syntax to search for
|
|
673
|
+
target_dialect: Target dialect
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
List of matching source types with details
|
|
677
|
+
"""
|
|
678
|
+
target_dialect = target_dialect.lower()
|
|
679
|
+
target_syntax_upper = target_syntax.upper()
|
|
680
|
+
matches = []
|
|
681
|
+
|
|
682
|
+
for type_name, mapping in self.mappings.items():
|
|
683
|
+
dialect_syntax = mapping.get(target_dialect, "")
|
|
684
|
+
if dialect_syntax and dialect_syntax.upper().strip() == target_syntax_upper:
|
|
685
|
+
matches.append({
|
|
686
|
+
"type_name": type_name,
|
|
687
|
+
"target_syntax": dialect_syntax,
|
|
688
|
+
"category": self.get_type_category(type_name),
|
|
689
|
+
"notes": mapping.get("notes", ""),
|
|
690
|
+
"exact_match": dialect_syntax.upper() == target_syntax_upper
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
# Sort by exact match first
|
|
694
|
+
matches.sort(key=lambda x: (not x["exact_match"], x["type_name"]))
|
|
695
|
+
return matches
|
|
696
|
+
|
|
697
|
+
def get_type_compatibility_matrix(
|
|
698
|
+
self,
|
|
699
|
+
type_name: str
|
|
700
|
+
) -> Dict[str, Dict[str, bool]]:
|
|
701
|
+
"""Get compatibility matrix showing which dialects support a type natively.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
type_name: Type name to check
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
Matrix of dialect -> dialect compatibility
|
|
708
|
+
"""
|
|
709
|
+
type_info = self.mappings.get(type_name.upper())
|
|
710
|
+
if not type_info:
|
|
711
|
+
return {"error": f"Type {type_name} not found"}
|
|
712
|
+
|
|
713
|
+
matrix = {}
|
|
714
|
+
for source in self.DIALECTS:
|
|
715
|
+
matrix[source] = {}
|
|
716
|
+
source_syntax = type_info.get(source, "N/A")
|
|
717
|
+
|
|
718
|
+
for target in self.DIALECTS:
|
|
719
|
+
target_syntax = type_info.get(target, "N/A")
|
|
720
|
+
|
|
721
|
+
# Check if conversion is lossless
|
|
722
|
+
if source_syntax == "N/A" or target_syntax == "N/A":
|
|
723
|
+
matrix[source][target] = None # Not applicable
|
|
724
|
+
elif source_syntax == target_syntax:
|
|
725
|
+
matrix[source][target] = True # Identical
|
|
726
|
+
else:
|
|
727
|
+
# Check for potential data loss
|
|
728
|
+
matrix[source][target] = self._is_compatible_conversion(
|
|
729
|
+
type_name, source_syntax, target_syntax
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
return matrix
|
|
733
|
+
|
|
734
|
+
def _is_compatible_conversion(
|
|
735
|
+
self,
|
|
736
|
+
type_name: str,
|
|
737
|
+
source_syntax: str,
|
|
738
|
+
target_syntax: str
|
|
739
|
+
) -> bool:
|
|
740
|
+
"""Check if type conversion is compatible (no data loss).
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
type_name: Type name
|
|
744
|
+
source_syntax: Source dialect syntax
|
|
745
|
+
target_syntax: Target dialect syntax
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
True if conversion is safe
|
|
749
|
+
"""
|
|
750
|
+
# Simple heuristic: check for common compatibility patterns
|
|
751
|
+
type_upper = type_name.upper()
|
|
752
|
+
|
|
753
|
+
# String types are generally compatible
|
|
754
|
+
if type_upper in ["STRING", "VARCHAR", "TEXT"]:
|
|
755
|
+
return True
|
|
756
|
+
|
|
757
|
+
# Numeric types need precision check
|
|
758
|
+
if type_upper in ["DECIMAL", "NUMERIC"]:
|
|
759
|
+
return False # Precision may vary
|
|
760
|
+
|
|
761
|
+
# Timestamp precision varies
|
|
762
|
+
if "TIMESTAMP" in type_upper:
|
|
763
|
+
return False # Precision may vary
|
|
764
|
+
|
|
765
|
+
return True
|