sql-dialect-master 1.1.4__tar.gz → 1.1.6__tar.gz

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 (102) hide show
  1. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/PKG-INFO +21 -44
  2. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/README.md +20 -42
  3. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/api/main.py +157 -20
  4. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/api/middleware.py +71 -7
  5. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/api/readiness.py +44 -13
  6. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/api/workers.py +7 -1
  7. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/cache.py +15 -3
  8. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/config.py +3 -1
  9. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/functions_lookup.py +57 -25
  10. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/metadata.py +2 -2
  11. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/p1_sql_scanner.py +30 -2
  12. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/rules.py +68 -22
  13. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/semantic_diff.py +0 -1
  14. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/transpiler.py +617 -58
  15. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/type_mapping.py +4 -4
  16. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/pyproject.toml +2 -14
  17. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/sql_dialect_master.egg-info/PKG-INFO +21 -44
  18. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/sql_dialect_master.egg-info/SOURCES.txt +0 -35
  19. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/sql_dialect_master.egg-info/requires.txt +0 -1
  20. sql_dialect_master-1.1.6/sql_dialect_master.egg-info/top_level.txt +1 -0
  21. sql_dialect_master-1.1.4/backend/frontend_entrypoint.py +0 -22
  22. sql_dialect_master-1.1.4/frontend/__init__.py +0 -10
  23. sql_dialect_master-1.1.4/frontend/app_context_v2.py +0 -201
  24. sql_dialect_master-1.1.4/frontend/core/__init__.py +0 -33
  25. sql_dialect_master-1.1.4/frontend/core/design_tokens.py +0 -131
  26. sql_dialect_master-1.1.4/frontend/core/escaping.py +0 -13
  27. sql_dialect_master-1.1.4/frontend/core/fingerprints.py +0 -24
  28. sql_dialect_master-1.1.4/frontend/core/navigation.py +0 -189
  29. sql_dialect_master-1.1.4/frontend/core/sql_inputs.py +0 -6
  30. sql_dialect_master-1.1.4/frontend/core/state.py +0 -702
  31. sql_dialect_master-1.1.4/frontend/core/statuses.py +0 -34
  32. sql_dialect_master-1.1.4/frontend/core/styles.py +0 -523
  33. sql_dialect_master-1.1.4/frontend/core/themes.py +0 -75
  34. sql_dialect_master-1.1.4/frontend/core/type_status.py +0 -26
  35. sql_dialect_master-1.1.4/frontend/core/viewmodels.py +0 -502
  36. sql_dialect_master-1.1.4/frontend/pages/__init__.py +0 -0
  37. sql_dialect_master-1.1.4/frontend/pages/convert.py +0 -371
  38. sql_dialect_master-1.1.4/frontend/pages/diff.py +0 -185
  39. sql_dialect_master-1.1.4/frontend/pages/functions.py +0 -99
  40. sql_dialect_master-1.1.4/frontend/pages/history.py +0 -148
  41. sql_dialect_master-1.1.4/frontend/pages/lineage.py +0 -185
  42. sql_dialect_master-1.1.4/frontend/pages/nl2sql.py +0 -317
  43. sql_dialect_master-1.1.4/frontend/pages/query_analysis.py +0 -128
  44. sql_dialect_master-1.1.4/frontend/pages/runtime.py +0 -66
  45. sql_dialect_master-1.1.4/frontend/pages/settings.py +0 -60
  46. sql_dialect_master-1.1.4/frontend/pages/templates.py +0 -86
  47. sql_dialect_master-1.1.4/frontend/pages/types.py +0 -106
  48. sql_dialect_master-1.1.4/frontend/templates_v2.py +0 -53
  49. sql_dialect_master-1.1.4/frontend/ui/__init__.py +0 -0
  50. sql_dialect_master-1.1.4/frontend/ui/command_palette.py +0 -172
  51. sql_dialect_master-1.1.4/frontend/ui/diff.py +0 -269
  52. sql_dialect_master-1.1.4/frontend/ui/editor.py +0 -106
  53. sql_dialect_master-1.1.4/frontend/ui/status.py +0 -252
  54. sql_dialect_master-1.1.4/sdm_local_v2.py +0 -219
  55. sql_dialect_master-1.1.4/sql_dialect_master.egg-info/entry_points.txt +0 -2
  56. sql_dialect_master-1.1.4/sql_dialect_master.egg-info/top_level.txt +0 -3
  57. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/LICENSE +0 -0
  58. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/__init__.py +0 -0
  59. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/api/__init__.py +0 -0
  60. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/api/auth.py +0 -0
  61. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/api/rate_limit_store.py +0 -0
  62. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/__init__.py +0 -0
  63. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/column_hint_validation.py +0 -0
  64. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/constants.py +0 -0
  65. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/exceptions.py +0 -0
  66. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/function_call_scanner.py +0 -0
  67. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/function_semantics.py +0 -0
  68. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/functions_db.json +0 -0
  69. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql.py +0 -0
  70. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_components/__init__.py +0 -0
  71. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_components/aggregations.py +0 -0
  72. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
  73. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_components/core_extract.py +0 -0
  74. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_components/evidence.py +0 -0
  75. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_components/mappings.py +0 -0
  76. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_components/relations.py +0 -0
  77. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_components/templates.py +0 -0
  78. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_components/tokenizer.py +0 -0
  79. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_legacy.py +0 -0
  80. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/__init__.py +0 -0
  81. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
  82. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
  83. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/dialect.py +0 -0
  84. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/intent.py +0 -0
  85. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/ir.py +0 -0
  86. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/models.py +0 -0
  87. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/orchestrator.py +0 -0
  88. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/predicates.py +0 -0
  89. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/renderer.py +0 -0
  90. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/resolution.py +0 -0
  91. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
  92. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/templates.py +0 -0
  93. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/nl2sql_pipeline/validation.py +0 -0
  94. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/p2_data_validation.py +0 -0
  95. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/parser.py +0 -0
  96. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/post_processor.py +0 -0
  97. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/sql_statements.py +0 -0
  98. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/core/type_mapping.json +0 -0
  99. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/utils/__init__.py +0 -0
  100. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/backend/utils/validators.py +0 -0
  101. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/setup.cfg +0 -0
  102. {sql_dialect_master-1.1.4 → sql_dialect_master-1.1.6}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sql-dialect-master
