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