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
backend/core/cache.py ADDED
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env python3
2
+ """Unified caching module for SQL Dialect Master.
3
+
4
+ Provides thread-safe caching with TTL support.
5
+ """
6
+ import hashlib
7
+ import logging
8
+ import threading
9
+ import time
10
+ from collections import OrderedDict
11
+ from dataclasses import dataclass
12
+ from typing import Any, Dict, Optional, Callable
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class CacheEntry:
19
+ """A single cache entry with metadata."""
20
+ value: Any
21
+ created_at: float
22
+ ttl: int
23
+ hits: int = 0
24
+
25
+ def is_expired(self) -> bool:
26
+ """Check if entry has expired."""
27
+ if self.ttl <= 0:
28
+ return False
29
+ return time.time() - self.created_at > self.ttl
30
+
31
+
32
+ class TTLCache:
33
+ """Thread-safe LRU cache with TTL support.
34
+
35
+ Features:
36
+ - Time-to-live expiration
37
+ - LRU eviction when max size reached
38
+ - Thread-safe operations
39
+ - Hit/miss statistics
40
+ """
41
+
42
+ def __init__(self, max_size: int = 1000, ttl: int = 300):
43
+ """Initialize cache.
44
+
45
+ Args:
46
+ max_size: Maximum number of entries
47
+ ttl: Time-to-live in seconds (0 = no expiration)
48
+ """
49
+ if max_size <= 0:
50
+ raise ValueError("max_size must be greater than 0")
51
+ self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
52
+ self._max_size = max_size
53
+ self._ttl = ttl
54
+ self._lock = threading.RLock()
55
+ self._hits = 0
56
+ self._misses = 0
57
+ self._evictions = 0
58
+ self._cleanup_thread: Optional[threading.Thread] = None
59
+
60
+ def _make_key(self, *args, **kwargs) -> str:
61
+ """Create cache key; hashing is explicitly non-security use."""
62
+ content = str(args) + str(sorted(kwargs.items()))
63
+ return hashlib.md5(content.encode(), usedforsecurity=False).hexdigest()
64
+
65
+ def _get_value(self, key: str) -> tuple[bool, Any]:
66
+ """Get a cached value while preserving the distinction between None and a miss."""
67
+ if key not in self._cache:
68
+ self._misses += 1
69
+ return False, None
70
+
71
+ entry = self._cache[key]
72
+
73
+ if entry.is_expired():
74
+ del self._cache[key]
75
+ self._misses += 1
76
+ logger.debug(f"Cache entry expired: {key[:8]}...")
77
+ return False, None
78
+
79
+ self._cache.move_to_end(key)
80
+ entry.hits += 1
81
+ self._hits += 1
82
+ logger.debug(f"Cache hit: {key[:8]}...")
83
+ return True, entry.value
84
+
85
+ def get(self, key: str) -> Optional[Any]:
86
+ """Get value from cache, returning None when absent or expired."""
87
+ with self._lock:
88
+ found, value = self._get_value(key)
89
+ return value if found else None
90
+
91
+ def set(self, key: str, value: Any, ttl: int = None) -> None:
92
+ """Store value in cache."""
93
+ with self._lock:
94
+ if key in self._cache:
95
+ self._cache[key] = CacheEntry(
96
+ value=value,
97
+ created_at=time.time(),
98
+ ttl=ttl if ttl is not None else self._ttl
99
+ )
100
+ self._cache.move_to_end(key)
101
+ logger.debug(f"Cache updated: {key[:8]}...")
102
+ return
103
+
104
+ if len(self._cache) >= self._max_size:
105
+ oldest_key = next(iter(self._cache))
106
+ del self._cache[oldest_key]
107
+ self._evictions += 1
108
+ logger.debug(f"Cache eviction: {oldest_key[:8]}...")
109
+
110
+ self._cache[key] = CacheEntry(
111
+ value=value,
112
+ created_at=time.time(),
113
+ ttl=ttl if ttl is not None else self._ttl
114
+ )
115
+ logger.debug(f"Cache set: {key[:8]}...")
116
+
117
+ def delete(self, key: str) -> bool:
118
+ """Delete entry from cache."""
119
+ with self._lock:
120
+ if key in self._cache:
121
+ del self._cache[key]
122
+ return True
123
+ return False
124
+
125
+ def clear(self) -> None:
126
+ """Clear all cached entries."""
127
+ with self._lock:
128
+ self._cache.clear()
129
+ self._hits = 0
130
+ self._misses = 0
131
+ self._evictions = 0
132
+ logger.info("Cache cleared")
133
+
134
+ def cleanup_expired(self) -> int:
135
+ """Remove all expired entries and return the number removed."""
136
+ with self._lock:
137
+ expired_keys = [key for key, entry in self._cache.items() if entry.is_expired()]
138
+ for key in expired_keys:
139
+ del self._cache[key]
140
+ if expired_keys:
141
+ logger.debug(f"Cleaned up {len(expired_keys)} expired entries")
142
+ return len(expired_keys)
143
+
144
+ def get_stats(self) -> Dict[str, Any]:
145
+ """Get cache statistics."""
146
+ with self._lock:
147
+ total = self._hits + self._misses
148
+ hit_rate = (self._hits / total * 100) if total > 0 else 0
149
+ return {
150
+ "size": len(self._cache),
151
+ "max_size": self._max_size,
152
+ "ttl": self._ttl,
153
+ "hits": self._hits,
154
+ "misses": self._misses,
155
+ "evictions": self._evictions,
156
+ "hit_rate": f"{hit_rate:.1f}%"
157
+ }
158
+
159
+ def __len__(self) -> int:
160
+ return len(self._cache)
161
+
162
+ def __contains__(self, key: str) -> bool:
163
+ with self._lock:
164
+ if key not in self._cache:
165
+ return False
166
+ return not self._cache[key].is_expired()
167
+
168
+ def get_or_set(self, key: str, factory: Callable[[], Any], ttl: int = None) -> Any:
169
+ """Get cached value or compute and cache it.
170
+
171
+ The factory is invoked *outside* the cache lock so that expensive
172
+ computation (e.g. a transpile or an LLM call) does not block every
173
+ other thread that reads from or writes to the cache. The value is
174
+ written back under the lock, so concurrent callers that miss at
175
+ the same time may each compute once — that is an acceptable,
176
+ documented trade-off for a non-idempotent-safe cache.
177
+ """
178
+ with self._lock:
179
+ found, value = self._get_value(key)
180
+ if found:
181
+ return value
182
+ value = factory()
183
+ self.set(key, value, ttl)
184
+ return value
185
+
186
+ async def get_async(self, key: str) -> Optional[Any]:
187
+ """Async version of get."""
188
+ import asyncio
189
+ loop = asyncio.get_running_loop()
190
+ return await loop.run_in_executor(None, self.get, key)
191
+
192
+ async def set_async(self, key: str, value: Any, ttl: int = None) -> None:
193
+ """Async version of set."""
194
+ import asyncio
195
+ loop = asyncio.get_running_loop()
196
+ await loop.run_in_executor(None, lambda: self.set(key, value, ttl))
197
+
198
+ def start_background_cleanup(self, interval: int = 60) -> threading.Thread:
199
+ """Start a background daemon thread for periodic cache cleanup.
200
+
201
+ Repeated calls are rejected with ``RuntimeError`` rather than
202
+ spawning a second (or third…) thread. The cleanup loop is
203
+ idempotent — one thread is enough — and silently restarting would
204
+ leak a thread per call and eventually starve the GIL.
205
+ """
206
+ if interval <= 0:
207
+ raise ValueError("interval must be positive")
208
+
209
+ with self._lock:
210
+ if self._cleanup_thread is not None and self._cleanup_thread.is_alive():
211
+ raise RuntimeError(
212
+ "background cleanup is already running for this cache"
213
+ )
214
+
215
+ def cleanup_loop():
216
+ while True:
217
+ time.sleep(interval)
218
+ try:
219
+ removed = self.cleanup_expired()
220
+ if removed > 0:
221
+ logger.info(f"Background cleanup removed {removed} expired entries")
222
+ except Exception as e:
223
+ logger.error(f"Background cleanup error: {e}")
224
+
225
+ thread = threading.Thread(target=cleanup_loop, daemon=True, name="CacheCleanup")
226
+ with self._lock:
227
+ self._cleanup_thread = thread
228
+ thread.start()
229
+ logger.info(f"Started background cache cleanup (interval={interval}s)")
230
+ return thread
231
+
232
+
233
+ class CachedFunction:
234
+ """Decorator for caching function results."""
235
+
236
+ def __init__(self, cache: TTLCache = None, key_func: Callable = None):
237
+ self._cache = cache if cache is not None else TTLCache()
238
+ self._key_func = key_func
239
+
240
+ def __call__(self, func: Callable) -> Callable:
241
+ def wrapper(*args, **kwargs):
242
+ if self._key_func:
243
+ key = self._key_func(*args, **kwargs)
244
+ else:
245
+ key = self._cache._make_key(func.__name__, *args, **kwargs)
246
+ with self._cache._lock:
247
+ found, value = self._cache._get_value(key)
248
+ if found:
249
+ return value
250
+ value = func(*args, **kwargs)
251
+ self._cache.set(key, value)
252
+ return value
253
+ wrapper.__name__ = func.__name__
254
+ wrapper.__doc__ = func.__doc__
255
+ wrapper.cache = self._cache
256
+ return wrapper
257
+
258
+
259
+ transpile_cache = TTLCache(max_size=1000, ttl=300)
260
+ function_cache = TTLCache(max_size=500, ttl=600)
261
+ type_cache = TTLCache(max_size=200, ttl=600)
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Iterable, Optional
5
+
6
+ _IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)*$")
7
+
8
+
9
+ def validate_column_hints(column_hints: Optional[Iterable[str]]) -> Optional[str]:
10
+ """Return a stable validation error for unsafe or malformed column hints."""
11
+ if column_hints is None:
12
+ return None
13
+ if not isinstance(column_hints, (list, tuple)):
14
+ return "column_hints must be a list of column identifiers"
15
+ if len(column_hints) > 100:
16
+ return "column_hints must contain at most 100 columns"
17
+ for index, column in enumerate(column_hints):
18
+ if not isinstance(column, str) or not column.strip():
19
+ return f"column_hints[{index}] must be a non-empty string"
20
+ if not _IDENTIFIER.fullmatch(column.strip()):
21
+ return f"column_hints[{index}] is not a valid column identifier"
22
+ return None