3
- Version: 1.1.4
3
+ Version: 1.1.6
4
4
  Summary: SQL Dialect Master - Multi-database SQL conversion engine
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
8
  Requires-Dist: sqlglot<31.0.0,>=30.18.0
9
- Requires-Dist: streamlit>=1.38.0
10
9
  Requires-Dist: fastapi>=0.115.0
11
10
  Requires-Dist: uvicorn>=0.30.0
12
11
  Requires-Dist: pydantic>=2.9.0
@@ -36,12 +35,12 @@ Dynamic: license-file
36
35
 
37
36
  Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
38
37
 
39
- Current release: 1.1.4
38
+ Current release: 1.1.6
40
39
 
41
40
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
42
41
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
43
42
  [![FastAPI](https://img.shields.io/badge/FastAPI-0.115+-green.svg)](https://fastapi.tiangolo.com/)
44
- [![Streamlit](https://img.shields.io/badge/Streamlit-1.38+-red.svg)](https://streamlit.io/)
43
+ [![React](https://img.shields.io/badge/React-19-61DAFB.svg)](https://react.dev/)
45
44
 
46
45
  ## ✨ Features
47
46
 
@@ -58,7 +57,7 @@ Current release: 1.1.4
58
57
  | 🔧 **Rule Engine** | Deterministic priority ordering, compiled patterns, dialect-aware conflict detection |
59
58
  | 🛡️ **Security Validation** | Statement-level stacked-query detection with configurable blocking policy |
60
59
  | ⚡ **Performance** | Rule-aware cache versioning, bounded async batch conversion, and reproducible benchmark |
61
- | 🎨 **Modern UI** | Streamlit developer workspace: dark / light theme system, sidebar navigation, command palette |
60
+ | 🎨 **Modern UI** | React developer workspace with dark / light themes, responsive navigation, and command palette |
62
61
 
63
62
  ## 💾 Supported Databases
64
63
 
@@ -96,20 +95,24 @@ For database-backed semantic tests:
96
95
  .venv/bin/python -m pip install -e ".[dev,semantic]"
97
96
  ```
98
97
 
99
- ### Run Streamlit UI (Recommended)
98
+ ### Run the React UI
100
99
 
101
- ```bash
102
- streamlit run sdm_local_v2.py
103
- ```
104
-
105
- After installing the package, the same workspace can be launched from any
106
- working directory with:
100
+ The React workspace is the product frontend and runs independently from the
101
+ Python package. Start the API first, then run:
107
102
 
108
103
  ```bash
109
- sdm-streamlit
104
+ cd frontend-react
105
+ npm install
106
+ npm run dev
110
107
  ```
111
108
 
112
- Open http://localhost:8501 in your browser.
109
+ Open http://localhost:5174. The Vite development proxy forwards `/api`,
110
+ `/health`, and `/ready` to `http://127.0.0.1:8000`. Set `VITE_API_BASE_URL`
111
+ for a separately deployed API, or configure the URL and optional API key in
112
+ the React Settings page.
113
+
114
+ React implementation goals and gates are tracked in
115
+ [`docs/react-frontend-refactor-plan.md`](docs/react-frontend-refactor-plan.md).
113
116
 
114
117
  ### Run FastAPI Backend
115
118
 
@@ -270,42 +273,16 @@ sql-dialect-master/
270
273
  │ │ └── validators.py # Input validation
271
274
  │ │
272
275
  │ └── tests/ # Test suite and regression coverage
273
- ├── frontend/ # Streamlit UI Components (v2 architecture)
274
- │ ├── core/ # Cross-cutting concerns
275
- │ │ ├── design_tokens.py # Theme model (dark/light) + semantic status
276
- │ │ ├── navigation.py # NavigationIntent contract
277
- │ │ ├── state.py # SessionState adapter
278
- │ │ ├── themes.py # Theme switching (dark/light only)
279
- │ │ ├── styles.py # CSS generation from design tokens
280
- │ │ ├── escaping.py # HTML-safe rendering helpers
281
- │ │ └── viewmodels.py # Typed view models
282
- │ ├── pages/ # One module per workspace page
283
- │ │ ├── convert.py
284
- │ │ ├── nl2sql.py
285
- │ │ ├── diff.py
286
- │ │ ├── lineage.py
287
- │ │ ├── runtime.py
288
- │ │ ├── functions.py
289
- │ │ ├── types.py
290
- │ │ ├── templates.py
291
- │ │ ├── history.py
292
- │ │ ├── settings.py
293
- │ │ └── query_analysis.py
294
- │ ├── ui/ # Reusable UI widgets
295
- │ │ ├── command_palette.py
296
- │ │ ├── diff.py
297
- │ │ ├── editor.py
298
- │ │ └── status.py
299
- │ ├── app_context_v2.py # Shared application context (v2)
300
- │ └── templates_v2.py # SQL templates (v2)
301
276
  ├── examples/
302
277
  │ ├── api_client.py # API usage examples
303
278
  │ └── basic_usage.py # SDK usage examples
304
279
  ├── scripts/
305
280
  │ └── verify_readme_consistency.py # README source-of-truth checks
306
- ├── sdm_local_v2.py # Streamlit Application Entry (v2)
281
+ ├── frontend-react/ # React + TypeScript product frontend
282
+ │ ├── src/
283
+ │ ├── package.json
284
+ │ └── vite.config.ts
307
285
  ├── pyproject.toml # Project configuration
308
- ├── requirements.txt # Legacy/runtime dependency list
309
286
  ├── .env.example # Environment template
310
287
  ├── LICENSE # MIT License
311
288
  └── README.md
@@ -2,12 +2,12 @@
2
2
 
3
3
  Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
4
4
 
5
- Current release: 1.1.4
5
+ Current release: 1.1.6
6
6
 
7
7
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
9
  [![FastAPI](https://img.shields.io/badge/FastAPI-0.115+-green.svg)](https://fastapi.tiangolo.com/)
10
- [![Streamlit](https://img.shields.io/badge/Streamlit-1.38+-red.svg)](https://streamlit.io/)
10
+ [![React](https://img.shields.io/badge/React-19-61DAFB.svg)](https://react.dev/)
11
11
 
12
12
  ## ✨ Features
13
13
 
@@ -24,7 +24,7 @@ Current release: 1.1.4
24
24
  | 🔧 **Rule Engine** | Deterministic priority ordering, compiled patterns, dialect-aware conflict detection |
25
25
  | 🛡️ **Security Validation** | Statement-level stacked-query detection with configurable blocking policy |
26
26
  | ⚡ **Performance** | Rule-aware cache versioning, bounded async batch conversion, and reproducible benchmark |
27
- | 🎨 **Modern UI** | Streamlit developer workspace: dark / light theme system, sidebar navigation, command palette |
27
+ | 🎨 **Modern UI** | React developer workspace with dark / light themes, responsive navigation, and command palette |
28
28
 
29
29
  ## 💾 Supported Databases
30
30
 
@@ -62,20 +62,24 @@ For database-backed semantic tests:
62
62
  .venv/bin/python -m pip install -e ".[dev,semantic]"
63
63
  ```
64
64
 
65
- ### Run Streamlit UI (Recommended)
65
+ ### Run the React UI
66
66
 
67
- ```bash
68
- streamlit run sdm_local_v2.py
69
- ```
70
-
71
- After installing the package, the same workspace can be launched from any
72
- working directory with:
67
+ The React workspace is the product frontend and runs independently from the
68
+ Python package. Start the API first, then run:
73
69
 
74
70
  ```bash
75
- sdm-streamlit
71
+ cd frontend-react
72
+ npm install
73
+ npm run dev
76
74
  ```
77
75
 
78
- Open http://localhost:8501 in your browser.
76
+ Open http://localhost:5174. The Vite development proxy forwards `/api`,
77
+ `/health`, and `/ready` to `http://127.0.0.1:8000`. Set `VITE_API_BASE_URL`
78
+ for a separately deployed API, or configure the URL and optional API key in
79
+ the React Settings page.
80
+
81
+ React implementation goals and gates are tracked in
82
+ [`docs/react-frontend-refactor-plan.md`](docs/react-frontend-refactor-plan.md).
79
83
 
80
84
  ### Run FastAPI Backend
81
85
 
@@ -236,42 +240,16 @@ sql-dialect-master/
236
240
  │ │ └── validators.py # Input validation
237
241
  │ │
238
242
  │ └── tests/ # Test suite and regression coverage
239
- ├── frontend/ # Streamlit UI Components (v2 architecture)
240
- │ ├── core/ # Cross-cutting concerns
241
- │ │ ├── design_tokens.py # Theme model (dark/light) + semantic status
242
- │ │ ├── navigation.py # NavigationIntent contract
243
- │ │ ├── state.py # SessionState adapter
244
- │ │ ├── themes.py # Theme switching (dark/light only)
245
- │ │ ├── styles.py # CSS generation from design tokens
246
- │ │ ├── escaping.py # HTML-safe rendering helpers
247
- │ │ └── viewmodels.py # Typed view models
248
- │ ├── pages/ # One module per workspace page
249
- │ │ ├── convert.py
250
- │ │ ├── nl2sql.py
251
- │ │ ├── diff.py
252
- │ │ ├── lineage.py
253
- │ │ ├── runtime.py
254
- │ │ ├── functions.py
255
- │ │ ├── types.py
256
- │ │ ├── templates.py
257
- │ │ ├── history.py
258
- │ │ ├── settings.py
259
- │ │ └── query_analysis.py
260
- │ ├── ui/ # Reusable UI widgets
261
- │ │ ├── command_palette.py
262
- │ │ ├── diff.py
263
- │ │ ├── editor.py
264
- │ │ └── status.py
265
- │ ├── app_context_v2.py # Shared application context (v2)
266
- │ └── templates_v2.py # SQL templates (v2)
267
243
  ├── examples/
268
244
  │ ├── api_client.py # API usage examples
269
245
  │ └── basic_usage.py # SDK usage examples
270
246
  ├── scripts/
271
247
  │ └── verify_readme_consistency.py # README source-of-truth checks
272
- ├── sdm_local_v2.py # Streamlit Application Entry (v2)
248
+ ├── frontend-react/ # React + TypeScript product frontend
249
+ │ ├── src/
250
+ │ ├── package.json
251
+ │ └── vite.config.ts
273
252
  ├── pyproject.toml # Project configuration
274
- ├── requirements.txt # Legacy/runtime dependency list
275
253
  ├── .env.example # Environment template
276
254
  ├── LICENSE # MIT License
277
255
  └── README.md
@@ -12,59 +12,135 @@ Exact counts are derived from the canonical data files at runtime —
12
12
  see backend.core.metadata — and must not be restated here.
13
13
  """
14
14
  import asyncio
15
+ import atexit
16
+ import copy
15
17
  import logging
16
18
  import time
17
19
  from contextlib import asynccontextmanager
20
+ from dataclasses import asdict
18
21
  from datetime import datetime
22
+ from functools import lru_cache
23
+ from threading import Lock
19
24
  from typing import Any, Dict, List, Optional
20
25
 
21
26
  from fastapi import Depends, FastAPI, HTTPException, Query, Request
22
27
  from fastapi.middleware.cors import CORSMiddleware
28
+ from fastapi.middleware.gzip import GZipMiddleware
23
29
  from fastapi.responses import JSONResponse
24
30
  from pydantic import BaseModel, ConfigDict, Field
25
31
 
26
- from backend.core.config import get_dialect_api_info, settings, setup_logging
32
+ from backend.api.auth import require_api_access
33
+ from backend.api.rate_limit_store import InMemoryRateLimitStore
34
+ from backend.api.readiness import health_probe_response
35
+ from backend.api.workers import BoundedWorkerPool, WorkerCapacityError
27
36
  from backend.core import metadata as meta
37
+ from backend.core.config import get_dialect_api_info, settings, setup_logging
28
38
  from backend.core.functions_lookup import FunctionEncyclopedia
29
39
  from backend.core.nl2sql import NL2SQLGenerator
30
40
  from backend.core.nl2sql_pipeline.orchestrator import SAFE_TABLE_HINT
31
41
  from backend.core.parser import SUPPORTED_DIALECTS, SQLParser
42
+ from backend.core.semantic_diff import diff_sql_ast
32
43
  from backend.core.transpiler import SQLTranspiler
33
44
  from backend.core.type_mapping import TypeMapper
34
45
 
35
- from backend.api.readiness import health_probe_response
36
- from backend.api.auth import require_api_access
37
- from backend.api.workers import BoundedWorkerPool, WorkerCapacityError
38
- from backend.api.rate_limit_store import InMemoryRateLimitStore
39
-
40
46
  # Configure logging for the application from the validated settings object.
41
47
  # Keep an INFO fallback for defensive compatibility with custom settings
42
48
  # objects, while normal AppSettings values are resolved from SDM_LOG_LEVEL.
43
49
  _log_level = getattr(logging, settings.log_level.upper(), logging.INFO)
44
50
  logger = setup_logging(level=_log_level, log_file=settings.log_file or None)
45
51
  api_logger = logging.getLogger(__name__)
46
- _worker_pool = BoundedWorkerPool(settings.api_workers, settings.api_queue_size)
52
+ _worker_pool: BoundedWorkerPool | None = None
53
+ _worker_pool_lock = Lock()
54
+ _worker_pool_exit_hook = None
55
+
56
+
57
+ def _get_worker_pool() -> BoundedWorkerPool:
58
+ """Return the process-wide pool, creating it lazily for non-ASGI callers.
59
+
60
+ ``app`` is intentionally resolved when this helper is called, after the
61
+ module-level FastAPI application has been initialized. This keeps import
62
+ time free of executor creation while preserving the ASGI/non-ASGI split.
63
+ """
64
+ state_pool = getattr(app.state, "worker_pool", None)
65
+ if state_pool is not None:
66
+ return state_pool
67
+
68
+ global _worker_pool, _worker_pool_exit_hook
69
+ with _worker_pool_lock:
70
+ if _worker_pool is None:
71
+ _worker_pool = BoundedWorkerPool(settings.api_workers, settings.api_queue_size)
72
+ # Direct callers (benchmarks/tests/scripts) do not enter the ASGI
73
+ # lifespan, so give their lazily-created pool a process shutdown
74
+ # hook instead of leaving executor threads alive indefinitely.
75
+ _worker_pool_exit_hook = _worker_pool.shutdown
76
+ atexit.register(_worker_pool_exit_hook)
77
+ return _worker_pool
78
+
79
+
80
+ def shutdown_worker_pool() -> None:
81
+ """Explicitly release a lazily-created non-ASGI worker pool.
82
+
83
+ Scripts and benchmarks should call this when their work is complete. The
84
+ atexit hook is retained only as a final safety net and is unregistered
85
+ before shutdown, so explicit and process-exit cleanup cannot run twice.
86
+ """
87
+ global _worker_pool, _worker_pool_exit_hook
88
+ with _worker_pool_lock:
89
+ pool = _worker_pool
90
+ hook = _worker_pool_exit_hook
91
+ _worker_pool = None
92
+ _worker_pool_exit_hook = None
93
+ if hook is not None:
94
+ atexit.unregister(hook)
95
+ if pool is not None:
96
+ pool.shutdown()
97
+
98
+
99
+ @lru_cache(maxsize=len(SUPPORTED_DIALECTS))
100
+ def _get_sql_parser(dialect: str) -> SQLParser:
101
+ """Return the immutable parser shell shared by parse requests.
102
+
103
+ ``SQLParser`` stores only the normalized dialect and stateless extraction
104
+ helpers. Reusing one instance per dialect avoids constructing the same
105
+ object for every request while keeping request-specific AST/results local
106
+ to the worker that executes them.
107
+ """
108
+ return SQLParser(dialect)
109
+
110
+
111
+ @lru_cache(maxsize=256)
112
+ def _parse_elements_cached(dialect: str, sql: str) -> dict[str, Any]:
113
+ """Cache bounded parser extraction results for repeated API queries."""
114
+ return _get_sql_parser(dialect).extract_elements(sql)
47
115
 
48
116
 
49
117
  @asynccontextmanager
50
118
  async def lifespan(application: FastAPI):
51
119
  """Manage the service worker pool for a normal ASGI startup/shutdown."""
52
- pool = BoundedWorkerPool(settings.api_workers, settings.api_queue_size)
120
+ pool = _get_worker_pool()
53
121
  application.state.worker_pool = pool
54
122
  try:
55
123
  yield
56
124
  finally:
57
- pool.shutdown()
58
- del application.state.worker_pool
125
+ if _worker_pool is pool:
126
+ shutdown_worker_pool()
127
+ else:
128
+ pool.shutdown()
129
+ if getattr(application.state, "worker_pool", None) is pool:
130
+ del application.state.worker_pool
59
131
 
60
132
 
61
133
  async def _run_cpu_bound(func, *args):
62
134
  """Run parser/transpiler work off-loop with the configured timeout."""
63
135
  try:
64
- pool = getattr(app.state, "worker_pool", _worker_pool)
136
+ pool = _get_worker_pool()
65
137
  return await pool.run(func, *args, timeout=settings.request_timeout)
66
138
  except WorkerCapacityError as exc:
67
- raise HTTPException(status_code=503, detail=str(exc), headers={"Retry-After": "1"}) from exc
139
+ raise HTTPException(
140
+ status_code=503,
141
+ detail=str(exc),
142
+ headers={"Retry-After": "1"},
143
+ ) from exc
68
144
  except asyncio.TimeoutError as exc:
69
145
  raise HTTPException(
70
146
  status_code=504,
@@ -174,9 +250,28 @@ app.add_middleware(
174
250
  allow_origins=ALLOWED_ORIGINS,
175
251
  allow_credentials=True,
176
252
  allow_methods=["GET", "POST", "OPTIONS"], # Restrict to needed methods
177
- allow_headers=["Content-Type", "Authorization", "X-API-Key", "X-Request-ID"],
253
+ # Keep preflight explicit so browser clients can send the correlation and
254
+ # API-key headers used by the React request layer. Expose operational
255
+ # response headers as well: without this list browsers silently hide
256
+ # Retry-After and request timing/correlation metadata from JavaScript even
257
+ # though the server emits them correctly.
258
+ allow_headers=["Accept", "Content-Type", "Authorization", "X-API-Key", "X-Request-ID"],
259
+ expose_headers=[
260
+ "Retry-After",
261
+ "X-API-Version",
262
+ "X-Process-Time",
263
+ "X-RateLimit-Limit",
264
+ "X-RateLimit-Remaining",
265
+ "X-RateLimit-Reset",
266
+ "X-Request-ID",
267
+ ],
178
268
  )
179
269
 
270
+ # Type/function matrices and conversion reports are often much larger than
271
+ # the request itself. Compress them at the ASGI boundary so browser clients
272
+ # spend less time waiting on the network without changing response schemas.
273
+ app.add_middleware(GZipMiddleware, minimum_size=1024, compresslevel=5)
274
+
180
275
  # Add security headers
181
276
  app.add_middleware(SecurityHeadersMiddleware)
182
277
 
@@ -204,9 +299,12 @@ nl2sql_generator = NL2SQLGenerator()
204
299
  # Request timing middleware with logging
205
300
  @app.middleware("http")
206
301
  async def add_process_time_header(request: Request, call_next):
207
- start_time = time.time()
302
+ # ``perf_counter`` is monotonic and high resolution. Wall-clock time can
303
+ # jump when NTP adjusts the host clock, which would otherwise produce a
304
+ # negative or wildly inflated duration header and log entry.
305
+ start_time = time.perf_counter()
208
306
  response = await call_next(request)
209
- process_time = time.time() - start_time
307
+ process_time = time.perf_counter() - start_time
210
308
  response.headers["X-Process-Time"] = f"{process_time:.4f}s"
211
309
  response.headers["X-API-Version"] = API_VERSION
212
310
 
@@ -323,6 +421,15 @@ class ParseRequest(BaseModel):
323
421
  sql: str = Field(..., description="SQL statement to parse")
324
422
  dialect: str = Field("hive", description="SQL dialect")
325
423
 
424
+
425
+ class DiffRequest(BaseModel):
426
+ """Semantic SQL comparison request."""
427
+
428
+ source_sql: str = Field(..., description="Source SQL statement")
429
+ target_sql: str = Field(..., description="Target SQL statement")
430
+ source_dialect: str = Field(..., description="Source SQL dialect")
431
+ target_dialect: str = Field(..., description="Target SQL dialect")
432
+
326
433
  class TypeMapRequest(BaseModel):
327
434
  """Type mapping request model."""
328
435
  type_name: str = Field(..., description="Source data type name")
@@ -487,8 +594,8 @@ async def parse_sql(request: ParseRequest):
487
594
  Extracts tables, columns, functions, and other elements from SQL.
488
595
  """
489
596
  dialect = normalize_dialect(request.dialect)
490
- parser = SQLParser(dialect)
491
- elements = await _run_cpu_bound(parser.extract_elements, request.sql)
597
+ elements = await _run_cpu_bound(_parse_elements_cached, dialect, request.sql)
598
+ elements = copy.deepcopy(elements)
492
599
  if "error" in elements:
493
600
  raise HTTPException(status_code=422, detail=elements["error"] or "SQL parsing failed")
494
601
  return {
@@ -498,6 +605,25 @@ async def parse_sql(request: ParseRequest):
498
605
  "timestamp": datetime.now().isoformat()
499
606
  }
500
607
 
608
+
609
+ @app.post("/api/diff", tags=["conversion"])
610
+ async def compare_semantic_diff(request: DiffRequest):
611
+ """Compare source and target SQL without executing either statement."""
612
+ source_dialect = normalize_dialect(request.source_dialect, "source dialect")
613
+ target_dialect = normalize_dialect(request.target_dialect, "target dialect")
614
+ result = await _run_cpu_bound(
615
+ diff_sql_ast,
616
+ request.source_sql,
617
+ request.target_sql,
618
+ source_dialect,
619
+ target_dialect,
620
+ )
621
+ payload = asdict(result)
622
+ payload["success"] = True
623
+ if result.parse_error:
624
+ payload["error"] = result.parse_error
625
+ return payload
626
+
501
627
  @app.get("/api/functions", tags=["functions"])
502
628
  async def list_functions(
503
629
  search: Optional[str] = Query(None, description="Search term for function name"),
@@ -523,7 +649,10 @@ async def list_functions(
523
649
  + ", ".join(func_encyclopedia.CATEGORIES)),
524
650
  )
525
651
  if search:
526
- functions = func_encyclopedia.search(search, limit)
652
+ # The search cache applies the category predicate before ranking and
653
+ # the public limit, so category-filtered queries do not scan or copy
654
+ # unrelated function records.
655
+ functions = func_encyclopedia.search(search, limit, normalized_category)
527
656
  elif category:
528
657
  functions = func_encyclopedia.list_by_category(normalized_category)[:limit]
529
658
  else:
@@ -594,9 +723,13 @@ async def list_types(
594
723
  if target is not None:
595
724
  target = normalize_dialect(target, "target dialect")
596
725
  matrix = type_mapper.get_matrix(source, target)
726
+ mappings = [
727
+ {"type": type_name, **mapping}
728
+ for type_name, mapping in matrix.items()
729
+ ] if isinstance(matrix, dict) else list(matrix)
597
730
  return {
598
731
  "success": True,
599
- "mappings": matrix,
732
+ "mappings": mappings,
600
733
  "dialects": type_mapper.DIALECTS,
601
734
  "type_count": len(matrix),
602
735
  "precision_warnings": type_mapper.get_precision_warnings()
@@ -747,13 +880,17 @@ async def get_stats():
747
880
  # Custom exception handler for better error responses
748
881
  @app.exception_handler(HTTPException)
749
882
  async def custom_http_exception_handler(request: Request, exc: HTTPException):
883
+ error_code_by_status = {
884
+ 503: ErrorCode.SERVICE_UNAVAILABLE.value,
885
+ 504: ErrorCode.REQUEST_TIMEOUT.value,
886
+ }
750
887
  return JSONResponse(
751
888
  status_code=exc.status_code,
752
889
  headers=exc.headers,
753
890
  content={
754
891
  "success": False,
755
892
  "error": {
756
- "code": exc.status_code,
893
+ "code": error_code_by_status.get(exc.status_code, exc.status_code),
757
894
  "message": exc.detail,
758
895
  "type": "HTTPException"
759
896
  },
@@ -3,6 +3,7 @@
3
3
 
4
4
  Provides rate limiting, request-size protection, logging, and security headers.
5
5
  """
6
+ import gzip
6
7
  import json
7
8
  import logging
8
9
  import time
@@ -108,13 +109,22 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
108
109
  request.url.path in {"/health", "/", "/ready", "/health/deep"}
109
110
  and request.method in {"GET", "HEAD"}
110
111
  )
112
+ # Browser CORS preflight requests carry no business payload and are
113
+ # generated automatically before the actual API call. Counting them
114
+ # against the application rate limit can make an otherwise healthy
115
+ # client fail before it reaches the endpoint, especially when each
116
+ # request uses a non-simple X-Request-ID or X-API-Key header.
117
+ is_cors_preflight = request.method == "OPTIONS" and bool(
118
+ request.headers.get("origin")
119
+ and request.headers.get("access-control-request-method")
120
+ )
111
121
 
112
122
  # Apply rate limiting before JSON/body parsing. This prevents a
113
123
  # client from spending unbounded CPU on invalid payloads while still
114
124
  # keeping the request-size guard active when rate limiting is disabled.
115
125
  rate_limit_checked = False
116
126
  remaining = reset_time = None
117
- if self.enabled and not is_health_probe:
127
+ if self.enabled and not is_health_probe and not is_cors_preflight:
118
128
  client_id = self._get_client_id(request)
119
129
  # Redis checks use a synchronous client; a stalled store must not
120
130
  # block the event loop used by unrelated liveness/API requests.
@@ -173,11 +183,18 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
173
183
 
174
184
  @staticmethod
175
185
  def _get_request_id(request: Request) -> str:
176
- """Reuse a valid RFC 4122 request ID; otherwise issue a fresh UUID."""
186
+ """Reuse a valid request ID; otherwise issue a fresh UUID.
187
+
188
+ The React client prefixes UUIDs with ``web-`` to identify browser
189
+ traffic. Keep accepting the prefix while still validating the UUID
190
+ payload so arbitrary header values cannot be copied into logs or
191
+ response headers.
192
+ """
177
193
  candidate = request.headers.get("X-Request-ID")
178
194
  if candidate:
195
+ uuid_candidate = candidate.removeprefix("web-")
179
196
  try:
180
- parsed = uuid.UUID(candidate)
197
+ parsed = uuid.UUID(uuid_candidate)
181
198
  if parsed.variant == uuid.RFC_4122:
182
199
  return candidate
183
200
  except (ValueError, AttributeError, TypeError):
@@ -203,6 +220,13 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
203
220
  if "json" not in response.headers.get("content-type", "").lower():
204
221
  return response
205
222
 
223
+ # BaseHTTPMiddleware often returns a streaming response even when the
224
+ # endpoint produced JSON. Buffer that stream exactly once for
225
+ # normalization, but replay the original bytes when the payload is
226
+ # malformed so an error response is never silently emptied.
227
+ encoded_with_gzip = "gzip" in response.headers.get("content-encoding", "").lower()
228
+ streamed_body = not hasattr(response, "body")
229
+ raw_body = b""
206
230
  try:
207
231
  if hasattr(response, "body"):
208
232
  raw_body = response.body
@@ -211,9 +235,44 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
211
235
  async for chunk in response.body_iterator:
212
236
  chunks.append(chunk)
213
237
  raw_body = b"".join(chunks)
214
- payload = json.loads(raw_body.decode("utf-8"))
215
- except (AttributeError, UnicodeDecodeError, json.JSONDecodeError):
238
+ # GZipMiddleware is inside this middleware. Error responses over
239
+ # its threshold therefore arrive compressed; decode them before
240
+ # applying the stable JSON envelope so large and small errors have
241
+ # identical browser-visible contracts.
242
+ if encoded_with_gzip:
243
+ raw_body = gzip.decompress(raw_body)
244
+ except (AttributeError, TypeError):
216
245
  return response
246
+ except (OSError, EOFError):
247
+ # A malformed compressed stream must remain observable to the
248
+ # caller. If it was buffered from a streaming response, replay
249
+ # the original bytes instead of returning an already-consumed
250
+ # iterator.
251
+ if not streamed_body:
252
+ return response
253
+ headers = dict(response.headers)
254
+ headers.pop("content-length", None)
255
+ return Response(
256
+ content=raw_body,
257
+ status_code=response.status_code,
258
+ headers=headers,
259
+ background=response.background,
260
+ )
261
+ try:
262
+ payload = json.loads(raw_body.decode("utf-8"))
263
+ except (AttributeError, TypeError, UnicodeDecodeError, json.JSONDecodeError, OSError):
264
+ if hasattr(response, "body"):
265
+ return response
266
+ headers = dict(response.headers)
267
+ headers.pop("content-length", None)
268
+ if encoded_with_gzip:
269
+ headers.pop("content-encoding", None)
270
+ return Response(
271
+ content=raw_body,
272
+ status_code=response.status_code,
273
+ headers=headers,
274
+ background=response.background,
275
+ )
217
276
 
218
277
  if response.status_code == 503 and isinstance(payload, dict) and "checks" in payload:
219
278
  # Readiness has a public diagnostics schema of its own. Preserve
@@ -222,6 +281,7 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
222
281
  headers = dict(response.headers)
223
282
  headers.pop("content-length", None)
224
283
  headers.pop("content-type", None)
284
+ headers.pop("content-encoding", None)
225
285
  payload.setdefault("request_id", request_id)
226
286
  return JSONResponse(status_code=503, content=payload, headers=headers)
227
287
 
@@ -274,10 +334,14 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
274
334
  headers = dict(response.headers)
275
335
  headers.pop("content-length", None)
276
336
  headers.pop("content-type", None)
337
+ headers.pop("content-encoding", None)
277
338
  return JSONResponse(status_code=response.status_code, content=normalized, headers=headers)
278
339
 
279
340
  async def dispatch(self, request: Request, call_next) -> Response:
280
- start_time = time.time()
341
+ # Request duration is an elapsed interval, so use a monotonic clock.
342
+ # ``time.time()`` may move backwards/forwards during a wall-clock
343
+ # correction and corrupt both logs and latency metrics.
344
+ start_time = time.perf_counter()
281
345
  request_id = self._get_request_id(request)
282
346
  request.state.request_id = request_id
283
347
  status_code = 500
@@ -292,7 +356,7 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
292
356
  error = _sanitize_log_value(str(exc))
293
357
  raise
294
358
  finally:
295
- process_time = time.time() - start_time
359
+ process_time = time.perf_counter() - start_time
296
360
  log_data = {"event": "request_end", "request_id": request_id, "method": _sanitize_log_value(request.method), "path": _sanitize_log_value(request.url.path), "status_code": status_code, "process_time_ms": round(process_time * 1000, 2), "error": error, "timestamp": datetime.now().isoformat()}
297
361
  (logger.warning if status_code >= 400 else logger.info)(json.dumps(log_data))
298
362
  response.headers["X-Request-ID"] = request_id