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.
Files changed (86) hide show
  1. backend/__init__.py +1 -0
  2. backend/api/__init__.py +1 -0
  3. backend/api/main.py +732 -0
  4. backend/api/middleware.py +274 -0
  5. backend/api/rate_limit_store.py +105 -0
  6. backend/api/readiness.py +178 -0
  7. backend/core/__init__.py +57 -0
  8. backend/core/cache.py +261 -0
  9. backend/core/column_hint_validation.py +22 -0
  10. backend/core/config.py +546 -0
  11. backend/core/constants.py +84 -0
  12. backend/core/exceptions.py +124 -0
  13. backend/core/function_call_scanner.py +64 -0
  14. backend/core/function_semantics.py +148 -0
  15. backend/core/functions_db.json +601 -0
  16. backend/core/functions_lookup.py +332 -0
  17. backend/core/metadata.py +71 -0
  18. backend/core/nl2sql.py +85 -0
  19. backend/core/nl2sql_components/__init__.py +72 -0
  20. backend/core/nl2sql_components/aggregations.py +35 -0
  21. backend/core/nl2sql_components/boolean_conditions.py +279 -0
  22. backend/core/nl2sql_components/core_extract.py +106 -0
  23. backend/core/nl2sql_components/evidence.py +122 -0
  24. backend/core/nl2sql_components/mappings.py +185 -0
  25. backend/core/nl2sql_components/relations.py +180 -0
  26. backend/core/nl2sql_components/templates.py +130 -0
  27. backend/core/nl2sql_components/tokenizer.py +187 -0
  28. backend/core/nl2sql_legacy.py +22 -0
  29. backend/core/nl2sql_pipeline/__init__.py +13 -0
  30. backend/core/nl2sql_pipeline/aggregations.py +26 -0
  31. backend/core/nl2sql_pipeline/clause_extract.py +97 -0
  32. backend/core/nl2sql_pipeline/dialect.py +139 -0
  33. backend/core/nl2sql_pipeline/intent.py +85 -0
  34. backend/core/nl2sql_pipeline/ir.py +70 -0
  35. backend/core/nl2sql_pipeline/models.py +30 -0
  36. backend/core/nl2sql_pipeline/orchestrator.py +576 -0
  37. backend/core/nl2sql_pipeline/predicates.py +182 -0
  38. backend/core/nl2sql_pipeline/renderer.py +248 -0
  39. backend/core/nl2sql_pipeline/resolution.py +69 -0
  40. backend/core/nl2sql_pipeline/suggestions.py +30 -0
  41. backend/core/nl2sql_pipeline/templates.py +371 -0
  42. backend/core/nl2sql_pipeline/validation.py +28 -0
  43. backend/core/p1_sql_scanner.py +196 -0
  44. backend/core/p2_data_validation.py +72 -0
  45. backend/core/parser.py +280 -0
  46. backend/core/post_processor.py +129 -0
  47. backend/core/rules.py +808 -0
  48. backend/core/semantic_diff.py +385 -0
  49. backend/core/transpiler.py +791 -0
  50. backend/core/type_mapping.json +593 -0
  51. backend/core/type_mapping.py +765 -0
  52. backend/utils/__init__.py +2 -0
  53. backend/utils/validators.py +407 -0
  54. frontend/__init__.py +10 -0
  55. frontend/app_context_v2.py +201 -0
  56. frontend/core/__init__.py +33 -0
  57. frontend/core/design_tokens.py +127 -0
  58. frontend/core/escaping.py +13 -0
  59. frontend/core/navigation.py +135 -0
  60. frontend/core/state.py +612 -0
  61. frontend/core/styles.py +523 -0
  62. frontend/core/themes.py +69 -0
  63. frontend/core/viewmodels.py +499 -0
  64. frontend/pages/__init__.py +0 -0
  65. frontend/pages/convert.py +283 -0
  66. frontend/pages/diff.py +157 -0
  67. frontend/pages/functions.py +100 -0
  68. frontend/pages/history.py +144 -0
  69. frontend/pages/lineage.py +168 -0
  70. frontend/pages/nl2sql.py +305 -0
  71. frontend/pages/query_analysis.py +120 -0
  72. frontend/pages/runtime.py +66 -0
  73. frontend/pages/settings.py +60 -0
  74. frontend/pages/templates.py +67 -0
  75. frontend/pages/types.py +110 -0
  76. frontend/templates_v2.py +53 -0
  77. frontend/ui/__init__.py +0 -0
  78. frontend/ui/command_palette.py +153 -0
  79. frontend/ui/diff.py +275 -0
  80. frontend/ui/editor.py +102 -0
  81. frontend/ui/status.py +255 -0
  82. sql_dialect_master-1.1.1.dist-info/METADATA +360 -0
  83. sql_dialect_master-1.1.1.dist-info/RECORD +86 -0
  84. sql_dialect_master-1.1.1.dist-info/WHEEL +5 -0
  85. sql_dialect_master-1.1.1.dist-info/licenses/LICENSE +21 -0
  86. 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