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.
Files changed (102) hide show
  1. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/PKG-INFO +21 -5
  2. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/README.md +16 -4
  3. sql_dialect_master-1.1.4/backend/api/auth.py +29 -0
  4. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/main.py +58 -15
  5. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/middleware.py +21 -3
  6. sql_dialect_master-1.1.4/backend/api/workers.py +44 -0
  7. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/config.py +6 -0
  8. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/exceptions.py +3 -0
  9. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/functions_lookup.py +9 -5
  10. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/metadata.py +2 -2
  11. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/core_extract.py +31 -8
  12. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/mappings.py +2 -2
  13. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/dialect.py +3 -1
  14. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/models.py +3 -0
  15. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/orchestrator.py +155 -20
  16. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/validation.py +4 -3
  17. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/p1_sql_scanner.py +26 -1
  18. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/parser.py +3 -50
  19. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/semantic_diff.py +3 -1
  20. sql_dialect_master-1.1.4/backend/core/sql_statements.py +30 -0
  21. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/transpiler.py +55 -12
  22. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/type_mapping.py +8 -6
  23. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/utils/validators.py +27 -41
  24. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/navigation.py +37 -5
  25. sql_dialect_master-1.1.4/frontend/core/sql_inputs.py +6 -0
  26. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/state.py +46 -11
  27. sql_dialect_master-1.1.4/frontend/core/statuses.py +34 -0
  28. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/viewmodels.py +3 -20
  29. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/convert.py +2 -3
  30. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/diff.py +18 -3
  31. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/history.py +6 -5
  32. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/nl2sql.py +19 -7
  33. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/diff.py +2 -8
  34. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/editor.py +5 -1
  35. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/status.py +8 -11
  36. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/pyproject.toml +5 -1
  37. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/PKG-INFO +21 -5
  38. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/SOURCES.txt +4 -0
  39. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/requires.txt +4 -0
  40. sql_dialect_master-1.1.3/frontend/core/sql_inputs.py +0 -29
  41. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/LICENSE +0 -0
  42. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/__init__.py +0 -0
  43. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/__init__.py +0 -0
  44. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/rate_limit_store.py +0 -0
  45. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/api/readiness.py +0 -0
  46. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/__init__.py +0 -0
  47. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/cache.py +0 -0
  48. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/column_hint_validation.py +0 -0
  49. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/constants.py +0 -0
  50. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/function_call_scanner.py +0 -0
  51. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/function_semantics.py +0 -0
  52. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/functions_db.json +0 -0
  53. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql.py +0 -0
  54. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/__init__.py +0 -0
  55. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/aggregations.py +0 -0
  56. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
  57. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/evidence.py +0 -0
  58. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/relations.py +0 -0
  59. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/templates.py +0 -0
  60. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_components/tokenizer.py +0 -0
  61. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_legacy.py +0 -0
  62. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/__init__.py +0 -0
  63. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
  64. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
  65. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/intent.py +0 -0
  66. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/ir.py +0 -0
  67. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/predicates.py +0 -0
  68. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/renderer.py +0 -0
  69. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/resolution.py +0 -0
  70. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
  71. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/nl2sql_pipeline/templates.py +0 -0
  72. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/p2_data_validation.py +0 -0
  73. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/post_processor.py +0 -0
  74. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/rules.py +0 -0
  75. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/core/type_mapping.json +0 -0
  76. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/frontend_entrypoint.py +0 -0
  77. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/backend/utils/__init__.py +0 -0
  78. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/__init__.py +0 -0
  79. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/app_context_v2.py +0 -0
  80. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/__init__.py +0 -0
  81. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/design_tokens.py +0 -0
  82. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/escaping.py +0 -0
  83. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/fingerprints.py +0 -0
  84. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/styles.py +0 -0
  85. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/themes.py +0 -0
  86. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/core/type_status.py +0 -0
  87. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/__init__.py +0 -0
  88. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/functions.py +0 -0
  89. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/lineage.py +0 -0
  90. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/query_analysis.py +0 -0
  91. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/runtime.py +0 -0
  92. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/settings.py +0 -0
  93. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/templates.py +0 -0
  94. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/pages/types.py +0 -0
  95. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/templates_v2.py +0 -0
  96. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/__init__.py +0 -0
  97. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/frontend/ui/command_palette.py +0 -0
  98. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sdm_local_v2.py +0 -0
  99. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/setup.cfg +0 -0
  100. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
  101. {sql_dialect_master-1.1.3 → sql_dialect_master-1.1.4}/sql_dialect_master.egg-info/entry_points.txt +0 -0
  102. {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
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.3
39
+ Current release: 1.1.4
36
40
 
37
41
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
38
42
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 testing, install the optional development dependencies:
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 pip install -e ".[dev]"
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.3
5
+ Current release: 1.1.4
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)
@@ -48,16 +48,18 @@ cd sql-dialect-master
48
48
  python -m pip install -e .
49
49
  ```
50
50
 
51
- For development and testing, install the optional development dependencies:
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 pip install -e ".[dev]"
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
- return await asyncio.wait_for(
49
- run_in_threadpool(func, *args),
50
- timeout=settings.request_timeout,
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
- f"{request.method} {request.url.path} - {response.status_code} - {process_time:.4f}s"
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(category)[:limit]
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
- is_allowed, remaining, reset_time = self.limiter.is_allowed(client_id)
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
- limit = limit or settings.function_search_limit
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
- return self.name_index.get(name.upper())
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.3"
21
- API_VERSION = "1.1.3"
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
 
@@ -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 in text and op in ["SELECT", "INSERT", "UPDATE", "DELETE"]:
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
- if pattern in text:
40
- tables_found.append((text.find(pattern), table, len(pattern)))
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
- pos = text.find(pattern)
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
- pos = text.find(pattern)
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 in text and column not in columns:
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) > len(str(MAX_NL2SQL_LIMIT)):
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(raw_value)
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}"
@@ -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", "create": "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", "drop": "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",
@@ -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
- for start, end in executable_segments(sql):
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)
@@ -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)