sql-dialect-master 1.1.3__tar.gz → 1.1.4__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.
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/PKG-INFO +21 -5
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/README.md +16 -4
- sql_dialect_master-1.1.4/backend/api/auth.py +29 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/main.py +58 -15
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/middleware.py +21 -3
- sql_dialect_master-1.1.4/backend/api/workers.py +44 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/config.py +6 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/exceptions.py +3 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/functions_lookup.py +9 -5
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/metadata.py +2 -2
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/core_extract.py +31 -8
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/mappings.py +2 -2
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/dialect.py +3 -1
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/models.py +3 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/orchestrator.py +155 -20
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/validation.py +4 -3
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/p1_sql_scanner.py +26 -1
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/parser.py +3 -50
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/semantic_diff.py +3 -1
- sql_dialect_master-1.1.4/backend/core/sql_statements.py +30 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/transpiler.py +55 -12
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/type_mapping.py +8 -6
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/utils/validators.py +27 -41
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/navigation.py +37 -5
- sql_dialect_master-1.1.4/frontend/core/sql_inputs.py +6 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/state.py +46 -11
- sql_dialect_master-1.1.4/frontend/core/statuses.py +34 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/viewmodels.py +3 -20
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/convert.py +2 -3
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/diff.py +18 -3
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/history.py +6 -5
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/nl2sql.py +19 -7
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/diff.py +2 -8
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/editor.py +5 -1
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/status.py +8 -11
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/pyproject.toml +5 -1
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/PKG-INFO +21 -5
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/SOURCES.txt +4 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/requires.txt +4 -0
- sql_dialect_master-1.1.3/frontend/core/sql_inputs.py +0 -29
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/LICENSE +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/rate_limit_store.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/readiness.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/cache.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/column_hint_validation.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/constants.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/function_call_scanner.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/function_semantics.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/functions_db.json +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/aggregations.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/evidence.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/relations.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/templates.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/tokenizer.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_legacy.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/intent.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/ir.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/predicates.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/renderer.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/resolution.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/templates.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/p2_data_validation.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/post_processor.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/rules.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/type_mapping.json +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/frontend_entrypoint.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/utils/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/app_context_v2.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/design_tokens.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/escaping.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/fingerprints.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/styles.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/themes.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/type_status.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/functions.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/lineage.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/query_analysis.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/runtime.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/settings.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/templates.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/types.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/templates_v2.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/__init__.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/command_palette.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sdm_local_v2.py +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/setup.cfg +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/entry_points.txt +0 -0
- {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-dialect-master
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: SQL Dialect Master - Multi-database SQL conversion engine
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -21,6 +21,10 @@ Provides-Extra: qa
|
|
|
21
21
|
Requires-Dist: ruff>=0.6; extra == "qa"
|
|
22
22
|
Requires-Dist: bandit>=1.7; extra == "qa"
|
|
23
23
|
Requires-Dist: pip-audit>=2.7; extra == "qa"
|
|
24
|
+
Requires-Dist: build>=1.2.2; extra == "qa"
|
|
25
|
+
Requires-Dist: twine>=5.1; extra == "qa"
|
|
26
|
+
Requires-Dist: setuptools>=75.0.0; extra == "qa"
|
|
27
|
+
Requires-Dist: wheel>=0.45; extra == "qa"
|
|
24
28
|
Provides-Extra: semantic
|
|
25
29
|
Requires-Dist: duckdb>=1.5.5; extra == "semantic"
|
|
26
30
|
Requires-Dist: psycopg[binary]>=3.3.5; extra == "semantic"
|
|
@@ -32,7 +36,7 @@ Dynamic: license-file
|
|
|
32
36
|
|
|
33
37
|
Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
|
|
34
38
|
|
|
35
|
-
Current release: 1.1.
|
|
39
|
+
Current release: 1.1.4
|
|
36
40
|
|
|
37
41
|
[](https://www.python.org/downloads/)
|
|
38
42
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -78,16 +82,18 @@ cd sql-dialect-master
|
|
|
78
82
|
python -m pip install -e .
|
|
79
83
|
```
|
|
80
84
|
|
|
81
|
-
For development and
|
|
85
|
+
For development, testing, quality checks, and local builds, use the project's
|
|
86
|
+
Python environment and install the optional tooling:
|
|
82
87
|
|
|
83
88
|
```bash
|
|
84
|
-
python -m
|
|
89
|
+
python -m venv .venv
|
|
90
|
+
.venv/bin/python -m pip install -e ".[dev,qa]"
|
|
85
91
|
```
|
|
86
92
|
|
|
87
93
|
For database-backed semantic tests:
|
|
88
94
|
|
|
89
95
|
```bash
|
|
90
|
-
python -m pip install -e ".[dev,semantic]"
|
|
96
|
+
.venv/bin/python -m pip install -e ".[dev,semantic]"
|
|
91
97
|
```
|
|
92
98
|
|
|
93
99
|
### Run Streamlit UI (Recommended)
|
|
@@ -113,6 +119,16 @@ uvicorn backend.api.main:app --reload
|
|
|
113
119
|
|
|
114
120
|
API docs available at http://localhost:8000/docs
|
|
115
121
|
|
|
122
|
+
Set `SDM_API_KEY` in `.env` to require credentials for `/api/*` endpoints.
|
|
123
|
+
Clients may send `X-API-Key` or `Authorization: Bearer <key>`. Health probes
|
|
124
|
+
use the separate `SDM_HEALTH_PROBE_TOKEN` setting. An empty API key preserves
|
|
125
|
+
local access.
|
|
126
|
+
|
|
127
|
+
`SDM_API_WORKERS` (default 4) and `SDM_API_QUEUE_SIZE` (default 4) bound CPU
|
|
128
|
+
work. When all slots are occupied, requests receive HTTP 503 and a
|
|
129
|
+
`Retry-After` header. `SDM_REQUEST_TIMEOUT` returns HTTP 504 when a request
|
|
130
|
+
times out; running threads retain their capacity until they finish.
|
|
131
|
+
|
|
116
132
|
## 📖 Usage Examples
|
|
117
133
|
|
|
118
134
|
### Python SDK
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
|
|
4
4
|
|
|
5
|
-
Current release: 1.1.
|
|
5
|
+
Current release: 1.1.4
|
|
6
6
|
|
|
7
7
|
[](https://www.python.org/downloads/)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -48,16 +48,18 @@ cd sql-dialect-master
|
|
|
48
48
|
python -m pip install -e .
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
For development and
|
|
51
|
+
For development, testing, quality checks, and local builds, use the project's
|
|
52
|
+
Python environment and install the optional tooling:
|
|
52
53
|
|
|
53
54
|
```bash
|
|
54
|
-
python -m
|
|
55
|
+
python -m venv .venv
|
|
56
|
+
.venv/bin/python -m pip install -e ".[dev,qa]"
|
|
55
57
|
```
|
|
56
58
|
|
|
57
59
|
For database-backed semantic tests:
|
|
58
60
|
|
|
59
61
|
```bash
|
|
60
|
-
python -m pip install -e ".[dev,semantic]"
|
|
62
|
+
.venv/bin/python -m pip install -e ".[dev,semantic]"
|
|
61
63
|
```
|
|
62
64
|
|
|
63
65
|
### Run Streamlit UI (Recommended)
|
|
@@ -83,6 +85,16 @@ uvicorn backend.api.main:app --reload
|
|
|
83
85
|
|
|
84
86
|
API docs available at http://localhost:8000/docs
|
|
85
87
|
|
|
88
|
+
Set `SDM_API_KEY` in `.env` to require credentials for `/api/*` endpoints.
|
|
89
|
+
Clients may send `X-API-Key` or `Authorization: Bearer <key>`. Health probes
|
|
90
|
+
use the separate `SDM_HEALTH_PROBE_TOKEN` setting. An empty API key preserves
|
|
91
|
+
local access.
|
|
92
|
+
|
|
93
|
+
`SDM_API_WORKERS` (default 4) and `SDM_API_QUEUE_SIZE` (default 4) bound CPU
|
|
94
|
+
work. When all slots are occupied, requests receive HTTP 503 and a
|
|
95
|
+
`Retry-After` header. `SDM_REQUEST_TIMEOUT` returns HTTP 504 when a request
|
|
96
|
+
times out; running threads retain their capacity until they finish.
|
|
97
|
+
|
|
86
98
|
## 📖 Usage Examples
|
|
87
99
|
|
|
88
100
|
### Python SDK
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Optional shared API-key protection for business endpoints."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import secrets
|
|
5
|
+
|
|
6
|
+
from fastapi import HTTPException, Request
|
|
7
|
+
|
|
8
|
+
from backend.core.config import settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def require_api_access(request: Request) -> None:
|
|
12
|
+
"""Require a configured key for /api endpoints while keeping probes separate."""
|
|
13
|
+
key = settings.api_key
|
|
14
|
+
if key is None or not request.url.path.startswith("/api/"):
|
|
15
|
+
return
|
|
16
|
+
expected = key.get_secret_value()
|
|
17
|
+
if not expected:
|
|
18
|
+
return
|
|
19
|
+
supplied = request.headers.get("X-API-Key", "")
|
|
20
|
+
if not supplied:
|
|
21
|
+
scheme, _, credential = request.headers.get("Authorization", "").partition(" ")
|
|
22
|
+
if scheme.lower() == "bearer":
|
|
23
|
+
supplied = credential
|
|
24
|
+
if not supplied or not secrets.compare_digest(supplied.encode("utf-8"), expected.encode("utf-8")):
|
|
25
|
+
raise HTTPException(
|
|
26
|
+
status_code=401,
|
|
27
|
+
detail="Invalid or missing API key",
|
|
28
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
29
|
+
)
|
|
@@ -14,13 +14,13 @@ see backend.core.metadata — and must not be restated here.
|
|
|
14
14
|
import asyncio
|
|
15
15
|
import logging
|
|
16
16
|
import time
|
|
17
|
+
from contextlib import asynccontextmanager
|
|
17
18
|
from datetime import datetime
|
|
18
19
|
from typing import Any, Dict, List, Optional
|
|
19
20
|
|
|
20
|
-
from fastapi import FastAPI, HTTPException, Query, Request
|
|
21
|
+
from fastapi import Depends, FastAPI, HTTPException, Query, Request
|
|
21
22
|
from fastapi.middleware.cors import CORSMiddleware
|
|
22
23
|
from fastapi.responses import JSONResponse
|
|
23
|
-
from starlette.concurrency import run_in_threadpool
|
|
24
24
|
from pydantic import BaseModel, ConfigDict, Field
|
|
25
25
|
|
|
26
26
|
from backend.core.config import get_dialect_api_info, settings, setup_logging
|
|
@@ -33,6 +33,9 @@ from backend.core.transpiler import SQLTranspiler
|
|
|
33
33
|
from backend.core.type_mapping import TypeMapper
|
|
34
34
|
|
|
35
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
|
|
36
39
|
|
|
37
40
|
# Configure logging for the application from the validated settings object.
|
|
38
41
|
# Keep an INFO fallback for defensive compatibility with custom settings
|
|
@@ -40,15 +43,28 @@ from backend.api.readiness import health_probe_response
|
|
|
40
43
|
_log_level = getattr(logging, settings.log_level.upper(), logging.INFO)
|
|
41
44
|
logger = setup_logging(level=_log_level, log_file=settings.log_file or None)
|
|
42
45
|
api_logger = logging.getLogger(__name__)
|
|
46
|
+
_worker_pool = BoundedWorkerPool(settings.api_workers, settings.api_queue_size)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@asynccontextmanager
|
|
50
|
+
async def lifespan(application: FastAPI):
|
|
51
|
+
"""Manage the service worker pool for a normal ASGI startup/shutdown."""
|
|
52
|
+
pool = BoundedWorkerPool(settings.api_workers, settings.api_queue_size)
|
|
53
|
+
application.state.worker_pool = pool
|
|
54
|
+
try:
|
|
55
|
+
yield
|
|
56
|
+
finally:
|
|
57
|
+
pool.shutdown()
|
|
58
|
+
del application.state.worker_pool
|
|
43
59
|
|
|
44
60
|
|
|
45
61
|
async def _run_cpu_bound(func, *args):
|
|
46
62
|
"""Run parser/transpiler work off-loop with the configured timeout."""
|
|
47
63
|
try:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
64
|
+
pool = getattr(app.state, "worker_pool", _worker_pool)
|
|
65
|
+
return await pool.run(func, *args, timeout=settings.request_timeout)
|
|
66
|
+
except WorkerCapacityError as exc:
|
|
67
|
+
raise HTTPException(status_code=503, detail=str(exc), headers={"Retry-After": "1"}) from exc
|
|
52
68
|
except asyncio.TimeoutError as exc:
|
|
53
69
|
raise HTTPException(
|
|
54
70
|
status_code=504,
|
|
@@ -105,6 +121,8 @@ print(response.json()["target_sql"])
|
|
|
105
121
|
|
|
106
122
|
# Initialize FastAPI app with enhanced metadata
|
|
107
123
|
app = FastAPI(
|
|
124
|
+
dependencies=[Depends(require_api_access)],
|
|
125
|
+
lifespan=lifespan,
|
|
108
126
|
title=API_TITLE,
|
|
109
127
|
description=API_DESCRIPTION,
|
|
110
128
|
version=API_VERSION,
|
|
@@ -156,7 +174,7 @@ app.add_middleware(
|
|
|
156
174
|
allow_origins=ALLOWED_ORIGINS,
|
|
157
175
|
allow_credentials=True,
|
|
158
176
|
allow_methods=["GET", "POST", "OPTIONS"], # Restrict to needed methods
|
|
159
|
-
allow_headers=["Content-Type", "Authorization", "X-Request-ID"],
|
|
177
|
+
allow_headers=["Content-Type", "Authorization", "X-API-Key", "X-Request-ID"],
|
|
160
178
|
)
|
|
161
179
|
|
|
162
180
|
# Add security headers
|
|
@@ -166,6 +184,7 @@ app.add_middleware(SecurityHeadersMiddleware)
|
|
|
166
184
|
rate_limiter = RateLimiter(
|
|
167
185
|
requests_per_window=settings.rate_limit_requests,
|
|
168
186
|
window_seconds=settings.rate_limit_window,
|
|
187
|
+
store=None if settings.rate_limit_enabled else InMemoryRateLimitStore(),
|
|
169
188
|
)
|
|
170
189
|
app.add_middleware(
|
|
171
190
|
RateLimitMiddleware,
|
|
@@ -192,9 +211,8 @@ async def add_process_time_header(request: Request, call_next):
|
|
|
192
211
|
response.headers["X-API-Version"] = API_VERSION
|
|
193
212
|
|
|
194
213
|
# Log request details
|
|
195
|
-
api_logger.info(
|
|
196
|
-
|
|
197
|
-
)
|
|
214
|
+
api_logger.info("%s %r - %s - %.4fs", request.method, request.url.path,
|
|
215
|
+
response.status_code, process_time)
|
|
198
216
|
return response
|
|
199
217
|
|
|
200
218
|
# Dialect info for enhanced responses (from centralized config)
|
|
@@ -285,6 +303,20 @@ class NL2SQLResponse(BaseModel):
|
|
|
285
303
|
explanation: str = Field("", description="Explanation of generated SQL")
|
|
286
304
|
confidence: float = Field(0.0, description="Confidence score (0.0-1.0)")
|
|
287
305
|
suggestions: List[str] = Field(default_factory=list, description="Improvement suggestions")
|
|
306
|
+
parameter_style: Optional[str] = Field(
|
|
307
|
+
None,
|
|
308
|
+
description=(
|
|
309
|
+
"Placeholder style for generated DML: numeric ($1 in PostgreSQL/Redshift, "
|
|
310
|
+
":1 in Oracle), named (@p1 in T-SQL), format (%s in MySQL), or qmark (?)"
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
parameters: List[str] = Field(
|
|
314
|
+
default_factory=list,
|
|
315
|
+
description="Ordered placeholders that callers must bind for generated DML",
|
|
316
|
+
)
|
|
317
|
+
evidence: Optional[Dict[str, Any]] = Field(
|
|
318
|
+
None, description="Structured evidence supporting the confidence score",
|
|
319
|
+
)
|
|
288
320
|
|
|
289
321
|
class ParseRequest(BaseModel):
|
|
290
322
|
"""SQL parse request model."""
|
|
@@ -456,10 +488,9 @@ async def parse_sql(request: ParseRequest):
|
|
|
456
488
|
"""
|
|
457
489
|
dialect = normalize_dialect(request.dialect)
|
|
458
490
|
parser = SQLParser(dialect)
|
|
459
|
-
parsed = await _run_cpu_bound(parser.parse, request.sql)
|
|
460
|
-
if not parsed.success:
|
|
461
|
-
raise HTTPException(status_code=422, detail=parsed.error or "SQL parsing failed")
|
|
462
491
|
elements = await _run_cpu_bound(parser.extract_elements, request.sql)
|
|
492
|
+
if "error" in elements:
|
|
493
|
+
raise HTTPException(status_code=422, detail=elements["error"] or "SQL parsing failed")
|
|
463
494
|
return {
|
|
464
495
|
"success": True,
|
|
465
496
|
"dialect": dialect,
|
|
@@ -483,10 +514,18 @@ async def list_functions(
|
|
|
483
514
|
- `/api/functions?category=string` - List all string functions
|
|
484
515
|
- `/api/functions?limit=100` - Get first 100 functions
|
|
485
516
|
"""
|
|
517
|
+
normalized_category = category.strip().lower() if category is not None else None
|
|
518
|
+
if category is not None:
|
|
519
|
+
if normalized_category not in func_encyclopedia.CATEGORIES:
|
|
520
|
+
raise HTTPException(
|
|
521
|
+
status_code=400,
|
|
522
|
+
detail=(f"Unknown function category: {category}. Available categories: "
|
|
523
|
+
+ ", ".join(func_encyclopedia.CATEGORIES)),
|
|
524
|
+
)
|
|
486
525
|
if search:
|
|
487
526
|
functions = func_encyclopedia.search(search, limit)
|
|
488
527
|
elif category:
|
|
489
|
-
functions = func_encyclopedia.list_by_category(
|
|
528
|
+
functions = func_encyclopedia.list_by_category(normalized_category)[:limit]
|
|
490
529
|
else:
|
|
491
530
|
functions = func_encyclopedia.functions[:limit]
|
|
492
531
|
|
|
@@ -640,7 +679,10 @@ async def generate_sql(request: NL2SQLRequest):
|
|
|
640
679
|
dialect=dialect,
|
|
641
680
|
explanation=result.explanation,
|
|
642
681
|
confidence=result.confidence,
|
|
643
|
-
suggestions=list(result.suggestions[: settings.nl2sql_max_suggestions])
|
|
682
|
+
suggestions=list(result.suggestions[: settings.nl2sql_max_suggestions]),
|
|
683
|
+
parameter_style=result.parameter_style,
|
|
684
|
+
parameters=list(result.parameters),
|
|
685
|
+
evidence=result.evidence.to_dict() if result.evidence is not None else None,
|
|
644
686
|
)
|
|
645
687
|
|
|
646
688
|
@app.get("/health", tags=["system"])
|
|
@@ -707,6 +749,7 @@ async def get_stats():
|
|
|
707
749
|
async def custom_http_exception_handler(request: Request, exc: HTTPException):
|
|
708
750
|
return JSONResponse(
|
|
709
751
|
status_code=exc.status_code,
|
|
752
|
+
headers=exc.headers,
|
|
710
753
|
content={
|
|
711
754
|
"success": False,
|
|
712
755
|
"error": {
|
|
@@ -14,10 +14,11 @@ from typing import Dict
|
|
|
14
14
|
from fastapi import Request, Response
|
|
15
15
|
from fastapi.responses import JSONResponse
|
|
16
16
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
17
|
+
from starlette.concurrency import run_in_threadpool
|
|
17
18
|
|
|
18
19
|
from backend.core.exceptions import ErrorCode
|
|
19
20
|
|
|
20
|
-
from .rate_limit_store import RateLimitStore, create_rate_limit_store
|
|
21
|
+
from .rate_limit_store import InMemoryRateLimitStore, RateLimitStore, create_rate_limit_store
|
|
21
22
|
from .readiness import health_probe_response
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
@@ -69,7 +70,7 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
69
70
|
super().__init__(app)
|
|
70
71
|
if max_body_bytes <= 0:
|
|
71
72
|
raise ValueError("max_body_bytes must be positive")
|
|
72
|
-
self.limiter = limiter or RateLimiter()
|
|
73
|
+
self.limiter = limiter or RateLimiter(store=None if enabled else InMemoryRateLimitStore())
|
|
73
74
|
self.enabled = enabled
|
|
74
75
|
self.max_body_bytes = max_body_bytes
|
|
75
76
|
|
|
@@ -115,7 +116,9 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
115
116
|
remaining = reset_time = None
|
|
116
117
|
if self.enabled and not is_health_probe:
|
|
117
118
|
client_id = self._get_client_id(request)
|
|
118
|
-
|
|
119
|
+
# Redis checks use a synchronous client; a stalled store must not
|
|
120
|
+
# block the event loop used by unrelated liveness/API requests.
|
|
121
|
+
is_allowed, remaining, reset_time = await run_in_threadpool(self.limiter.is_allowed, client_id)
|
|
119
122
|
rate_limit_checked = True
|
|
120
123
|
if not is_allowed:
|
|
121
124
|
logger.warning("Rate limit exceeded for client: %s", _sanitize_log_value(client_id))
|
|
@@ -186,14 +189,19 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
186
189
|
"""Normalize standard HTTP 4xx JSON responses while preserving domain-specific codes."""
|
|
187
190
|
code_by_status = {
|
|
188
191
|
400: ErrorCode.BAD_REQUEST.value,
|
|
192
|
+
401: ErrorCode.UNAUTHORIZED.value,
|
|
189
193
|
404: ErrorCode.NOT_FOUND.value,
|
|
190
194
|
405: ErrorCode.METHOD_NOT_ALLOWED.value,
|
|
191
195
|
413: ErrorCode.PAYLOAD_TOO_LARGE.value,
|
|
192
196
|
422: ErrorCode.VALIDATION_FAILED.value,
|
|
193
197
|
429: ErrorCode.RATE_LIMITED.value,
|
|
198
|
+
503: ErrorCode.SERVICE_UNAVAILABLE.value,
|
|
199
|
+
504: ErrorCode.REQUEST_TIMEOUT.value,
|
|
194
200
|
}
|
|
195
201
|
if response.status_code not in code_by_status:
|
|
196
202
|
return response
|
|
203
|
+
if "json" not in response.headers.get("content-type", "").lower():
|
|
204
|
+
return response
|
|
197
205
|
|
|
198
206
|
try:
|
|
199
207
|
if hasattr(response, "body"):
|
|
@@ -207,6 +215,16 @@ class StructuredLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
207
215
|
except (AttributeError, UnicodeDecodeError, json.JSONDecodeError):
|
|
208
216
|
return response
|
|
209
217
|
|
|
218
|
+
if response.status_code == 503 and isinstance(payload, dict) and "checks" in payload:
|
|
219
|
+
# Readiness has a public diagnostics schema of its own. Preserve
|
|
220
|
+
# status/checks when the service is unhealthy, including after
|
|
221
|
+
# consuming the downstream streaming response to decode JSON.
|
|
222
|
+
headers = dict(response.headers)
|
|
223
|
+
headers.pop("content-length", None)
|
|
224
|
+
headers.pop("content-type", None)
|
|
225
|
+
payload.setdefault("request_id", request_id)
|
|
226
|
+
return JSONResponse(status_code=503, content=payload, headers=headers)
|
|
227
|
+
|
|
210
228
|
error = payload.get("error") if isinstance(payload, dict) else None
|
|
211
229
|
expected_code = code_by_status[response.status_code]
|
|
212
230
|
if not isinstance(error, dict):
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Bound synchronous work even when an HTTP caller times out."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
from threading import BoundedSemaphore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class WorkerCapacityError(RuntimeError):
|
|
10
|
+
"""All execution and queue slots are occupied."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BoundedWorkerPool:
|
|
14
|
+
"""Keep a slot until its underlying future finishes, including timeouts.
|
|
15
|
+
|
|
16
|
+
Cancelling an HTTP wait cannot interrupt Python threads. Holding capacity
|
|
17
|
+
until completion prevents a stream of timed-out requests from creating an
|
|
18
|
+
unbounded executor queue. Slots cover executing and queued work together.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, workers: int, queue_size: int = 0):
|
|
22
|
+
if workers <= 0 or queue_size < 0:
|
|
23
|
+
raise ValueError("workers must be positive and queue_size non-negative")
|
|
24
|
+
self._slots = BoundedSemaphore(workers + queue_size)
|
|
25
|
+
self._executor = ThreadPoolExecutor(max_workers=workers, thread_name_prefix="sdm-api")
|
|
26
|
+
|
|
27
|
+
async def run(self, function, *args, timeout: float):
|
|
28
|
+
if not self._slots.acquire(blocking=False):
|
|
29
|
+
raise WorkerCapacityError("Request processing capacity is exhausted")
|
|
30
|
+
try:
|
|
31
|
+
future = self._executor.submit(function, *args)
|
|
32
|
+
except BaseException:
|
|
33
|
+
self._slots.release()
|
|
34
|
+
raise
|
|
35
|
+
future.add_done_callback(lambda _future: self._slots.release())
|
|
36
|
+
wrapped = asyncio.wrap_future(future)
|
|
37
|
+
# A timed-out caller no longer awaits this result. Retrieve exceptions
|
|
38
|
+
# to avoid an unhandled-future warning when background work later fails.
|
|
39
|
+
wrapped.add_done_callback(lambda completed: None if completed.cancelled() else completed.exception())
|
|
40
|
+
return await asyncio.wait_for(asyncio.shield(wrapped), timeout=timeout)
|
|
41
|
+
|
|
42
|
+
def shutdown(self) -> None:
|
|
43
|
+
"""Cancel queued work; already-running calls retain their slots."""
|
|
44
|
+
self._executor.shutdown(wait=False, cancel_futures=True)
|
|
@@ -216,6 +216,9 @@ class AppSettings(BaseSettings):
|
|
|
216
216
|
# API settings
|
|
217
217
|
api_version: str = _METADATA_API_VERSION
|
|
218
218
|
api_title: str = "SQL Dialect Master API"
|
|
219
|
+
api_key: Optional[SecretStr] = None
|
|
220
|
+
api_workers: int = Field(4, gt=0)
|
|
221
|
+
api_queue_size: int = Field(4, ge=0)
|
|
219
222
|
allowed_origins: str = (
|
|
220
223
|
"http://localhost:8501,http://localhost:8000,"
|
|
221
224
|
"http://127.0.0.1:8501,http://127.0.0.1:8000"
|
|
@@ -248,6 +251,9 @@ class AppSettings(BaseSettings):
|
|
|
248
251
|
nl2sql_confidence_threshold: float = Field(0.6, ge=0.0, le=1.0)
|
|
249
252
|
nl2sql_max_suggestions: int = Field(5, ge=0)
|
|
250
253
|
nl2sql_max_input_length: int = Field(8192, gt=0)
|
|
254
|
+
# Maximum natural-language date window accepted by NL2SQL. Keeping this
|
|
255
|
+
# bounded prevents enormous interval literals from reaching renderers.
|
|
256
|
+
nl2sql_max_date_interval_days: int = Field(36525, gt=0)
|
|
251
257
|
|
|
252
258
|
# SQL Transpiler / Parser settings
|
|
253
259
|
#
|
|
@@ -13,6 +13,9 @@ class ErrorCode(str, Enum):
|
|
|
13
13
|
SECURITY_VIOLATION = "SECURITY_VIOLATION"
|
|
14
14
|
VALIDATION_FAILED = "VALIDATION_FAILED"
|
|
15
15
|
BAD_REQUEST = "BAD_REQUEST"
|
|
16
|
+
UNAUTHORIZED = "UNAUTHORIZED"
|
|
17
|
+
SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
|
|
18
|
+
REQUEST_TIMEOUT = "REQUEST_TIMEOUT"
|
|
16
19
|
NOT_FOUND = "NOT_FOUND"
|
|
17
20
|
METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED"
|
|
18
21
|
PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE"
|
|
@@ -9,6 +9,7 @@ Provides comprehensive SQL function lookup with:
|
|
|
9
9
|
- Function examples and parameter documentation
|
|
10
10
|
"""
|
|
11
11
|
import json
|
|
12
|
+
import copy
|
|
12
13
|
import logging
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
from typing import Optional, List, Dict, Any
|
|
@@ -112,11 +113,13 @@ class FunctionEncyclopedia:
|
|
|
112
113
|
Returns:
|
|
113
114
|
List of matching functions sorted by relevance
|
|
114
115
|
"""
|
|
115
|
-
|
|
116
|
+
# ``0`` is a valid explicit request for no results. Treat only
|
|
117
|
+
# ``None`` as asking for the configured default.
|
|
118
|
+
limit = settings.function_search_limit if limit is None else max(0, limit)
|
|
116
119
|
query = query.upper().strip()
|
|
117
120
|
|
|
118
121
|
if not query:
|
|
119
|
-
return self.functions[:limit]
|
|
122
|
+
return copy.deepcopy(self.functions[:limit])
|
|
120
123
|
|
|
121
124
|
results: List[tuple] = []
|
|
122
125
|
|
|
@@ -138,16 +141,17 @@ class FunctionEncyclopedia:
|
|
|
138
141
|
results.append((func, ratio))
|
|
139
142
|
|
|
140
143
|
results.sort(key=lambda x: x[1], reverse=True)
|
|
141
|
-
return [r[0] for r in results[:limit]]
|
|
144
|
+
return copy.deepcopy([r[0] for r in results[:limit]])
|
|
142
145
|
|
|
143
146
|
def get_function(self, name: str) -> Optional[FunctionInfo]:
|
|
144
147
|
"""Get detailed function information by exact name."""
|
|
145
|
-
|
|
148
|
+
result = self.name_index.get(name.upper())
|
|
149
|
+
return copy.deepcopy(result) if result is not None else None
|
|
146
150
|
|
|
147
151
|
def list_by_category(self, category: str) -> List[FunctionInfo]:
|
|
148
152
|
"""List all functions in a category."""
|
|
149
153
|
category = category.lower()
|
|
150
|
-
return self.category_index.get(category, [])
|
|
154
|
+
return copy.deepcopy(self.category_index.get(category, []))
|
|
151
155
|
|
|
152
156
|
def get_dialect_syntax(self, func_name: str, dialect: str) -> Optional[str]:
|
|
153
157
|
"""Get function syntax for specific dialect."""
|
|
@@ -17,8 +17,8 @@ Design rules:
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
# ── Release version facts (sole source of truth) ────────────────────────
|
|
20
|
-
PROJECT_VERSION = "1.1.
|
|
21
|
-
API_VERSION = "1.1.
|
|
20
|
+
PROJECT_VERSION = "1.1.4"
|
|
21
|
+
API_VERSION = "1.1.4"
|
|
22
22
|
# Frontend v2 architecture — independent of the product release version.
|
|
23
23
|
FRONTEND_ARCHITECTURE_VERSION = 2
|
|
24
24
|
|
{sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/core_extract.py
RENAMED
|
@@ -8,6 +8,8 @@ from ``backend.core.nl2sql_components.mappings``).
|
|
|
8
8
|
import re
|
|
9
9
|
from typing import Dict, List, Optional
|
|
10
10
|
|
|
11
|
+
from ..p1_sql_scanner import mask_non_executable
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
# Generated SQL is intended to be reviewed and parameterized by a caller.
|
|
13
15
|
# Refuse unbounded/absurd row counts before integer conversion (which can
|
|
@@ -15,6 +17,19 @@ from typing import Dict, List, Optional
|
|
|
15
17
|
MAX_NL2SQL_LIMIT = 1_000_000
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
def _pattern_match(text: str, pattern: str) -> Optional[re.Match]:
|
|
21
|
+
"""Match an NL2SQL mapping as a token when it is an English identifier.
|
|
22
|
+
|
|
23
|
+
Substring matching turns ``update`` into the ``date`` column and can
|
|
24
|
+
similarly match ``order`` inside unrelated words. Chinese phrases do
|
|
25
|
+
not have whitespace boundaries, so those retain substring semantics.
|
|
26
|
+
"""
|
|
27
|
+
expression = re.escape(pattern)
|
|
28
|
+
if not re.search(r"[\u4e00-\u9fff]", pattern):
|
|
29
|
+
expression = rf"(?<![\w$]){expression}(?![\w$])"
|
|
30
|
+
return re.search(expression, text, re.IGNORECASE)
|
|
31
|
+
|
|
32
|
+
|
|
18
33
|
def detect_operation(text: str, keywords: Dict[str, str]) -> str:
|
|
19
34
|
"""Detect SQL operation type from text.
|
|
20
35
|
|
|
@@ -22,8 +37,9 @@ def detect_operation(text: str, keywords: Dict[str, str]) -> str:
|
|
|
22
37
|
and returns the first matching operation (SELECT/INSERT/UPDATE/DELETE).
|
|
23
38
|
Defaults to SELECT.
|
|
24
39
|
"""
|
|
40
|
+
intent_text = mask_non_executable(text)
|
|
25
41
|
for keyword, op in keywords.items():
|
|
26
|
-
if keyword
|
|
42
|
+
if _pattern_match(intent_text, keyword) and op in ["SELECT", "INSERT", "UPDATE", "DELETE"]:
|
|
27
43
|
return op
|
|
28
44
|
return "SELECT"
|
|
29
45
|
|
|
@@ -36,8 +52,9 @@ def extract_table(text: str, table_patterns: Dict[str, str]) -> str:
|
|
|
36
52
|
"""
|
|
37
53
|
tables_found = []
|
|
38
54
|
for pattern, table in table_patterns.items():
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
match = _pattern_match(text, pattern)
|
|
56
|
+
if match:
|
|
57
|
+
tables_found.append((match.start(), table, len(pattern)))
|
|
41
58
|
if tables_found:
|
|
42
59
|
tables_found.sort(key=lambda item: (-item[2], item[0]))
|
|
43
60
|
return tables_found[0][1]
|
|
@@ -62,7 +79,8 @@ def extract_table_subject(
|
|
|
62
79
|
for pattern, table in table_patterns.items():
|
|
63
80
|
if relational_context and table == relational_context:
|
|
64
81
|
continue
|
|
65
|
-
|
|
82
|
+
match = _pattern_match(text, pattern)
|
|
83
|
+
pos = match.start() if match else -1
|
|
66
84
|
if pos != -1:
|
|
67
85
|
if table in [t for _, t in candidates]:
|
|
68
86
|
continue
|
|
@@ -80,7 +98,8 @@ def named_tables(text: str, table_patterns: Dict[str, str]) -> List[str]:
|
|
|
80
98
|
for pattern, table in table_patterns.items():
|
|
81
99
|
if table in seen:
|
|
82
100
|
continue
|
|
83
|
-
|
|
101
|
+
match = _pattern_match(text, pattern)
|
|
102
|
+
pos = match.start() if match else -1
|
|
84
103
|
if pos != -1:
|
|
85
104
|
occurrences.append((pos, table))
|
|
86
105
|
seen.add(table)
|
|
@@ -94,8 +113,9 @@ def extract_columns(text: str, column_patterns: Dict[str, str]) -> List[str]:
|
|
|
94
113
|
keyword is present.
|
|
95
114
|
"""
|
|
96
115
|
columns = []
|
|
116
|
+
text = mask_non_executable(text)
|
|
97
117
|
for pattern, column in column_patterns.items():
|
|
98
|
-
if pattern
|
|
118
|
+
if _pattern_match(text, pattern) and column not in columns:
|
|
99
119
|
columns.append(column)
|
|
100
120
|
return columns if columns else ["*"]
|
|
101
121
|
|
|
@@ -121,11 +141,14 @@ def extract_limit(text: str) -> Optional[int]:
|
|
|
121
141
|
# query.
|
|
122
142
|
if raw_value.startswith("-"):
|
|
123
143
|
raise ValueError("LIMIT cannot be negative")
|
|
124
|
-
if len(raw_value) >
|
|
144
|
+
if len(raw_value) > 64:
|
|
145
|
+
raise ValueError("LIMIT number representation exceeds the maximum supported length")
|
|
146
|
+
normalized_value = raw_value.lstrip("0") or "0"
|
|
147
|
+
if len(normalized_value) > len(str(MAX_NL2SQL_LIMIT)):
|
|
125
148
|
raise ValueError(
|
|
126
149
|
f"LIMIT exceeds maximum supported value of {MAX_NL2SQL_LIMIT}"
|
|
127
150
|
)
|
|
128
|
-
value = int(
|
|
151
|
+
value = int(normalized_value)
|
|
129
152
|
if value > MAX_NL2SQL_LIMIT:
|
|
130
153
|
raise ValueError(
|
|
131
154
|
f"LIMIT exceeds maximum supported value of {MAX_NL2SQL_LIMIT}"
|
{sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/mappings.py
RENAMED
|
@@ -13,13 +13,13 @@ KEYWORDS: Dict[str, str] = {
|
|
|
13
13
|
"select": "SELECT", "get": "SELECT", "find": "SELECT", "show": "SELECT", "list": "SELECT", "fetch": "SELECT",
|
|
14
14
|
# Insert operations
|
|
15
15
|
"插入": "INSERT", "添加": "INSERT", "新增": "INSERT", "创建": "INSERT",
|
|
16
|
-
"insert": "INSERT", "add": "INSERT",
|
|
16
|
+
"insert": "INSERT", "add": "INSERT",
|
|
17
17
|
# Update operations
|
|
18
18
|
"更新": "UPDATE", "修改": "UPDATE", "改变": "UPDATE", "设置": "UPDATE",
|
|
19
19
|
"update": "UPDATE", "modify": "UPDATE", "change": "UPDATE", "set": "UPDATE",
|
|
20
20
|
# Delete operations
|
|
21
21
|
"删除": "DELETE", "移除": "DELETE", "清除": "DELETE",
|
|
22
|
-
"delete": "DELETE", "remove": "DELETE",
|
|
22
|
+
"delete": "DELETE", "remove": "DELETE",
|
|
23
23
|
# Aggregations
|
|
24
24
|
"统计": "COUNT", "计数": "COUNT", "数量": "COUNT", "多少": "COUNT", "几个": "COUNT",
|
|
25
25
|
"count": "COUNT", "total": "COUNT", "how many": "COUNT",
|
{sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/dialect.py
RENAMED
|
@@ -127,7 +127,9 @@ def apply_dialect_adjustments(sql: str, dialect: str) -> str:
|
|
|
127
127
|
for pattern, replacement in replacements:
|
|
128
128
|
parts = []
|
|
129
129
|
cursor = 0
|
|
130
|
-
|
|
130
|
+
# Pass the target dialect so MySQL ``#`` comments are excluded from
|
|
131
|
+
# regex rewrites just like ``--`` and ``/* ... */`` comments.
|
|
132
|
+
for start, end in executable_segments(sql, dialect=dialect):
|
|
131
133
|
parts.append(sql[cursor:start])
|
|
132
134
|
segment = sql[start:end]
|
|
133
135
|
segment = pattern.sub(replacement, segment)
|
{sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/models.py
RENAMED
|
@@ -28,3 +28,6 @@ class NL2SQLResult:
|
|
|
28
28
|
evidence: GenerationEvidence | None = None
|
|
29
29
|
suggestions: list = field(default_factory=list)
|
|
30
30
|
parsed_elements: dict = field(default_factory=dict)
|
|
31
|
+
# DML callers need to know how generated placeholders should be bound.
|
|
32
|
+
parameter_style: str | None = None
|
|
33
|
+
parameters: list = field(default_factory=list)
|