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
frontend/core/state.py
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
"""Session State Adapter for SQL Dialect Master v2.
|
|
2
|
+
|
|
3
|
+
Provides a typed, centralized boundary between raw Streamlit session state
|
|
4
|
+
and application domain state. Widget-local keys (passed to ``st.*`` widgets)
|
|
5
|
+
remain managed by Streamlit directly — only cross-rerun / cross-page
|
|
6
|
+
application state flows through this adapter.
|
|
7
|
+
|
|
8
|
+
Design:
|
|
9
|
+
Streamlit widget state
|
|
10
|
+
|
|
|
11
|
+
| raw session_state (widget keys)
|
|
12
|
+
v
|
|
13
|
+
SessionState adapter <--- canonical boundary
|
|
14
|
+
|
|
|
15
|
+
v
|
|
16
|
+
typed application state
|
|
17
|
+
|
|
|
18
|
+
v
|
|
19
|
+
pages (read/write via adapter)
|
|
20
|
+
|
|
21
|
+
HistoryEntry contract:
|
|
22
|
+
identity str — opaque stable key (hex, 8+ chars)
|
|
23
|
+
sql str — source SQL
|
|
24
|
+
source_dialect str
|
|
25
|
+
target_dialect str
|
|
26
|
+
result_sql str | None
|
|
27
|
+
status StatusKind
|
|
28
|
+
created_at str — ISO-8601
|
|
29
|
+
is_favorite bool — mirrors favorites membership
|
|
30
|
+
|
|
31
|
+
Legacy raw dict shape (still accepted):
|
|
32
|
+
{identity, sql, src, tgt, result, status, created_at, is_favorite}
|
|
33
|
+
|
|
34
|
+
Current state version: 1
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from typing import Any, Literal
|
|
40
|
+
|
|
41
|
+
import streamlit as st
|
|
42
|
+
|
|
43
|
+
from frontend.core.navigation import (
|
|
44
|
+
NavigationIntent,
|
|
45
|
+
consume_navigation_intent,
|
|
46
|
+
get_pending_intent,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# ── Versioning ────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
_STATE_VERSION_KEY = "sdm_state_version"
|
|
52
|
+
_CURRENT_VERSION = 1
|
|
53
|
+
|
|
54
|
+
# ── Type aliases ──────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
StatusKind = Literal["valid", "warning", "error", "info", "neutral"]
|
|
57
|
+
|
|
58
|
+
# ── Domain models ──────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class HistoryEntry:
|
|
63
|
+
"""Immutable history entry with stable identity.
|
|
64
|
+
|
|
65
|
+
NOTE: `is_favorite` is intentionally NOT a persisted field.
|
|
66
|
+
Favorite status is tracked solely in the `favorites` list
|
|
67
|
+
(sdm_favorites) and accessed via `state.is_favorite(identity)`.
|
|
68
|
+
This prevents dual-source-of-truth for favorite state.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
identity: str
|
|
72
|
+
sql: str
|
|
73
|
+
source_dialect: str
|
|
74
|
+
target_dialect: str
|
|
75
|
+
result_sql: str | None = None
|
|
76
|
+
status: StatusKind = "neutral"
|
|
77
|
+
created_at: str = ""
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def from_raw_dict(cls, raw: dict[str, Any]) -> "HistoryEntry":
|
|
81
|
+
"""Build a HistoryEntry from a legacy runtime dict."""
|
|
82
|
+
return cls(
|
|
83
|
+
identity=raw.get("identity", ""),
|
|
84
|
+
sql=raw.get("sql", ""),
|
|
85
|
+
source_dialect=raw.get("src", raw.get("source_dialect", "")),
|
|
86
|
+
target_dialect=raw.get("tgt", raw.get("target_dialect", "")),
|
|
87
|
+
result_sql=raw.get("result") if "result" in raw else raw.get("result_sql"),
|
|
88
|
+
status=raw.get("status", "neutral"),
|
|
89
|
+
created_at=raw.get("created_at", ""),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def to_raw_dict(self) -> dict[str, Any]:
|
|
93
|
+
"""Serialize to the raw dict shape stored in session state.
|
|
94
|
+
|
|
95
|
+
NOTE: `is_favorite` is intentionally omitted. Favorite status is
|
|
96
|
+
tracked solely in the `favorites` list, not on individual entries.
|
|
97
|
+
"""
|
|
98
|
+
return {
|
|
99
|
+
"identity": self.identity,
|
|
100
|
+
"sql": self.sql,
|
|
101
|
+
"src": self.source_dialect,
|
|
102
|
+
"tgt": self.target_dialect,
|
|
103
|
+
"result": self.result_sql,
|
|
104
|
+
"status": self.status,
|
|
105
|
+
"created_at": self.created_at,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── Legacy state model classes (backward-compatible, not wired into runtime) ──
|
|
110
|
+
#
|
|
111
|
+
# These classes exist for test compatibility and documentation. Production
|
|
112
|
+
# code should use SessionState.get() instead. They mirror the original
|
|
113
|
+
# intent of state.py and are intentionally NOT instantiated by pages.
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class ConversionState:
|
|
118
|
+
"""LEGACY — use SessionState instead. Kept for test compatibility."""
|
|
119
|
+
|
|
120
|
+
source_dialect: str = "postgres"
|
|
121
|
+
target_dialect: str = "mysql"
|
|
122
|
+
source_sql: str = ""
|
|
123
|
+
target_sql: str | None = None
|
|
124
|
+
last_result: Any | None = None
|
|
125
|
+
formatted_source: str | None = None
|
|
126
|
+
batch_source: str = ""
|
|
127
|
+
batch_target: str = "mysql"
|
|
128
|
+
batch_results: list[Any] | None = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class NL2SQLState:
|
|
133
|
+
"""LEGACY — use SessionState instead. Kept for test compatibility."""
|
|
134
|
+
|
|
135
|
+
target_dialect: str = "postgres"
|
|
136
|
+
natural_language: str = ""
|
|
137
|
+
table_hint: str = ""
|
|
138
|
+
last_result: Any | None = None
|
|
139
|
+
loading: bool = False
|
|
140
|
+
error: str | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass(frozen=True)
|
|
144
|
+
class LineageState:
|
|
145
|
+
"""LEGACY — use SessionState instead. Kept for test compatibility."""
|
|
146
|
+
|
|
147
|
+
dialect: str = "postgres"
|
|
148
|
+
sql: str = ""
|
|
149
|
+
last_tables: list[dict[str, str]] = field(default_factory=list)
|
|
150
|
+
last_output_cols: list[str] = field(default_factory=list)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class ExplainState:
|
|
155
|
+
"""LEGACY — use SessionState instead. Kept for test compatibility."""
|
|
156
|
+
|
|
157
|
+
sql: str = ""
|
|
158
|
+
dialect_a: str = "postgres"
|
|
159
|
+
dialect_b: str = "mysql"
|
|
160
|
+
plan_a: dict[str, Any] | None = None
|
|
161
|
+
plan_b: dict[str, Any] | None = None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class FunctionsState:
|
|
166
|
+
"""LEGACY — use widget keys instead. Kept for test compatibility."""
|
|
167
|
+
|
|
168
|
+
search_query: str = ""
|
|
169
|
+
category_filter: str = "all"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True)
|
|
173
|
+
class TypesState:
|
|
174
|
+
"""LEGACY — use widget keys instead. Kept for test compatibility."""
|
|
175
|
+
|
|
176
|
+
source_dialect: str = "postgres"
|
|
177
|
+
target_dialect: str = "mysql"
|
|
178
|
+
selected_type: str = ""
|
|
179
|
+
category_filter: str = "all"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class SettingsState:
|
|
184
|
+
"""LEGACY — use SessionState.theme instead. Kept for test compatibility."""
|
|
185
|
+
|
|
186
|
+
theme: Literal["dark", "light"] = "dark"
|
|
187
|
+
keybindings_visible: bool = False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass
|
|
191
|
+
class AppState:
|
|
192
|
+
"""LEGACY — use SessionState instead. Kept for test compatibility."""
|
|
193
|
+
|
|
194
|
+
theme: Literal["dark", "light"] = "dark"
|
|
195
|
+
history: list[HistoryEntry] = field(default_factory=list)
|
|
196
|
+
favorites: list[str] = field(default_factory=list) # FIXED: was list[int]
|
|
197
|
+
conversion: ConversionState = field(default_factory=ConversionState)
|
|
198
|
+
nl2sql: NL2SQLState = field(default_factory=NL2SQLState)
|
|
199
|
+
lineage: LineageState = field(default_factory=LineageState)
|
|
200
|
+
explain: ExplainState = field(default_factory=ExplainState)
|
|
201
|
+
functions: FunctionsState = field(default_factory=FunctionsState)
|
|
202
|
+
types: TypesState = field(default_factory=TypesState)
|
|
203
|
+
settings: SettingsState = field(default_factory=SettingsState)
|
|
204
|
+
navigation: NavigationIntent = field(default_factory=NavigationIntent)
|
|
205
|
+
|
|
206
|
+
def current_theme_name(self) -> Literal["dark", "light"]:
|
|
207
|
+
return self.theme
|
|
208
|
+
|
|
209
|
+
def history_count(self) -> int:
|
|
210
|
+
return len(self.history)
|
|
211
|
+
|
|
212
|
+
def favorite_count(self) -> int:
|
|
213
|
+
return len(self.favorites)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ── SessionState ───────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class SessionState:
|
|
220
|
+
"""Typed adapter over Streamlit session state.
|
|
221
|
+
|
|
222
|
+
Use ``SessionState.get()`` to obtain the singleton instance for the
|
|
223
|
+
current Streamlit session. All application-level state flows through
|
|
224
|
+
this class — widget-local keys remain managed by Streamlit directly.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
# Application-state keys (managed by this adapter)
|
|
228
|
+
_KEY_THEME = "sdm_theme"
|
|
229
|
+
_KEY_HISTORY = "sdm_history"
|
|
230
|
+
_KEY_FAVORITES = "sdm_favorites"
|
|
231
|
+
_KEY_NAVIGATION = "sdm_navigation_intent"
|
|
232
|
+
_KEY_FINDING = "sdm_selected_finding_index"
|
|
233
|
+
_KEY_CONVERT_VM = "convert_last_vm"
|
|
234
|
+
_KEY_CONVERT_TARGET_SQL = "convert_target_sql"
|
|
235
|
+
_KEY_BATCH_VM = "batch_last_vm"
|
|
236
|
+
_KEY_NL_VM = "nl_last_vm"
|
|
237
|
+
_KEY_NL_ERROR = "nl_generation_error"
|
|
238
|
+
_KEY_DIFF_VM = "diff_last_vm"
|
|
239
|
+
_KEY_LINEAGE_VM = "lineage_last_vm"
|
|
240
|
+
_KEY_QA_RESULT = "qa_last_result"
|
|
241
|
+
|
|
242
|
+
# Widget-local keys (NOT managed by this adapter)
|
|
243
|
+
# convert_src, convert_tgt, convert_src_sql, batch_sql_input,
|
|
244
|
+
# batch_src, batch_tgt, nl_example_sel, nl_input_text, nl_dialect,
|
|
245
|
+
# nl_table_hint, diff_src_sql, diff_tgt_sql, diff_src_dialect,
|
|
246
|
+
# diff_tgt_dialect, lineage_dialect, lineage_sql, qa_dialect, qa_sql,
|
|
247
|
+
# hist_search, hist_filter, settings_theme_sel
|
|
248
|
+
|
|
249
|
+
def __init__(self) -> None:
|
|
250
|
+
self._raw: dict[str, Any] = st.session_state
|
|
251
|
+
|
|
252
|
+
# ── Lifecycle ────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def get(cls) -> "SessionState":
|
|
256
|
+
"""Return the singleton SessionState for the current session."""
|
|
257
|
+
return cls()
|
|
258
|
+
|
|
259
|
+
def ensure_defaults(self) -> None:
|
|
260
|
+
"""Initialize missing application-state keys with safe defaults.
|
|
261
|
+
|
|
262
|
+
NOTE: Does NOT write the state version key. That is the responsibility
|
|
263
|
+
of `migrate()`. This separation ensures migration can read the
|
|
264
|
+
existing version before it gets overwritten.
|
|
265
|
+
"""
|
|
266
|
+
raw = self._raw
|
|
267
|
+
# Theme default — only set if missing (preserve user's choice)
|
|
268
|
+
if self._KEY_THEME not in raw:
|
|
269
|
+
raw[self._KEY_THEME] = "dark"
|
|
270
|
+
# History default
|
|
271
|
+
if self._KEY_HISTORY not in raw:
|
|
272
|
+
raw[self._KEY_HISTORY] = []
|
|
273
|
+
# Favorites default
|
|
274
|
+
if self._KEY_FAVORITES not in raw:
|
|
275
|
+
raw[self._KEY_FAVORITES] = []
|
|
276
|
+
# Navigation intent — reset stale intents on fresh session
|
|
277
|
+
if self._KEY_NAVIGATION not in raw or not isinstance(
|
|
278
|
+
raw[self._KEY_NAVIGATION], NavigationIntent
|
|
279
|
+
):
|
|
280
|
+
raw[self._KEY_NAVIGATION] = NavigationIntent()
|
|
281
|
+
# Finding selection
|
|
282
|
+
if self._KEY_FINDING not in raw:
|
|
283
|
+
raw[self._KEY_FINDING] = None
|
|
284
|
+
# Workspace VMs
|
|
285
|
+
if self._KEY_CONVERT_VM not in raw:
|
|
286
|
+
raw[self._KEY_CONVERT_VM] = None
|
|
287
|
+
if self._KEY_CONVERT_TARGET_SQL not in raw:
|
|
288
|
+
raw[self._KEY_CONVERT_TARGET_SQL] = ""
|
|
289
|
+
if self._KEY_BATCH_VM not in raw:
|
|
290
|
+
raw[self._KEY_BATCH_VM] = None
|
|
291
|
+
if self._KEY_NL_VM not in raw:
|
|
292
|
+
raw[self._KEY_NL_VM] = None
|
|
293
|
+
if self._KEY_NL_ERROR not in raw:
|
|
294
|
+
raw[self._KEY_NL_ERROR] = None
|
|
295
|
+
if self._KEY_DIFF_VM not in raw:
|
|
296
|
+
raw[self._KEY_DIFF_VM] = None
|
|
297
|
+
if self._KEY_LINEAGE_VM not in raw:
|
|
298
|
+
raw[self._KEY_LINEAGE_VM] = None
|
|
299
|
+
if self._KEY_QA_RESULT not in raw:
|
|
300
|
+
raw[self._KEY_QA_RESULT] = None
|
|
301
|
+
|
|
302
|
+
def migrate(self) -> None:
|
|
303
|
+
"""Upgrade legacy session state to the current schema version.
|
|
304
|
+
|
|
305
|
+
IMPORTANT: Must be called AFTER ensure_defaults() has read the
|
|
306
|
+
existing version but BEFORE it overwrites with the current version.
|
|
307
|
+
See __init__ for the correct call order.
|
|
308
|
+
"""
|
|
309
|
+
raw = self._raw
|
|
310
|
+
# Read version BEFORE ensure_defaults writes the current version.
|
|
311
|
+
# If the user calls migrate() directly, check what's there.
|
|
312
|
+
version = raw.get(_STATE_VERSION_KEY, 0)
|
|
313
|
+
if version >= _CURRENT_VERSION:
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# Migrate favorites from int indices to string identities
|
|
317
|
+
favorites = raw.get(self._KEY_FAVORITES, [])
|
|
318
|
+
if isinstance(favorites, list) and favorites:
|
|
319
|
+
if isinstance(favorites[0], int):
|
|
320
|
+
# Legacy int-index favorites → empty (identities lost)
|
|
321
|
+
raw[self._KEY_FAVORITES] = []
|
|
322
|
+
logger = __import__("logging").getLogger(__name__)
|
|
323
|
+
logger.warning(
|
|
324
|
+
"Migrated legacy int-based favorites to empty list "
|
|
325
|
+
"(identities cannot be recovered)"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Migrate history entries to canonical shape
|
|
329
|
+
history = raw.get(self._KEY_HISTORY, [])
|
|
330
|
+
if history and isinstance(history, list):
|
|
331
|
+
cleaned: list[dict[str, Any]] = []
|
|
332
|
+
import uuid
|
|
333
|
+
for entry in history:
|
|
334
|
+
if not isinstance(entry, dict):
|
|
335
|
+
continue
|
|
336
|
+
if "identity" not in entry or not entry["identity"]:
|
|
337
|
+
# Generate a stable identity for entries that lack one
|
|
338
|
+
entry["identity"] = uuid.uuid4().hex[:8]
|
|
339
|
+
cleaned.append(entry)
|
|
340
|
+
if len(cleaned) != len(history):
|
|
341
|
+
raw[self._KEY_HISTORY] = cleaned
|
|
342
|
+
logger = __import__("logging").getLogger(__name__)
|
|
343
|
+
logger.warning(
|
|
344
|
+
"Migrated %d invalid history entries",
|
|
345
|
+
len(history) - len(cleaned),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
raw[_STATE_VERSION_KEY] = _CURRENT_VERSION
|
|
349
|
+
|
|
350
|
+
# ── Theme ──────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def theme(self) -> str:
|
|
354
|
+
return self._raw.get(self._KEY_THEME, "dark")
|
|
355
|
+
|
|
356
|
+
@theme.setter
|
|
357
|
+
def theme(self, value: str) -> None:
|
|
358
|
+
self._raw[self._KEY_THEME] = value
|
|
359
|
+
|
|
360
|
+
# ── History ────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def history(self) -> list[dict[str, Any]]:
|
|
364
|
+
return list(self._raw.get(self._KEY_HISTORY, []))
|
|
365
|
+
|
|
366
|
+
@history.setter
|
|
367
|
+
def history(self, value: list[dict[str, Any]]) -> None:
|
|
368
|
+
self._raw[self._KEY_HISTORY] = value
|
|
369
|
+
|
|
370
|
+
def add_history_entry(
|
|
371
|
+
self,
|
|
372
|
+
sql: str,
|
|
373
|
+
source_dialect: str,
|
|
374
|
+
target_dialect: str,
|
|
375
|
+
result_sql: str | None = None,
|
|
376
|
+
status: StatusKind = "neutral",
|
|
377
|
+
created_at: str = "",
|
|
378
|
+
) -> str:
|
|
379
|
+
"""Add a history entry and return its identity string."""
|
|
380
|
+
import uuid
|
|
381
|
+
identity = uuid.uuid4().hex[:8]
|
|
382
|
+
entry: dict[str, Any] = {
|
|
383
|
+
"identity": identity,
|
|
384
|
+
"sql": sql,
|
|
385
|
+
"src": source_dialect,
|
|
386
|
+
"tgt": target_dialect,
|
|
387
|
+
"result": result_sql,
|
|
388
|
+
"status": status,
|
|
389
|
+
"created_at": created_at,
|
|
390
|
+
# NOTE: is_favorite is NOT persisted here.
|
|
391
|
+
# Favorite status is tracked solely in the `favorites` list.
|
|
392
|
+
# Access via state.is_favorite(identity).
|
|
393
|
+
}
|
|
394
|
+
if self._KEY_HISTORY not in self._raw:
|
|
395
|
+
self._raw[self._KEY_HISTORY] = []
|
|
396
|
+
self._raw[self._KEY_HISTORY].append(entry)
|
|
397
|
+
return identity
|
|
398
|
+
|
|
399
|
+
def remove_history_entry(self, identity: str) -> bool:
|
|
400
|
+
"""Remove a history entry by identity. Returns True if found."""
|
|
401
|
+
if self._KEY_HISTORY not in self._raw:
|
|
402
|
+
return False
|
|
403
|
+
history: list[dict[str, Any]] = self._raw[self._KEY_HISTORY]
|
|
404
|
+
before = len(history)
|
|
405
|
+
self._raw[self._KEY_HISTORY] = [
|
|
406
|
+
e for e in history if e.get("identity") != identity
|
|
407
|
+
]
|
|
408
|
+
removed = before - len(self._raw[self._KEY_HISTORY])
|
|
409
|
+
if removed > 0:
|
|
410
|
+
# Also remove from favorites
|
|
411
|
+
self._remove_favorite(identity)
|
|
412
|
+
return removed > 0
|
|
413
|
+
|
|
414
|
+
def history_count(self) -> int:
|
|
415
|
+
return len(self._raw.get(self._KEY_HISTORY, []))
|
|
416
|
+
|
|
417
|
+
# ── Favorites ──────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def favorites(self) -> list[str]:
|
|
421
|
+
"""Return favorites as a list of history entry identity strings."""
|
|
422
|
+
raw = self._raw.get(self._KEY_FAVORITES)
|
|
423
|
+
# Handle None or non-list types safely
|
|
424
|
+
if not isinstance(raw, list):
|
|
425
|
+
self._raw[self._KEY_FAVORITES] = []
|
|
426
|
+
return []
|
|
427
|
+
# Ensure all items are strings; filter out non-strings
|
|
428
|
+
if raw and not isinstance(raw[0], str):
|
|
429
|
+
# Legacy int-based or mixed — migrate to empty
|
|
430
|
+
self._raw[self._KEY_FAVORITES] = []
|
|
431
|
+
return []
|
|
432
|
+
# Filter to ensure only strings remain
|
|
433
|
+
result = [f for f in raw if isinstance(f, str)]
|
|
434
|
+
if len(result) != len(raw):
|
|
435
|
+
self._raw[self._KEY_FAVORITES] = result
|
|
436
|
+
return result
|
|
437
|
+
|
|
438
|
+
@favorites.setter
|
|
439
|
+
def favorites(self, value: list[str]) -> None:
|
|
440
|
+
# Validate: must be list[str]
|
|
441
|
+
if not isinstance(value, list):
|
|
442
|
+
raise TypeError(f"favorites must be list[str], got {type(value).__name__}")
|
|
443
|
+
for item in value:
|
|
444
|
+
if not isinstance(item, str):
|
|
445
|
+
raise TypeError(
|
|
446
|
+
f"favorites items must be str, got {type(item).__name__}"
|
|
447
|
+
)
|
|
448
|
+
self._raw[self._KEY_FAVORITES] = list(value)
|
|
449
|
+
|
|
450
|
+
def add_favorite(self, identity: str) -> None:
|
|
451
|
+
"""Add a history entry identity to favorites (idempotent)."""
|
|
452
|
+
favs = self.favorites
|
|
453
|
+
if identity not in favs:
|
|
454
|
+
self._raw[self._KEY_FAVORITES] = favs + [identity]
|
|
455
|
+
|
|
456
|
+
def remove_favorite(self, identity: str) -> None:
|
|
457
|
+
"""Remove a history entry identity from favorites."""
|
|
458
|
+
favs = self.favorites
|
|
459
|
+
if identity in favs:
|
|
460
|
+
self._raw[self._KEY_FAVORITES] = [f for f in favs if f != identity]
|
|
461
|
+
|
|
462
|
+
def toggle_favorite(self, identity: str) -> None:
|
|
463
|
+
"""Toggle favorite status for a history entry identity."""
|
|
464
|
+
if identity in self.favorites:
|
|
465
|
+
self.remove_favorite(identity)
|
|
466
|
+
else:
|
|
467
|
+
self.add_favorite(identity)
|
|
468
|
+
|
|
469
|
+
def is_favorite(self, identity: str) -> bool:
|
|
470
|
+
return identity in self.favorites
|
|
471
|
+
|
|
472
|
+
def favorite_count(self) -> int:
|
|
473
|
+
return len(self.favorites)
|
|
474
|
+
|
|
475
|
+
def _remove_favorite(self, identity: str) -> None:
|
|
476
|
+
"""Internal: remove identity from favorites without validation."""
|
|
477
|
+
favs = self._raw.get(self._KEY_FAVORITES, [])
|
|
478
|
+
if isinstance(favs, list) and identity in favs:
|
|
479
|
+
self._raw[self._KEY_FAVORITES] = [f for f in favs if f != identity]
|
|
480
|
+
|
|
481
|
+
# ── Conversion ─────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def convert_last_vm(self) -> Any:
|
|
485
|
+
return self._raw.get(self._KEY_CONVERT_VM)
|
|
486
|
+
|
|
487
|
+
@convert_last_vm.setter
|
|
488
|
+
def convert_last_vm(self, value: Any) -> None:
|
|
489
|
+
self._raw[self._KEY_CONVERT_VM] = value
|
|
490
|
+
|
|
491
|
+
@property
|
|
492
|
+
def convert_target_sql(self) -> str:
|
|
493
|
+
return self._raw.get(self._KEY_CONVERT_TARGET_SQL, "")
|
|
494
|
+
|
|
495
|
+
@convert_target_sql.setter
|
|
496
|
+
def convert_target_sql(self, value: str) -> None:
|
|
497
|
+
self._raw[self._KEY_CONVERT_TARGET_SQL] = value or ""
|
|
498
|
+
|
|
499
|
+
@property
|
|
500
|
+
def batch_last_vm(self) -> Any:
|
|
501
|
+
return self._raw.get(self._KEY_BATCH_VM)
|
|
502
|
+
|
|
503
|
+
@batch_last_vm.setter
|
|
504
|
+
def batch_last_vm(self, value: Any) -> None:
|
|
505
|
+
self._raw[self._KEY_BATCH_VM] = value
|
|
506
|
+
|
|
507
|
+
# ── NL2SQL ──────────────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
@property
|
|
510
|
+
def nl_last_vm(self) -> Any:
|
|
511
|
+
return self._raw.get(self._KEY_NL_VM)
|
|
512
|
+
|
|
513
|
+
@nl_last_vm.setter
|
|
514
|
+
def nl_last_vm(self, value: Any) -> None:
|
|
515
|
+
self._raw[self._KEY_NL_VM] = value
|
|
516
|
+
|
|
517
|
+
@property
|
|
518
|
+
def nl_generation_error(self) -> str | None:
|
|
519
|
+
return self._raw.get(self._KEY_NL_ERROR)
|
|
520
|
+
|
|
521
|
+
@nl_generation_error.setter
|
|
522
|
+
def nl_generation_error(self, value: str | None) -> None:
|
|
523
|
+
self._raw[self._KEY_NL_ERROR] = value
|
|
524
|
+
|
|
525
|
+
# ── Diff ────────────────────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
@property
|
|
528
|
+
def diff_last_vm(self) -> Any:
|
|
529
|
+
return self._raw.get(self._KEY_DIFF_VM)
|
|
530
|
+
|
|
531
|
+
@diff_last_vm.setter
|
|
532
|
+
def diff_last_vm(self, value: Any) -> None:
|
|
533
|
+
self._raw[self._KEY_DIFF_VM] = value
|
|
534
|
+
|
|
535
|
+
@property
|
|
536
|
+
def selected_finding_index(self) -> int | None:
|
|
537
|
+
return self._raw.get(self._KEY_FINDING)
|
|
538
|
+
|
|
539
|
+
@selected_finding_index.setter
|
|
540
|
+
def selected_finding_index(self, value: int | None) -> None:
|
|
541
|
+
self._raw[self._KEY_FINDING] = value
|
|
542
|
+
|
|
543
|
+
# ── Lineage ─────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
@property
|
|
546
|
+
def lineage_last_vm(self) -> Any:
|
|
547
|
+
return self._raw.get(self._KEY_LINEAGE_VM)
|
|
548
|
+
|
|
549
|
+
@lineage_last_vm.setter
|
|
550
|
+
def lineage_last_vm(self, value: Any) -> None:
|
|
551
|
+
self._raw[self._KEY_LINEAGE_VM] = value
|
|
552
|
+
|
|
553
|
+
# ── Query Analysis ──────────────────────────────────────────────────
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def qa_last_result(self) -> Any:
|
|
557
|
+
return self._raw.get(self._KEY_QA_RESULT)
|
|
558
|
+
|
|
559
|
+
@qa_last_result.setter
|
|
560
|
+
def qa_last_result(self, value: Any) -> None:
|
|
561
|
+
self._raw[self._KEY_QA_RESULT] = value
|
|
562
|
+
|
|
563
|
+
# ── Navigation (delegates to navigation.py) ────────────────────────
|
|
564
|
+
|
|
565
|
+
def get_pending_intent(self) -> NavigationIntent:
|
|
566
|
+
"""Read the current pending navigation intent."""
|
|
567
|
+
return get_pending_intent()
|
|
568
|
+
|
|
569
|
+
def consume_navigation_intent(self) -> NavigationIntent | None:
|
|
570
|
+
"""Atomically read and clear the pending navigation intent."""
|
|
571
|
+
return consume_navigation_intent()
|
|
572
|
+
|
|
573
|
+
def clear_finding_selection(self) -> None:
|
|
574
|
+
"""Clear the selected finding index."""
|
|
575
|
+
self._raw[self._KEY_FINDING] = None
|
|
576
|
+
|
|
577
|
+
# ── Raw access (for pages that still need it) ──────────────────────
|
|
578
|
+
|
|
579
|
+
def get_raw(self, key: str) -> Any:
|
|
580
|
+
"""Access a raw session-state key (use sparingly)."""
|
|
581
|
+
return self._raw.get(key)
|
|
582
|
+
|
|
583
|
+
def set_raw(self, key: str, value: Any) -> None:
|
|
584
|
+
"""Set a raw session-state key (use sparingly)."""
|
|
585
|
+
self._raw[key] = value
|
|
586
|
+
|
|
587
|
+
def has_raw(self, key: str) -> bool:
|
|
588
|
+
return key in self._raw
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ── Module-level convenience ───────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
def get_session_state() -> SessionState:
|
|
594
|
+
"""Return the current session's SessionState adapter."""
|
|
595
|
+
return SessionState.get()
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
__all__ = [
|
|
599
|
+
"SessionState",
|
|
600
|
+
"HistoryEntry",
|
|
601
|
+
"StatusKind",
|
|
602
|
+
"get_session_state",
|
|
603
|
+
# Legacy references (kept for import stability)
|
|
604
|
+
"ConversionState",
|
|
605
|
+
"NL2SQLState",
|
|
606
|
+
"LineageState",
|
|
607
|
+
"ExplainState",
|
|
608
|
+
"FunctionsState",
|
|
609
|
+
"TypesState",
|
|
610
|
+
"SettingsState",
|
|
611
|
+
"AppState",
|
|
612
|
+
]
|