sql-dialect-master 1.1.7__tar.gz → 1.1.9__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.7 → sql_dialect_master-1.1.9}/PKG-INFO +31 -3
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/README.md +29 -2
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/api/main.py +126 -5
- sql_dialect_master-1.1.9/backend/api/metrics.py +204 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/api/middleware.py +8 -1
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/api/workers.py +34 -2
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/config.py +12 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/metadata.py +2 -2
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/pyproject.toml +18 -1
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/sql_dialect_master.egg-info/PKG-INFO +31 -3
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/sql_dialect_master.egg-info/SOURCES.txt +1 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/sql_dialect_master.egg-info/requires.txt +1 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/LICENSE +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/__init__.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/api/__init__.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/api/auth.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/api/client_identity.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/api/rate_limit_store.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/api/readiness.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/__init__.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/cache.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/column_hint_validation.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/constants.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/exceptions.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/function_call_scanner.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/function_semantics.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/functions_db.json +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/functions_lookup.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/__init__.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/aggregations.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/core_extract.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/evidence.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/mappings.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/relations.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/templates.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/tokenizer.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_legacy.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/__init__.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/dialect.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/intent.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/ir.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/models.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/orchestrator.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/predicates.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/renderer.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/resolution.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/templates.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/validation.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/p1_sql_scanner.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/p2_data_validation.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/parser.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/post_processor.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/rules.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/semantic_diff.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/sql_statements.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/transpiler.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/type_mapping.json +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/type_mapping.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/utils/__init__.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/utils/validators.py +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/setup.cfg +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
- {sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/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.9
|
|
4
4
|
Summary: SQL Dialect Master - Multi-database SQL conversion engine
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -10,6 +10,7 @@ Requires-Dist: fastapi>=0.115.0
|
|
|
10
10
|
Requires-Dist: uvicorn>=0.30.0
|
|
11
11
|
Requires-Dist: pydantic>=2.9.0
|
|
12
12
|
Requires-Dist: pydantic-settings>=2.0.0
|
|
13
|
+
Requires-Dist: prometheus-client<1.0.0,>=0.20.0
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Requires-Dist: hypothesis>=6.112.0; extra == "dev"
|
|
15
16
|
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
@@ -35,7 +36,7 @@ Dynamic: license-file
|
|
|
35
36
|
|
|
36
37
|
Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
|
|
37
38
|
|
|
38
|
-
Current release: 1.1.
|
|
39
|
+
Current release: 1.1.9
|
|
39
40
|
|
|
40
41
|
[](https://www.python.org/downloads/)
|
|
41
42
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -293,6 +294,7 @@ sql-dialect-master/
|
|
|
293
294
|
| Endpoint | Method | Description |
|
|
294
295
|
|----------|--------|-------------|
|
|
295
296
|
| `/api/convert` | POST | Convert SQL between dialects; response includes `error_code` on failure |
|
|
297
|
+
| `/api/convert/batch` | POST | Convert an ordered batch with one dialect pair and per-item results |
|
|
296
298
|
| `/api/parse` | POST | Parse SQL and extract structural elements |
|
|
297
299
|
| `/api/nl2sql` | POST | Generate SQL from natural language |
|
|
298
300
|
| `/api/functions` | GET | Search SQL functions |
|
|
@@ -304,6 +306,24 @@ sql-dialect-master/
|
|
|
304
306
|
| `/health/deep` | GET | Deep health check |
|
|
305
307
|
| `/api/stats` | GET | Service and dataset statistics |
|
|
306
308
|
|
|
309
|
+
For example, send this JSON body to `POST /api/convert/batch`:
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
{
|
|
313
|
+
"statements": ["SELECT id FROM users", "SELECT user_id FROM orders"],
|
|
314
|
+
"source_dialect": "postgres",
|
|
315
|
+
"target_dialect": "mysql",
|
|
316
|
+
"pretty": true
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Results retain input order and include a zero-based `index` alongside the
|
|
321
|
+
single-conversion response fields. The summary includes `count`, `succeeded`,
|
|
322
|
+
and `failed`; top-level `success` is true only when every item succeeds.
|
|
323
|
+
Ordinary item failures, including a batch where every item fails, return HTTP
|
|
324
|
+
200 with per-item `error` and `error_code`. Request-level errors use the
|
|
325
|
+
existing error envelope; 503/504 responses do not include partial results.
|
|
326
|
+
|
|
307
327
|
## 🧪 Quality, Validation & Performance
|
|
308
328
|
|
|
309
329
|
The project maintains a 12 × 12 source-to-target dialect regression matrix, semantic regression tests, post-processing safety regressions, security boundary checks, deterministic rule-engine tests, cache-versioning tests, async concurrency tests, and stable error-code tests.
|
|
@@ -316,7 +336,14 @@ The semantic CI gate executes representative conversions against PostgreSQL and
|
|
|
316
336
|
|
|
317
337
|
Security validation structurally detects stacked SQL statements using the parser when available, with configurable policy controlling whether dangerous input is blocked or only warned about.
|
|
318
338
|
|
|
319
|
-
The
|
|
339
|
+
The SDK's `batch_transpile_async` bounds concurrent work and preserves input
|
|
340
|
+
order. REST batch conversion runs sequentially in one API worker slot, and the
|
|
341
|
+
entire batch shares the default 30-second queue-plus-execution budget
|
|
342
|
+
(`SDM_REQUEST_TIMEOUT`). HTTP 504 does not cancel the background batch. See
|
|
343
|
+
the [deployment limits](docs/deployment.md#1-deployment-model) for capacity
|
|
344
|
+
and timeout behavior.
|
|
345
|
+
|
|
346
|
+
A reproducible transpiler benchmark is available under `backend/benchmarks/` and is also executed in CI for performance observability. The benchmark intentionally reports measurements without failing the build on a fixed latency threshold because hosted CI timing is variable.
|
|
320
347
|
|
|
321
348
|
README source-of-truth checks run in CI and verify the release version, Python requirement, supported dialect list, and benchmark references against repository sources.
|
|
322
349
|
|
|
@@ -372,3 +399,4 @@ public catalog responses use short `private` caching. Full deployment details
|
|
|
372
399
|
will be documented in `docs/deployment.md`.
|
|
373
400
|
|
|
374
401
|
See [`docs/deployment.md`](docs/deployment.md) for production deployment guidance.
|
|
402
|
+
Metrics configuration and scraping guidance are documented in the same guide.
|
|
@@ -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.9
|
|
6
6
|
|
|
7
7
|
[](https://www.python.org/downloads/)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -260,6 +260,7 @@ sql-dialect-master/
|
|
|
260
260
|
| Endpoint | Method | Description |
|
|
261
261
|
|----------|--------|-------------|
|
|
262
262
|
| `/api/convert` | POST | Convert SQL between dialects; response includes `error_code` on failure |
|
|
263
|
+
| `/api/convert/batch` | POST | Convert an ordered batch with one dialect pair and per-item results |
|
|
263
264
|
| `/api/parse` | POST | Parse SQL and extract structural elements |
|
|
264
265
|
| `/api/nl2sql` | POST | Generate SQL from natural language |
|
|
265
266
|
| `/api/functions` | GET | Search SQL functions |
|
|
@@ -271,6 +272,24 @@ sql-dialect-master/
|
|
|
271
272
|
| `/health/deep` | GET | Deep health check |
|
|
272
273
|
| `/api/stats` | GET | Service and dataset statistics |
|
|
273
274
|
|
|
275
|
+
For example, send this JSON body to `POST /api/convert/batch`:
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{
|
|
279
|
+
"statements": ["SELECT id FROM users", "SELECT user_id FROM orders"],
|
|
280
|
+
"source_dialect": "postgres",
|
|
281
|
+
"target_dialect": "mysql",
|
|
282
|
+
"pretty": true
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Results retain input order and include a zero-based `index` alongside the
|
|
287
|
+
single-conversion response fields. The summary includes `count`, `succeeded`,
|
|
288
|
+
and `failed`; top-level `success` is true only when every item succeeds.
|
|
289
|
+
Ordinary item failures, including a batch where every item fails, return HTTP
|
|
290
|
+
200 with per-item `error` and `error_code`. Request-level errors use the
|
|
291
|
+
existing error envelope; 503/504 responses do not include partial results.
|
|
292
|
+
|
|
274
293
|
## 🧪 Quality, Validation & Performance
|
|
275
294
|
|
|
276
295
|
The project maintains a 12 × 12 source-to-target dialect regression matrix, semantic regression tests, post-processing safety regressions, security boundary checks, deterministic rule-engine tests, cache-versioning tests, async concurrency tests, and stable error-code tests.
|
|
@@ -283,7 +302,14 @@ The semantic CI gate executes representative conversions against PostgreSQL and
|
|
|
283
302
|
|
|
284
303
|
Security validation structurally detects stacked SQL statements using the parser when available, with configurable policy controlling whether dangerous input is blocked or only warned about.
|
|
285
304
|
|
|
286
|
-
The
|
|
305
|
+
The SDK's `batch_transpile_async` bounds concurrent work and preserves input
|
|
306
|
+
order. REST batch conversion runs sequentially in one API worker slot, and the
|
|
307
|
+
entire batch shares the default 30-second queue-plus-execution budget
|
|
308
|
+
(`SDM_REQUEST_TIMEOUT`). HTTP 504 does not cancel the background batch. See
|
|
309
|
+
the [deployment limits](docs/deployment.md#1-deployment-model) for capacity
|
|
310
|
+
and timeout behavior.
|
|
311
|
+
|
|
312
|
+
A reproducible transpiler benchmark is available under `backend/benchmarks/` and is also executed in CI for performance observability. The benchmark intentionally reports measurements without failing the build on a fixed latency threshold because hosted CI timing is variable.
|
|
287
313
|
|
|
288
314
|
README source-of-truth checks run in CI and verify the release version, Python requirement, supported dialect list, and benchmark references against repository sources.
|
|
289
315
|
|
|
@@ -339,3 +365,4 @@ public catalog responses use short `private` caching. Full deployment details
|
|
|
339
365
|
will be documented in `docs/deployment.md`.
|
|
340
366
|
|
|
341
367
|
See [`docs/deployment.md`](docs/deployment.md) for production deployment guidance.
|
|
368
|
+
Metrics configuration and scraping guidance are documented in the same guide.
|
|
@@ -27,9 +27,18 @@ from fastapi import Depends, FastAPI, HTTPException, Query, Request
|
|
|
27
27
|
from fastapi.middleware.cors import CORSMiddleware
|
|
28
28
|
from fastapi.middleware.gzip import GZipMiddleware
|
|
29
29
|
from fastapi.responses import JSONResponse
|
|
30
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
30
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictStr
|
|
31
31
|
|
|
32
32
|
from backend.api.auth import require_api_access
|
|
33
|
+
from backend.api.metrics import (
|
|
34
|
+
MetricsMiddleware,
|
|
35
|
+
configure_runtime_metrics,
|
|
36
|
+
metrics_response,
|
|
37
|
+
observe_transpile_batch_size,
|
|
38
|
+
record_transpile_batch_items,
|
|
39
|
+
record_transpile_batch_request,
|
|
40
|
+
record_transpile_request,
|
|
41
|
+
)
|
|
33
42
|
from backend.api.rate_limit_store import InMemoryRateLimitStore
|
|
34
43
|
from backend.api.readiness import health_probe_response
|
|
35
44
|
from backend.api.workers import BoundedWorkerPool, WorkerCapacityError
|
|
@@ -303,6 +312,12 @@ func_encyclopedia = FunctionEncyclopedia()
|
|
|
303
312
|
type_mapper = TypeMapper()
|
|
304
313
|
nl2sql_generator = NL2SQLGenerator()
|
|
305
314
|
|
|
315
|
+
configure_runtime_metrics(
|
|
316
|
+
transpiler_provider=lambda: transpiler,
|
|
317
|
+
worker_provider=_get_worker_pool,
|
|
318
|
+
limiter_provider=lambda: rate_limiter,
|
|
319
|
+
)
|
|
320
|
+
|
|
306
321
|
# Request timing middleware with logging
|
|
307
322
|
@app.middleware("http")
|
|
308
323
|
async def add_process_time_header(request: Request, call_next):
|
|
@@ -320,6 +335,11 @@ async def add_process_time_header(request: Request, call_next):
|
|
|
320
335
|
response.status_code, process_time)
|
|
321
336
|
return response
|
|
322
337
|
|
|
338
|
+
|
|
339
|
+
# This is the outermost application middleware, so it also observes short
|
|
340
|
+
# circuit responses returned by rate limiting and worker-capacity guards.
|
|
341
|
+
app.add_middleware(MetricsMiddleware)
|
|
342
|
+
|
|
323
343
|
# Dialect info for enhanced responses (from centralized config)
|
|
324
344
|
DIALECT_INFO = get_dialect_api_info()
|
|
325
345
|
|
|
@@ -380,6 +400,38 @@ class ConvertResponse(BaseModel):
|
|
|
380
400
|
)
|
|
381
401
|
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat(), description="Response timestamp")
|
|
382
402
|
|
|
403
|
+
|
|
404
|
+
class BatchConvertRequest(BaseModel):
|
|
405
|
+
"""Convert an ordered, bounded list using one dialect pair."""
|
|
406
|
+
|
|
407
|
+
statements: List[StrictStr] = Field(
|
|
408
|
+
...,
|
|
409
|
+
min_length=1,
|
|
410
|
+
max_length=settings.max_batch_size,
|
|
411
|
+
description="SQL statements in input order; each item is converted independently",
|
|
412
|
+
)
|
|
413
|
+
source_dialect: str
|
|
414
|
+
target_dialect: str
|
|
415
|
+
pretty: bool = settings.transpiler_pretty_default
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class BatchConvertItemResponse(ConvertResponse):
|
|
419
|
+
"""A conversion result associated with its original input position."""
|
|
420
|
+
|
|
421
|
+
index: int = Field(..., ge=0, description="Zero-based index in the input statements")
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class BatchConvertResponse(BaseModel):
|
|
425
|
+
"""Ordered conversion results, including ordinary per-item failures."""
|
|
426
|
+
|
|
427
|
+
success: bool
|
|
428
|
+
results: List[BatchConvertItemResponse]
|
|
429
|
+
count: int
|
|
430
|
+
succeeded: int
|
|
431
|
+
failed: int
|
|
432
|
+
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
433
|
+
|
|
434
|
+
|
|
383
435
|
class NL2SQLRequest(BaseModel):
|
|
384
436
|
"""Natural language to SQL request model."""
|
|
385
437
|
text: str = Field(..., description="Natural language query (Chinese or English)")
|
|
@@ -460,6 +512,10 @@ class APIResponse(BaseModel):
|
|
|
460
512
|
|
|
461
513
|
# === API Endpoints ===
|
|
462
514
|
|
|
515
|
+
@app.get("/metrics", include_in_schema=False)
|
|
516
|
+
async def metrics_endpoint(request: Request):
|
|
517
|
+
return await metrics_response(request)
|
|
518
|
+
|
|
463
519
|
@app.get("/", tags=["system"])
|
|
464
520
|
async def root():
|
|
465
521
|
"""
|
|
@@ -560,11 +616,13 @@ async def convert_sql(request: ConvertRequest):
|
|
|
560
616
|
source_dialect = request.source_dialect.strip().lower()
|
|
561
617
|
target_dialect = request.target_dialect.strip().lower()
|
|
562
618
|
if source_dialect not in SUPPORTED_DIALECTS:
|
|
619
|
+
record_transpile_request("unknown", "unknown", "validation_error")
|
|
563
620
|
raise HTTPException(
|
|
564
621
|
status_code=400,
|
|
565
622
|
detail=f"Unsupported source dialect: {request.source_dialect}. Supported: {SUPPORTED_DIALECTS}"
|
|
566
623
|
)
|
|
567
624
|
if target_dialect not in SUPPORTED_DIALECTS:
|
|
625
|
+
record_transpile_request(source_dialect, "unknown", "validation_error")
|
|
568
626
|
raise HTTPException(
|
|
569
627
|
status_code=400,
|
|
570
628
|
detail=f"Unsupported target dialect: {request.target_dialect}. Supported: {SUPPORTED_DIALECTS}"
|
|
@@ -572,12 +630,21 @@ async def convert_sql(request: ConvertRequest):
|
|
|
572
630
|
|
|
573
631
|
# sqlglot parsing/transpilation is CPU-bound. Keep it off the event loop
|
|
574
632
|
# so one large request cannot stall unrelated async requests.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
633
|
+
try:
|
|
634
|
+
result = await _run_cpu_bound(
|
|
635
|
+
transpiler.transpile,
|
|
636
|
+
request.sql,
|
|
637
|
+
source_dialect,
|
|
638
|
+
target_dialect,
|
|
639
|
+
request.pretty,
|
|
640
|
+
)
|
|
641
|
+
except HTTPException:
|
|
642
|
+
record_transpile_request(source_dialect, target_dialect, "error")
|
|
643
|
+
raise
|
|
644
|
+
record_transpile_request(
|
|
578
645
|
source_dialect,
|
|
579
646
|
target_dialect,
|
|
580
|
-
|
|
647
|
+
"success" if result.success else "error",
|
|
581
648
|
)
|
|
582
649
|
return ConvertResponse(
|
|
583
650
|
success=result.success,
|
|
@@ -593,6 +660,60 @@ async def convert_sql(request: ConvertRequest):
|
|
|
593
660
|
target_validation_state=result.target_validation_state,
|
|
594
661
|
)
|
|
595
662
|
|
|
663
|
+
@app.post("/api/convert/batch", response_model=BatchConvertResponse, tags=["conversion"])
|
|
664
|
+
async def convert_sql_batch(request: BatchConvertRequest):
|
|
665
|
+
"""Convert statements sequentially in one worker slot, preserving input order.
|
|
666
|
+
|
|
667
|
+
Ordinary item failures, including a batch where every item fails, return
|
|
668
|
+
HTTP 200 with per-item errors. Request-level failures use the existing
|
|
669
|
+
error envelope. A 504 ends the wait but does not cancel the worker batch.
|
|
670
|
+
"""
|
|
671
|
+
source_dialect = target_dialect = "unknown"
|
|
672
|
+
try:
|
|
673
|
+
source_dialect = normalize_dialect(request.source_dialect, "source dialect")
|
|
674
|
+
target_dialect = normalize_dialect(request.target_dialect, "target dialect")
|
|
675
|
+
observe_transpile_batch_size(len(request.statements))
|
|
676
|
+
batch_results = await _run_cpu_bound(
|
|
677
|
+
transpiler.batch_transpile,
|
|
678
|
+
list(request.statements),
|
|
679
|
+
source_dialect,
|
|
680
|
+
target_dialect,
|
|
681
|
+
request.pretty,
|
|
682
|
+
)
|
|
683
|
+
results = [
|
|
684
|
+
BatchConvertItemResponse(index=index, **result.to_dict())
|
|
685
|
+
for index, result in enumerate(batch_results)
|
|
686
|
+
]
|
|
687
|
+
count = len(results)
|
|
688
|
+
succeeded = sum(item.success for item in results)
|
|
689
|
+
failed = count - succeeded
|
|
690
|
+
response = BatchConvertResponse(
|
|
691
|
+
success=failed == 0,
|
|
692
|
+
results=results,
|
|
693
|
+
count=count,
|
|
694
|
+
succeeded=succeeded,
|
|
695
|
+
failed=failed,
|
|
696
|
+
)
|
|
697
|
+
except HTTPException as exc:
|
|
698
|
+
record_transpile_batch_request(
|
|
699
|
+
source_dialect,
|
|
700
|
+
target_dialect,
|
|
701
|
+
"validation_error" if exc.status_code == 400 else "error",
|
|
702
|
+
)
|
|
703
|
+
raise
|
|
704
|
+
except Exception:
|
|
705
|
+
record_transpile_batch_request(source_dialect, target_dialect, "error")
|
|
706
|
+
raise
|
|
707
|
+
|
|
708
|
+
record_transpile_batch_items(source_dialect, target_dialect, succeeded, failed)
|
|
709
|
+
record_transpile_batch_request(
|
|
710
|
+
source_dialect,
|
|
711
|
+
target_dialect,
|
|
712
|
+
"success" if failed == 0 else "partial_failure" if succeeded else "error",
|
|
713
|
+
)
|
|
714
|
+
return response
|
|
715
|
+
|
|
716
|
+
|
|
596
717
|
@app.post("/api/parse", tags=["conversion"])
|
|
597
718
|
async def parse_sql(request: ParseRequest):
|
|
598
719
|
"""
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Protected Prometheus metrics for the API process."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import secrets
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
from fastapi import Request
|
|
9
|
+
from fastapi.responses import PlainTextResponse, Response
|
|
10
|
+
from prometheus_client import (
|
|
11
|
+
CONTENT_TYPE_LATEST,
|
|
12
|
+
CollectorRegistry,
|
|
13
|
+
Counter,
|
|
14
|
+
Gauge,
|
|
15
|
+
Histogram,
|
|
16
|
+
generate_latest,
|
|
17
|
+
)
|
|
18
|
+
from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily
|
|
19
|
+
|
|
20
|
+
from backend.core.config import settings
|
|
21
|
+
from backend.core.metadata import PROJECT_VERSION
|
|
22
|
+
|
|
23
|
+
REGISTRY = CollectorRegistry(auto_describe=True)
|
|
24
|
+
|
|
25
|
+
BUILD_INFO = Gauge("sdm_build_info", "SQL Dialect Master build information", ["version"], registry=REGISTRY)
|
|
26
|
+
BUILD_INFO.labels(version=PROJECT_VERSION).set(1)
|
|
27
|
+
|
|
28
|
+
HTTP_REQUESTS = Counter(
|
|
29
|
+
"sdm_http_requests_total",
|
|
30
|
+
"HTTP requests handled by the API",
|
|
31
|
+
["method", "route", "status_class"],
|
|
32
|
+
registry=REGISTRY,
|
|
33
|
+
)
|
|
34
|
+
HTTP_DURATION = Histogram(
|
|
35
|
+
"sdm_http_request_duration_seconds",
|
|
36
|
+
"HTTP request duration in seconds",
|
|
37
|
+
["method", "route", "status_class"],
|
|
38
|
+
registry=REGISTRY,
|
|
39
|
+
)
|
|
40
|
+
RATE_LIMIT_REJECTIONS = Counter(
|
|
41
|
+
"sdm_rate_limit_rejections_total",
|
|
42
|
+
"Rate-limited requests",
|
|
43
|
+
registry=REGISTRY,
|
|
44
|
+
)
|
|
45
|
+
TRANSPILE_REQUESTS = Counter(
|
|
46
|
+
"sdm_transpile_requests_total",
|
|
47
|
+
"Conversion requests by dialect pair and result",
|
|
48
|
+
["source", "target", "result"],
|
|
49
|
+
registry=REGISTRY,
|
|
50
|
+
)
|
|
51
|
+
TRANSPILE_BATCH_REQUESTS = Counter(
|
|
52
|
+
"sdm_transpile_batch_requests_total",
|
|
53
|
+
"Batch conversion handler outcomes by dialect pair and result",
|
|
54
|
+
["source", "target", "result"],
|
|
55
|
+
registry=REGISTRY,
|
|
56
|
+
)
|
|
57
|
+
TRANSPILE_BATCH_ITEMS = Counter(
|
|
58
|
+
"sdm_transpile_batch_items_total",
|
|
59
|
+
"Items in complete batch results received by the API handler",
|
|
60
|
+
["source", "target", "result"],
|
|
61
|
+
registry=REGISTRY,
|
|
62
|
+
)
|
|
63
|
+
TRANSPILE_BATCH_SIZE = Histogram(
|
|
64
|
+
"sdm_transpile_batch_size",
|
|
65
|
+
"Statement count of validated batches before worker admission",
|
|
66
|
+
buckets=(1, 2, 5, 10, 20, 50, 100),
|
|
67
|
+
registry=REGISTRY,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
_transpiler_provider: Callable[[], object] | None = None
|
|
71
|
+
_worker_provider: Callable[[], object] | None = None
|
|
72
|
+
_limiter_provider: Callable[[], object] | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def configure_runtime_metrics(*, transpiler_provider=None, worker_provider=None, limiter_provider=None) -> None:
|
|
76
|
+
"""Register process-local runtime providers used when /metrics is scraped."""
|
|
77
|
+
global _transpiler_provider, _worker_provider, _limiter_provider
|
|
78
|
+
_transpiler_provider = transpiler_provider
|
|
79
|
+
_worker_provider = worker_provider
|
|
80
|
+
_limiter_provider = limiter_provider
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class RuntimeCollector:
|
|
84
|
+
"""Read low-frequency runtime snapshots without copying them per request."""
|
|
85
|
+
|
|
86
|
+
def collect(self):
|
|
87
|
+
if _transpiler_provider is not None:
|
|
88
|
+
try:
|
|
89
|
+
stats = _transpiler_provider().get_stats()
|
|
90
|
+
cache = stats.get("cache", {})
|
|
91
|
+
if cache.get("enabled", True) is not False:
|
|
92
|
+
yield CounterMetricFamily("sdm_transpile_cache_hits_total", "Transpiler cache hits", value=cache.get("hits", 0))
|
|
93
|
+
yield CounterMetricFamily("sdm_transpile_cache_misses_total", "Transpiler cache misses", value=cache.get("misses", 0))
|
|
94
|
+
yield CounterMetricFamily("sdm_transpile_cache_evictions_total", "Transpiler cache evictions", value=cache.get("evictions", 0))
|
|
95
|
+
yield GaugeMetricFamily("sdm_transpile_cache_size", "Current transpiler cache size", value=cache.get("size", 0))
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
if _worker_provider is not None:
|
|
100
|
+
try:
|
|
101
|
+
stats = _worker_provider().stats()
|
|
102
|
+
yield GaugeMetricFamily("sdm_api_worker_capacity", "API worker pool capacity", value=stats["capacity"])
|
|
103
|
+
yield GaugeMetricFamily("sdm_api_worker_in_use", "API worker slots in use", value=stats["in_use"])
|
|
104
|
+
yield CounterMetricFamily("sdm_api_worker_rejections_total", "API worker capacity rejections", value=stats["rejections"])
|
|
105
|
+
yield CounterMetricFamily("sdm_api_request_timeouts_total", "API request timeouts", value=stats["timeouts"])
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
if _limiter_provider is not None:
|
|
110
|
+
try:
|
|
111
|
+
stats = _limiter_provider().get_stats()
|
|
112
|
+
if stats.get("backend") == "memory":
|
|
113
|
+
yield GaugeMetricFamily("sdm_rate_limit_active_clients", "Active in-memory rate-limit clients", value=stats.get("active_clients", 0))
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
REGISTRY.register(RuntimeCollector())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def normalize_method(method: str) -> str:
|
|
122
|
+
return method if method in {"GET", "POST", "HEAD", "OPTIONS"} else "other"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def route_template(request: Request) -> str:
|
|
126
|
+
route = request.scope.get("route")
|
|
127
|
+
template = getattr(route, "path_format", None) or getattr(route, "path", None)
|
|
128
|
+
return template if template else "other"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def status_class(status_code: int) -> str:
|
|
132
|
+
return f"{status_code // 100}xx" if 100 <= status_code < 600 else "other"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def record_http_request(request: Request, status_code: int, duration: float) -> None:
|
|
136
|
+
if request.url.path == "/metrics":
|
|
137
|
+
return
|
|
138
|
+
labels = (normalize_method(request.method), route_template(request), status_class(status_code))
|
|
139
|
+
HTTP_REQUESTS.labels(*labels).inc()
|
|
140
|
+
HTTP_DURATION.labels(*labels).observe(duration)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def record_rate_limit_rejection() -> None:
|
|
144
|
+
RATE_LIMIT_REJECTIONS.inc()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def record_transpile_request(source: str, target: str, result: str) -> None:
|
|
148
|
+
TRANSPILE_REQUESTS.labels(source=source, target=target, result=result).inc()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def record_transpile_batch_request(source: str, target: str, result: str) -> None:
|
|
152
|
+
TRANSPILE_BATCH_REQUESTS.labels(source=source, target=target, result=result).inc()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def record_transpile_batch_items(source: str, target: str, succeeded: int, failed: int) -> None:
|
|
156
|
+
if succeeded:
|
|
157
|
+
TRANSPILE_BATCH_ITEMS.labels(source=source, target=target, result="success").inc(succeeded)
|
|
158
|
+
if failed:
|
|
159
|
+
TRANSPILE_BATCH_ITEMS.labels(source=source, target=target, result="error").inc(failed)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def observe_transpile_batch_size(size: int) -> None:
|
|
163
|
+
TRANSPILE_BATCH_SIZE.observe(size)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _authorized(request: Request) -> bool:
|
|
167
|
+
configured = settings.metrics_token
|
|
168
|
+
expected = configured.get_secret_value().strip() if configured else ""
|
|
169
|
+
supplied = request.headers.get("X-Metrics-Token", "")
|
|
170
|
+
return bool(expected and supplied and secrets.compare_digest(supplied, expected))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def metrics_response(request: Request) -> Response:
|
|
174
|
+
if not settings.metrics_enabled:
|
|
175
|
+
return Response(status_code=404)
|
|
176
|
+
if not _authorized(request):
|
|
177
|
+
return Response(status_code=401, content="Metrics token required")
|
|
178
|
+
return PlainTextResponse(generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class MetricsMiddleware:
|
|
182
|
+
"""ASGI middleware that records final HTTP status and elapsed time."""
|
|
183
|
+
|
|
184
|
+
def __init__(self, app):
|
|
185
|
+
self.app = app
|
|
186
|
+
|
|
187
|
+
async def __call__(self, scope, receive, send):
|
|
188
|
+
if scope.get("type") != "http":
|
|
189
|
+
await self.app(scope, receive, send)
|
|
190
|
+
return
|
|
191
|
+
import time
|
|
192
|
+
|
|
193
|
+
started = time.perf_counter()
|
|
194
|
+
status_code = 500
|
|
195
|
+
|
|
196
|
+
async def send_wrapper(message):
|
|
197
|
+
nonlocal status_code
|
|
198
|
+
if message.get("type") == "http.response.start":
|
|
199
|
+
status_code = message.get("status", 500)
|
|
200
|
+
await send(message)
|
|
201
|
+
|
|
202
|
+
await self.app(scope, receive, send_wrapper)
|
|
203
|
+
request = Request(scope)
|
|
204
|
+
record_http_request(request, status_code, time.perf_counter() - started)
|
|
@@ -20,6 +20,7 @@ from starlette.concurrency import run_in_threadpool
|
|
|
20
20
|
from backend.core.exceptions import ErrorCode
|
|
21
21
|
|
|
22
22
|
from .client_identity import resolve_client_ip
|
|
23
|
+
from .metrics import record_rate_limit_rejection
|
|
23
24
|
from .rate_limit_store import InMemoryRateLimitStore, RateLimitStore, create_rate_limit_store
|
|
24
25
|
from .readiness import health_probe_response
|
|
25
26
|
|
|
@@ -108,7 +109,7 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
108
109
|
|
|
109
110
|
async def dispatch(self, request: Request, call_next) -> Response:
|
|
110
111
|
is_health_probe = (
|
|
111
|
-
request.url.path in {"/health", "/", "/ready", "/health/deep"}
|
|
112
|
+
request.url.path in {"/health", "/", "/ready", "/health/deep", "/metrics"}
|
|
112
113
|
and request.method in {"GET", "HEAD"}
|
|
113
114
|
)
|
|
114
115
|
# Browser CORS preflight requests carry no business payload and are
|
|
@@ -133,6 +134,7 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
133
134
|
is_allowed, remaining, reset_time = await run_in_threadpool(self.limiter.is_allowed, client_id)
|
|
134
135
|
rate_limit_checked = True
|
|
135
136
|
if not is_allowed:
|
|
137
|
+
record_rate_limit_rejection()
|
|
136
138
|
logger.warning("Rate limit exceeded for client: %s", _sanitize_log_value(client_id))
|
|
137
139
|
return JSONResponse(
|
|
138
140
|
status_code=429,
|
|
@@ -164,6 +166,9 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
|
|
|
164
166
|
if is_health_probe:
|
|
165
167
|
if request.url.path in {"/ready", "/health/deep"}:
|
|
166
168
|
return await health_probe_response(request)
|
|
169
|
+
if request.url.path == "/metrics":
|
|
170
|
+
from .metrics import metrics_response
|
|
171
|
+
return await metrics_response(request)
|
|
167
172
|
return await call_next(request)
|
|
168
173
|
response = await call_next(request)
|
|
169
174
|
if rate_limit_checked:
|
|
@@ -386,6 +391,8 @@ class CacheControlMiddleware(BaseHTTPMiddleware):
|
|
|
386
391
|
if request.method == "OPTIONS":
|
|
387
392
|
return None
|
|
388
393
|
path = request.url.path
|
|
394
|
+
if path == "/metrics":
|
|
395
|
+
return "no-store"
|
|
389
396
|
if path in {"/docs", "/redoc", "/openapi.json"}:
|
|
390
397
|
return None
|
|
391
398
|
if request.method == "POST" and path.startswith("/api/"):
|
|
@@ -24,22 +24,54 @@ class BoundedWorkerPool:
|
|
|
24
24
|
self._slots = BoundedSemaphore(workers + queue_size)
|
|
25
25
|
self._executor = ThreadPoolExecutor(max_workers=workers, thread_name_prefix="sdm-api")
|
|
26
26
|
self._shutdown_lock = Lock()
|
|
27
|
+
self._stats_lock = Lock()
|
|
28
|
+
self._capacity = workers + queue_size
|
|
29
|
+
self._in_use = 0
|
|
30
|
+
self._rejections = 0
|
|
31
|
+
self._timeouts = 0
|
|
27
32
|
self._shutdown = False
|
|
28
33
|
|
|
29
34
|
async def run(self, function, *args, timeout: float):
|
|
30
35
|
if not self._slots.acquire(blocking=False):
|
|
36
|
+
with self._stats_lock:
|
|
37
|
+
self._rejections += 1
|
|
31
38
|
raise WorkerCapacityError("Request processing capacity is exhausted")
|
|
39
|
+
with self._stats_lock:
|
|
40
|
+
self._in_use += 1
|
|
32
41
|
try:
|
|
33
42
|
future = self._executor.submit(function, *args)
|
|
34
43
|
except BaseException:
|
|
35
44
|
self._slots.release()
|
|
45
|
+
with self._stats_lock:
|
|
46
|
+
self._in_use -= 1
|
|
36
47
|
raise
|
|
37
|
-
|
|
48
|
+
|
|
49
|
+
def release_slot(_future):
|
|
50
|
+
self._slots.release()
|
|
51
|
+
with self._stats_lock:
|
|
52
|
+
self._in_use -= 1
|
|
53
|
+
|
|
54
|
+
future.add_done_callback(release_slot)
|
|
38
55
|
wrapped = asyncio.wrap_future(future)
|
|
39
56
|
# A timed-out caller no longer awaits this result. Retrieve exceptions
|
|
40
57
|
# to avoid an unhandled-future warning when background work later fails.
|
|
41
58
|
wrapped.add_done_callback(lambda completed: None if completed.cancelled() else completed.exception())
|
|
42
|
-
|
|
59
|
+
try:
|
|
60
|
+
return await asyncio.wait_for(asyncio.shield(wrapped), timeout=timeout)
|
|
61
|
+
except asyncio.TimeoutError:
|
|
62
|
+
with self._stats_lock:
|
|
63
|
+
self._timeouts += 1
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
def stats(self) -> dict[str, int]:
|
|
67
|
+
"""Return a thread-safe snapshot of pool capacity counters."""
|
|
68
|
+
with self._stats_lock:
|
|
69
|
+
return {
|
|
70
|
+
"capacity": self._capacity,
|
|
71
|
+
"in_use": self._in_use,
|
|
72
|
+
"rejections": self._rejections,
|
|
73
|
+
"timeouts": self._timeouts,
|
|
74
|
+
}
|
|
43
75
|
|
|
44
76
|
def shutdown(self) -> None:
|
|
45
77
|
"""Cancel queued work; already-running calls retain their slots."""
|
|
@@ -276,6 +276,10 @@ class AppSettings(BaseSettings):
|
|
|
276
276
|
# Readiness probes. SecretStr keeps the value out of settings reprs and
|
|
277
277
|
# structured logs while still allowing deployment through SDM_ env vars.
|
|
278
278
|
health_probe_token: Optional[SecretStr] = None
|
|
279
|
+
|
|
280
|
+
# Prometheus metrics are opt-in and use a dedicated secret.
|
|
281
|
+
metrics_enabled: bool = False
|
|
282
|
+
metrics_token: Optional[SecretStr] = None
|
|
279
283
|
|
|
280
284
|
# Security settings
|
|
281
285
|
security_check_enabled: bool = True
|
|
@@ -292,6 +296,14 @@ class AppSettings(BaseSettings):
|
|
|
292
296
|
self.parser_max_sql_length = self.transpiler_max_sql_length
|
|
293
297
|
return self
|
|
294
298
|
|
|
299
|
+
@model_validator(mode="after")
|
|
300
|
+
def _validate_metrics_token(self) -> "AppSettings":
|
|
301
|
+
"""Require a non-blank dedicated token when metrics are enabled."""
|
|
302
|
+
token = self.metrics_token.get_secret_value().strip() if self.metrics_token else ""
|
|
303
|
+
if self.metrics_enabled and not token:
|
|
304
|
+
raise ValueError("metrics_token is required when metrics_enabled is true")
|
|
305
|
+
return self
|
|
306
|
+
|
|
295
307
|
@field_validator("log_level", mode="before")
|
|
296
308
|
@classmethod
|
|
297
309
|
def _normalize_log_level(cls, value: str) -> str:
|
|
@@ -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.9"
|
|
21
|
+
API_VERSION = "1.1.9"
|
|
22
22
|
# Frontend v2 architecture — independent of the product release version.
|
|
23
23
|
FRONTEND_ARCHITECTURE_VERSION = 2
|
|
24
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sql-dialect-master"
|
|
3
|
-
version = "1.1.
|
|
3
|
+
version = "1.1.9"
|
|
4
4
|
description = "SQL Dialect Master - Multi-database SQL conversion engine"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -10,6 +10,7 @@ dependencies = [
|
|
|
10
10
|
"uvicorn>=0.30.0",
|
|
11
11
|
"pydantic>=2.9.0",
|
|
12
12
|
"pydantic-settings>=2.0.0",
|
|
13
|
+
"prometheus-client>=0.20.0,<1.0.0",
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
[project.optional-dependencies]
|
|
@@ -51,3 +52,19 @@ exclude = ["backend.tests*", "backend.benchmarks*"]
|
|
|
51
52
|
testpaths = ["backend/tests"]
|
|
52
53
|
python_files = ["test_*.py"]
|
|
53
54
|
addopts = "-v --tb=short"
|
|
55
|
+
|
|
56
|
+
[tool.coverage.run]
|
|
57
|
+
source = ["backend"]
|
|
58
|
+
omit = [
|
|
59
|
+
"backend/tests/*",
|
|
60
|
+
"backend/benchmarks/*",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[tool.coverage.report]
|
|
64
|
+
show_missing = true
|
|
65
|
+
skip_covered = false
|
|
66
|
+
fail_under = 93
|
|
67
|
+
exclude_lines = [
|
|
68
|
+
"pragma: no cover",
|
|
69
|
+
"if __name__ == .__main__.:",
|
|
70
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-dialect-master
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.9
|
|
4
4
|
Summary: SQL Dialect Master - Multi-database SQL conversion engine
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -10,6 +10,7 @@ Requires-Dist: fastapi>=0.115.0
|
|
|
10
10
|
Requires-Dist: uvicorn>=0.30.0
|
|
11
11
|
Requires-Dist: pydantic>=2.9.0
|
|
12
12
|
Requires-Dist: pydantic-settings>=2.0.0
|
|
13
|
+
Requires-Dist: prometheus-client<1.0.0,>=0.20.0
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Requires-Dist: hypothesis>=6.112.0; extra == "dev"
|
|
15
16
|
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
@@ -35,7 +36,7 @@ Dynamic: license-file
|
|
|
35
36
|
|
|
36
37
|
Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
|
|
37
38
|
|
|
38
|
-
Current release: 1.1.
|
|
39
|
+
Current release: 1.1.9
|
|
39
40
|
|
|
40
41
|
[](https://www.python.org/downloads/)
|
|
41
42
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -293,6 +294,7 @@ sql-dialect-master/
|
|
|
293
294
|
| Endpoint | Method | Description |
|
|
294
295
|
|----------|--------|-------------|
|
|
295
296
|
| `/api/convert` | POST | Convert SQL between dialects; response includes `error_code` on failure |
|
|
297
|
+
| `/api/convert/batch` | POST | Convert an ordered batch with one dialect pair and per-item results |
|
|
296
298
|
| `/api/parse` | POST | Parse SQL and extract structural elements |
|
|
297
299
|
| `/api/nl2sql` | POST | Generate SQL from natural language |
|
|
298
300
|
| `/api/functions` | GET | Search SQL functions |
|
|
@@ -304,6 +306,24 @@ sql-dialect-master/
|
|
|
304
306
|
| `/health/deep` | GET | Deep health check |
|
|
305
307
|
| `/api/stats` | GET | Service and dataset statistics |
|
|
306
308
|
|
|
309
|
+
For example, send this JSON body to `POST /api/convert/batch`:
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
{
|
|
313
|
+
"statements": ["SELECT id FROM users", "SELECT user_id FROM orders"],
|
|
314
|
+
"source_dialect": "postgres",
|
|
315
|
+
"target_dialect": "mysql",
|
|
316
|
+
"pretty": true
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Results retain input order and include a zero-based `index` alongside the
|
|
321
|
+
single-conversion response fields. The summary includes `count`, `succeeded`,
|
|
322
|
+
and `failed`; top-level `success` is true only when every item succeeds.
|
|
323
|
+
Ordinary item failures, including a batch where every item fails, return HTTP
|
|
324
|
+
200 with per-item `error` and `error_code`. Request-level errors use the
|
|
325
|
+
existing error envelope; 503/504 responses do not include partial results.
|
|
326
|
+
|
|
307
327
|
## 🧪 Quality, Validation & Performance
|
|
308
328
|
|
|
309
329
|
The project maintains a 12 × 12 source-to-target dialect regression matrix, semantic regression tests, post-processing safety regressions, security boundary checks, deterministic rule-engine tests, cache-versioning tests, async concurrency tests, and stable error-code tests.
|
|
@@ -316,7 +336,14 @@ The semantic CI gate executes representative conversions against PostgreSQL and
|
|
|
316
336
|
|
|
317
337
|
Security validation structurally detects stacked SQL statements using the parser when available, with configurable policy controlling whether dangerous input is blocked or only warned about.
|
|
318
338
|
|
|
319
|
-
The
|
|
339
|
+
The SDK's `batch_transpile_async` bounds concurrent work and preserves input
|
|
340
|
+
order. REST batch conversion runs sequentially in one API worker slot, and the
|
|
341
|
+
entire batch shares the default 30-second queue-plus-execution budget
|
|
342
|
+
(`SDM_REQUEST_TIMEOUT`). HTTP 504 does not cancel the background batch. See
|
|
343
|
+
the [deployment limits](docs/deployment.md#1-deployment-model) for capacity
|
|
344
|
+
and timeout behavior.
|
|
345
|
+
|
|
346
|
+
A reproducible transpiler benchmark is available under `backend/benchmarks/` and is also executed in CI for performance observability. The benchmark intentionally reports measurements without failing the build on a fixed latency threshold because hosted CI timing is variable.
|
|
320
347
|
|
|
321
348
|
README source-of-truth checks run in CI and verify the release version, Python requirement, supported dialect list, and benchmark references against repository sources.
|
|
322
349
|
|
|
@@ -372,3 +399,4 @@ public catalog responses use short `private` caching. Full deployment details
|
|
|
372
399
|
will be documented in `docs/deployment.md`.
|
|
373
400
|
|
|
374
401
|
See [`docs/deployment.md`](docs/deployment.md) for production deployment guidance.
|
|
402
|
+
Metrics configuration and scraping guidance are documented in the same guide.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/column_hint_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/__init__.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/aggregations.py
RENAMED
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/core_extract.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/evidence.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/mappings.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/relations.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/templates.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_components/tokenizer.py
RENAMED
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/__init__.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/aggregations.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/clause_extract.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/dialect.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/intent.py
RENAMED
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/models.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/orchestrator.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/predicates.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/renderer.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/resolution.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/suggestions.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/templates.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/backend/core/nl2sql_pipeline/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.1.7 → sql_dialect_master-1.1.9}/sql_dialect_master.egg-info/top_level.txt
RENAMED
|
File without changes
|