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