sql-dialect-master 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backend/__init__.py +1 -0
- backend/api/__init__.py +1 -0
- backend/api/main.py +732 -0
- backend/api/middleware.py +274 -0
- backend/api/rate_limit_store.py +105 -0
- backend/api/readiness.py +178 -0
- backend/core/__init__.py +57 -0
- backend/core/cache.py +261 -0
- backend/core/column_hint_validation.py +22 -0
- backend/core/config.py +546 -0
- backend/core/constants.py +84 -0
- backend/core/exceptions.py +124 -0
- backend/core/function_call_scanner.py +64 -0
- backend/core/function_semantics.py +148 -0
- backend/core/functions_db.json +601 -0
- backend/core/functions_lookup.py +332 -0
- backend/core/metadata.py +71 -0
- backend/core/nl2sql.py +85 -0
- backend/core/nl2sql_components/__init__.py +72 -0
- backend/core/nl2sql_components/aggregations.py +35 -0
- backend/core/nl2sql_components/boolean_conditions.py +279 -0
- backend/core/nl2sql_components/core_extract.py +106 -0
- backend/core/nl2sql_components/evidence.py +122 -0
- backend/core/nl2sql_components/mappings.py +185 -0
- backend/core/nl2sql_components/relations.py +180 -0
- backend/core/nl2sql_components/templates.py +130 -0
- backend/core/nl2sql_components/tokenizer.py +187 -0
- backend/core/nl2sql_legacy.py +22 -0
- backend/core/nl2sql_pipeline/__init__.py +13 -0
- backend/core/nl2sql_pipeline/aggregations.py +26 -0
- backend/core/nl2sql_pipeline/clause_extract.py +97 -0
- backend/core/nl2sql_pipeline/dialect.py +139 -0
- backend/core/nl2sql_pipeline/intent.py +85 -0
- backend/core/nl2sql_pipeline/ir.py +70 -0
- backend/core/nl2sql_pipeline/models.py +30 -0
- backend/core/nl2sql_pipeline/orchestrator.py +576 -0
- backend/core/nl2sql_pipeline/predicates.py +182 -0
- backend/core/nl2sql_pipeline/renderer.py +248 -0
- backend/core/nl2sql_pipeline/resolution.py +69 -0
- backend/core/nl2sql_pipeline/suggestions.py +30 -0
- backend/core/nl2sql_pipeline/templates.py +371 -0
- backend/core/nl2sql_pipeline/validation.py +28 -0
- backend/core/p1_sql_scanner.py +196 -0
- backend/core/p2_data_validation.py +72 -0
- backend/core/parser.py +280 -0
- backend/core/post_processor.py +129 -0
- backend/core/rules.py +808 -0
- backend/core/semantic_diff.py +385 -0
- backend/core/transpiler.py +791 -0
- backend/core/type_mapping.json +593 -0
- backend/core/type_mapping.py +765 -0
- backend/utils/__init__.py +2 -0
- backend/utils/validators.py +407 -0
- frontend/__init__.py +10 -0
- frontend/app_context_v2.py +201 -0
- frontend/core/__init__.py +33 -0
- frontend/core/design_tokens.py +127 -0
- frontend/core/escaping.py +13 -0
- frontend/core/navigation.py +135 -0
- frontend/core/state.py +612 -0
- frontend/core/styles.py +523 -0
- frontend/core/themes.py +69 -0
- frontend/core/viewmodels.py +499 -0
- frontend/pages/__init__.py +0 -0
- frontend/pages/convert.py +283 -0
- frontend/pages/diff.py +157 -0
- frontend/pages/functions.py +100 -0
- frontend/pages/history.py +144 -0
- frontend/pages/lineage.py +168 -0
- frontend/pages/nl2sql.py +305 -0
- frontend/pages/query_analysis.py +120 -0
- frontend/pages/runtime.py +66 -0
- frontend/pages/settings.py +60 -0
- frontend/pages/templates.py +67 -0
- frontend/pages/types.py +110 -0
- frontend/templates_v2.py +53 -0
- frontend/ui/__init__.py +0 -0
- frontend/ui/command_palette.py +153 -0
- frontend/ui/diff.py +275 -0
- frontend/ui/editor.py +102 -0
- frontend/ui/status.py +255 -0
- sql_dialect_master-1.1.1.dist-info/METADATA +360 -0
- sql_dialect_master-1.1.1.dist-info/RECORD +86 -0
- sql_dialect_master-1.1.1.dist-info/WHEEL +5 -0
- sql_dialect_master-1.1.1.dist-info/licenses/LICENSE +21 -0
- sql_dialect_master-1.1.1.dist-info/top_level.txt +2 -0
backend/api/main.py
ADDED
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""FastAPI Backend for SQL Dialect Master.
|
|
3
|
+
|
|
4
|
+
Enterprise-grade multi-database SQL conversion API:
|
|
5
|
+
- 12 database dialects
|
|
6
|
+
- SQL functions encyclopedia
|
|
7
|
+
- data type mappings
|
|
8
|
+
- conversion rules
|
|
9
|
+
- Natural language to SQL generation
|
|
10
|
+
|
|
11
|
+
Exact counts are derived from the canonical data files at runtime —
|
|
12
|
+
see backend.core.metadata — and must not be restated here.
|
|
13
|
+
"""
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
from fastapi import FastAPI, HTTPException, Query, Request
|
|
20
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
21
|
+
from fastapi.responses import JSONResponse
|
|
22
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
23
|
+
|
|
24
|
+
from backend.core.config import get_dialect_api_info, settings, setup_logging
|
|
25
|
+
from backend.core import metadata as meta
|
|
26
|
+
from backend.core.functions_lookup import FunctionEncyclopedia
|
|
27
|
+
from backend.core.nl2sql import NL2SQLGenerator
|
|
28
|
+
from backend.core.parser import SUPPORTED_DIALECTS, SQLParser
|
|
29
|
+
from backend.core.transpiler import SQLTranspiler
|
|
30
|
+
from backend.core.type_mapping import TypeMapper
|
|
31
|
+
|
|
32
|
+
from backend.api.readiness import health_probe_response
|
|
33
|
+
|
|
34
|
+
# Configure logging for the application
|
|
35
|
+
logger = setup_logging(level=logging.INFO)
|
|
36
|
+
api_logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
# API metadata
|
|
39
|
+
API_VERSION = settings.api_version
|
|
40
|
+
API_TITLE = settings.api_title
|
|
41
|
+
API_DESCRIPTION = f"""
|
|
42
|
+
# 🔄 SQL Dialect Master API
|
|
43
|
+
|
|
44
|
+
Enterprise-grade multi-database SQL conversion engine.
|
|
45
|
+
|
|
46
|
+
## ✨ Features
|
|
47
|
+
|
|
48
|
+
| Feature | Description |
|
|
49
|
+
|---------|-------------|
|
|
50
|
+
| 🔄 **SQL Conversion** | Convert SQL between {meta.SUPPORTED_DIALECTS_COUNT} database dialects |
|
|
51
|
+
| 📚 **Function Encyclopedia** | {meta.FUNCTION_COUNT} SQL functions with cross-database comparison |
|
|
52
|
+
| 🗂️ **Type Mapping** | {meta.TYPE_COUNT} data type mappings × {meta.SUPPORTED_DIALECTS_COUNT} databases matrix |
|
|
53
|
+
| 💬 **NL2SQL** | Natural language to SQL (Chinese/English) |
|
|
54
|
+
|
|
55
|
+
## 💾 Supported Databases
|
|
56
|
+
|
|
57
|
+
| Category | Databases |
|
|
58
|
+
|----------|-----------|
|
|
59
|
+
| **RDBMS** | MySQL 🐬, PostgreSQL 🐘, Oracle 🔴, SQL Server 🟦 |
|
|
60
|
+
| **Big Data** | Hive 🐝, Spark ⚡, Trino 🔷, Databricks 🧱 |
|
|
61
|
+
| **Cloud DW** | Snowflake ❄️, Redshift 🔶 |
|
|
62
|
+
| **OLAP** | ClickHouse 🏠, DuckDB 🦆 |
|
|
63
|
+
|
|
64
|
+
## 🚀 Quick Start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import requests
|
|
68
|
+
|
|
69
|
+
# Convert SQL
|
|
70
|
+
response = requests.post("http://localhost:8000/api/convert", json={{
|
|
71
|
+
"sql": "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') FROM orders",
|
|
72
|
+
"source_dialect": "mysql",
|
|
73
|
+
"target_dialect": "postgres"
|
|
74
|
+
}})
|
|
75
|
+
print(response.json()["target_sql"])
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 📖 API Endpoints
|
|
79
|
+
|
|
80
|
+
- `POST /api/convert` - Convert SQL between {meta.SUPPORTED_DIALECTS_COUNT} database dialects
|
|
81
|
+
- `GET /api/functions` - Search SQL functions
|
|
82
|
+
- `GET /api/types` - Get type mapping matrix
|
|
83
|
+
- `POST /api/nl2sql` - Generate SQL from natural language
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
# Initialize FastAPI app with enhanced metadata
|
|
87
|
+
app = FastAPI(
|
|
88
|
+
title=API_TITLE,
|
|
89
|
+
description=API_DESCRIPTION,
|
|
90
|
+
version=API_VERSION,
|
|
91
|
+
docs_url="/docs",
|
|
92
|
+
redoc_url="/redoc",
|
|
93
|
+
license_info={
|
|
94
|
+
"name": "MIT License",
|
|
95
|
+
"url": "https://opensource.org/licenses/MIT"
|
|
96
|
+
},
|
|
97
|
+
openapi_tags=[
|
|
98
|
+
{
|
|
99
|
+
"name": "conversion",
|
|
100
|
+
"description": "🔄 SQL dialect conversion operations",
|
|
101
|
+
"externalDocs": {"description": "Learn more", "url": "https://sqlglot.com/"}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"name": "functions",
|
|
105
|
+
"description": f"📚 SQL function encyclopedia with {meta.FUNCTION_COUNT} functions"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "types",
|
|
109
|
+
"description": f"🗂️ Data type mapping matrix ({meta.TYPE_COUNT} types × {meta.SUPPORTED_DIALECTS_COUNT} databases)"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "nl2sql",
|
|
113
|
+
"description": "💬 Natural language to SQL generation"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"name": "system",
|
|
117
|
+
"description": "⚙️ System and health endpoints"
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Import middleware
|
|
123
|
+
from backend.api.middleware import (
|
|
124
|
+
RateLimiter,
|
|
125
|
+
RateLimitMiddleware,
|
|
126
|
+
SecurityHeadersMiddleware,
|
|
127
|
+
StructuredLoggingMiddleware,
|
|
128
|
+
)
|
|
129
|
+
from backend.core.exceptions import SDMException, UnsupportedDialectError, ErrorCode
|
|
130
|
+
|
|
131
|
+
# Configure CORS from the centralized Pydantic settings source.
|
|
132
|
+
ALLOWED_ORIGINS = [origin.strip() for origin in settings.allowed_origins.split(",") if origin.strip()]
|
|
133
|
+
|
|
134
|
+
app.add_middleware(
|
|
135
|
+
CORSMiddleware,
|
|
136
|
+
allow_origins=ALLOWED_ORIGINS,
|
|
137
|
+
allow_credentials=True,
|
|
138
|
+
allow_methods=["GET", "POST", "OPTIONS"], # Restrict to needed methods
|
|
139
|
+
allow_headers=["Content-Type", "Authorization", "X-Request-ID"],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Add security headers
|
|
143
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
144
|
+
|
|
145
|
+
# Add rate limiting (configurable via settings)
|
|
146
|
+
rate_limiter = RateLimiter(
|
|
147
|
+
requests_per_window=settings.rate_limit_requests,
|
|
148
|
+
window_seconds=settings.rate_limit_window,
|
|
149
|
+
)
|
|
150
|
+
app.add_middleware(
|
|
151
|
+
RateLimitMiddleware,
|
|
152
|
+
limiter=rate_limiter,
|
|
153
|
+
enabled=settings.rate_limit_enabled,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Structured request logging must wrap rate limiting so rejected requests also get IDs.
|
|
157
|
+
app.add_middleware(StructuredLoggingMiddleware)
|
|
158
|
+
|
|
159
|
+
# Initialize services
|
|
160
|
+
transpiler = SQLTranspiler()
|
|
161
|
+
func_encyclopedia = FunctionEncyclopedia()
|
|
162
|
+
type_mapper = TypeMapper()
|
|
163
|
+
nl2sql_generator = NL2SQLGenerator()
|
|
164
|
+
|
|
165
|
+
# Request timing middleware with logging
|
|
166
|
+
@app.middleware("http")
|
|
167
|
+
async def add_process_time_header(request: Request, call_next):
|
|
168
|
+
start_time = time.time()
|
|
169
|
+
response = await call_next(request)
|
|
170
|
+
process_time = time.time() - start_time
|
|
171
|
+
response.headers["X-Process-Time"] = f"{process_time:.4f}s"
|
|
172
|
+
response.headers["X-API-Version"] = API_VERSION
|
|
173
|
+
|
|
174
|
+
# Log request details
|
|
175
|
+
api_logger.info(
|
|
176
|
+
f"{request.method} {request.url.path} - {response.status_code} - {process_time:.4f}s"
|
|
177
|
+
)
|
|
178
|
+
return response
|
|
179
|
+
|
|
180
|
+
# Dialect info for enhanced responses (from centralized config)
|
|
181
|
+
DIALECT_INFO = get_dialect_api_info()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def normalize_dialect(dialect: str, field_name: str = "dialect") -> str:
|
|
185
|
+
"""Normalize and validate a dialect at the API boundary."""
|
|
186
|
+
normalized = dialect.strip().lower()
|
|
187
|
+
if normalized not in SUPPORTED_DIALECTS:
|
|
188
|
+
raise HTTPException(
|
|
189
|
+
status_code=400,
|
|
190
|
+
detail=f"Unsupported {field_name}: {dialect}. Supported: {SUPPORTED_DIALECTS}",
|
|
191
|
+
)
|
|
192
|
+
return normalized
|
|
193
|
+
|
|
194
|
+
# === Request/Response Models with Enhanced Documentation ===
|
|
195
|
+
|
|
196
|
+
class ConvertRequest(BaseModel):
|
|
197
|
+
"""SQL conversion request model."""
|
|
198
|
+
sql: str = Field(..., description="Source SQL statement to convert")
|
|
199
|
+
source_dialect: str = Field(..., description="Source database dialect")
|
|
200
|
+
target_dialect: str = Field(..., description="Target database dialect")
|
|
201
|
+
pretty: bool = Field(True, description="Format output SQL with indentation")
|
|
202
|
+
|
|
203
|
+
model_config = ConfigDict(json_schema_extra={
|
|
204
|
+
"example": {
|
|
205
|
+
"sql": "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') FROM orders",
|
|
206
|
+
"source_dialect": "mysql",
|
|
207
|
+
"target_dialect": "postgres",
|
|
208
|
+
"pretty": True
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
class ConvertResponse(BaseModel):
|
|
213
|
+
"""SQL conversion response model."""
|
|
214
|
+
success: bool = Field(..., description="Whether conversion was successful")
|
|
215
|
+
source_sql: str = Field(..., description="Original SQL statement")
|
|
216
|
+
target_sql: Optional[str] = Field(None, description="Converted SQL statement")
|
|
217
|
+
source_dialect: str = Field(..., description="Source dialect")
|
|
218
|
+
target_dialect: str = Field(..., description="Target dialect")
|
|
219
|
+
error: Optional[str] = Field(None, description="Error message if conversion failed")
|
|
220
|
+
error_code: Optional[str] = Field(None, description="Stable machine-readable error category")
|
|
221
|
+
compatibility_notes: List[str] = Field(default_factory=list, description="Compatibility notes")
|
|
222
|
+
transformations: List[str] = Field(default_factory=list, description="Applied transformations")
|
|
223
|
+
warnings: List[str] = Field(default_factory=list, description="Conversion warnings")
|
|
224
|
+
target_validation_state: str = Field(
|
|
225
|
+
"target_valid",
|
|
226
|
+
description=(
|
|
227
|
+
"Target-dialect parse status of the produced output. "
|
|
228
|
+
'"target_valid": target parser accepted the output; '
|
|
229
|
+
'"generic_only": target parser rejected it but the generic parser '
|
|
230
|
+
'accepted it — the conversion is retained with a warning and must '
|
|
231
|
+
'not be read as target-validated; '
|
|
232
|
+
'"invalid": neither parser accepts the output.'
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat(), description="Response timestamp")
|
|
236
|
+
|
|
237
|
+
class NL2SQLRequest(BaseModel):
|
|
238
|
+
"""Natural language to SQL request model."""
|
|
239
|
+
text: str = Field(..., description="Natural language query (Chinese or English)")
|
|
240
|
+
dialect: str = Field("hive", description="Target SQL dialect")
|
|
241
|
+
table_hint: Optional[str] = Field(None, description="Optional table name hint")
|
|
242
|
+
column_hints: Optional[List[str]] = Field(
|
|
243
|
+
None,
|
|
244
|
+
description="Optional column identifier hints; accepts simple or dotted identifiers",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
model_config = ConfigDict(json_schema_extra={
|
|
248
|
+
"example": {
|
|
249
|
+
"text": "统计每个部门的员工数量",
|
|
250
|
+
"dialect": "mysql",
|
|
251
|
+
"table_hint": "employees",
|
|
252
|
+
"column_hints": ["employees.department", "employees.id"]
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
class NL2SQLResponse(BaseModel):
|
|
257
|
+
"""Natural language to SQL response model."""
|
|
258
|
+
success: bool = Field(..., description="Whether generation was successful")
|
|
259
|
+
input_text: str = Field(..., description="Original natural language input")
|
|
260
|
+
sql: Optional[str] = Field(None, description="Generated SQL statement")
|
|
261
|
+
dialect: str = Field(..., description="Target dialect")
|
|
262
|
+
explanation: str = Field("", description="Explanation of generated SQL")
|
|
263
|
+
confidence: float = Field(0.0, description="Confidence score (0.0-1.0)")
|
|
264
|
+
suggestions: List[str] = Field(default_factory=list, description="Improvement suggestions")
|
|
265
|
+
|
|
266
|
+
class ParseRequest(BaseModel):
|
|
267
|
+
"""SQL parse request model."""
|
|
268
|
+
sql: str = Field(..., description="SQL statement to parse")
|
|
269
|
+
dialect: str = Field("hive", description="SQL dialect")
|
|
270
|
+
|
|
271
|
+
class TypeMapRequest(BaseModel):
|
|
272
|
+
"""Type mapping request model."""
|
|
273
|
+
type_name: str = Field(..., description="Source data type name")
|
|
274
|
+
source_dialect: str = Field(..., description="Source database dialect")
|
|
275
|
+
target_dialect: str = Field(..., description="Target database dialect")
|
|
276
|
+
|
|
277
|
+
model_config = ConfigDict(json_schema_extra={
|
|
278
|
+
"example": {
|
|
279
|
+
"type_name": "VARCHAR",
|
|
280
|
+
"source_dialect": "mysql",
|
|
281
|
+
"target_dialect": "postgres"
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
class APIResponse(BaseModel):
|
|
286
|
+
"""Standard API response wrapper."""
|
|
287
|
+
success: bool = Field(..., description="Operation success status")
|
|
288
|
+
data: Optional[Dict[str, Any]] = Field(None, description="Response data")
|
|
289
|
+
error: Optional[str] = Field(None, description="Error message if failed")
|
|
290
|
+
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
|
291
|
+
|
|
292
|
+
# === API Endpoints ===
|
|
293
|
+
|
|
294
|
+
@app.get("/", tags=["system"])
|
|
295
|
+
async def root():
|
|
296
|
+
"""
|
|
297
|
+
🏠 API Root - Welcome & Service Information
|
|
298
|
+
|
|
299
|
+
Returns comprehensive API information including:
|
|
300
|
+
- Service statistics
|
|
301
|
+
- Available endpoints
|
|
302
|
+
- Quick start guide
|
|
303
|
+
"""
|
|
304
|
+
return {
|
|
305
|
+
"name": API_TITLE,
|
|
306
|
+
"version": API_VERSION,
|
|
307
|
+
"status": "✅ Online",
|
|
308
|
+
"description": "Enterprise-grade multi-database SQL conversion engine",
|
|
309
|
+
"stats": {
|
|
310
|
+
"functions": {"count": len(func_encyclopedia.functions), "label": "📚 SQL Functions"},
|
|
311
|
+
"types": {"count": len(type_mapper.mappings), "label": "🗂️ Data Types"},
|
|
312
|
+
"dialects": {"count": len(SUPPORTED_DIALECTS), "label": "💾 Databases"},
|
|
313
|
+
"rules": {"count": len(transpiler.post_processor.engine.rules), "label": "🔧 Conversion Rules"}
|
|
314
|
+
},
|
|
315
|
+
"endpoints": {
|
|
316
|
+
"🔄 conversion": {"url": "/api/convert", "method": "POST"},
|
|
317
|
+
"📝 parsing": {"url": "/api/parse", "method": "POST"},
|
|
318
|
+
"💾 dialects": {"url": "/api/dialects", "method": "GET"},
|
|
319
|
+
"📚 functions": {"url": "/api/functions", "method": "GET"},
|
|
320
|
+
"🗂️ types": {"url": "/api/types", "method": "GET"},
|
|
321
|
+
"💬 nl2sql": {"url": "/api/nl2sql", "method": "POST"},
|
|
322
|
+
"❤️ health": {"url": "/health", "method": "GET"},
|
|
323
|
+
"🟢 readiness": {"url": "/ready", "method": "GET"},
|
|
324
|
+
"🔍 deep health": {"url": "/health/deep", "method": "GET"},
|
|
325
|
+
"📖 docs": {"url": "/docs", "method": "GET"}
|
|
326
|
+
},
|
|
327
|
+
"quick_start": {
|
|
328
|
+
"example": {
|
|
329
|
+
"endpoint": "POST /api/convert",
|
|
330
|
+
"body": {
|
|
331
|
+
"sql": "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') FROM orders",
|
|
332
|
+
"source_dialect": "mysql",
|
|
333
|
+
"target_dialect": "postgres"
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
"timestamp": datetime.now().isoformat()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@app.get("/api/dialects", tags=["system"])
|
|
341
|
+
async def get_dialects():
|
|
342
|
+
"""
|
|
343
|
+
💾 Get Supported SQL Dialects
|
|
344
|
+
|
|
345
|
+
Returns all 12 supported database dialects with:
|
|
346
|
+
- Display name and icon
|
|
347
|
+
- Category (RDBMS, Big Data, Cloud DW, etc.)
|
|
348
|
+
- Grouped by category for easy navigation
|
|
349
|
+
"""
|
|
350
|
+
# Group dialects by category
|
|
351
|
+
by_category = {}
|
|
352
|
+
for dialect, info in DIALECT_INFO.items():
|
|
353
|
+
cat = info["category"]
|
|
354
|
+
if cat not in by_category:
|
|
355
|
+
by_category[cat] = []
|
|
356
|
+
by_category[cat].append({
|
|
357
|
+
"id": dialect,
|
|
358
|
+
"name": info["name"],
|
|
359
|
+
"icon": info["icon"]
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
"success": True,
|
|
364
|
+
"dialects": SUPPORTED_DIALECTS,
|
|
365
|
+
"count": len(SUPPORTED_DIALECTS),
|
|
366
|
+
"details": DIALECT_INFO,
|
|
367
|
+
"by_category": by_category,
|
|
368
|
+
"categories": list(by_category.keys())
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@app.post("/api/convert", response_model=ConvertResponse, tags=["conversion"])
|
|
372
|
+
async def convert_sql(request: ConvertRequest):
|
|
373
|
+
"""
|
|
374
|
+
Convert SQL from one dialect to another.
|
|
375
|
+
|
|
376
|
+
Supports conversion between 12 database dialects with:
|
|
377
|
+
- Automatic syntax transformation
|
|
378
|
+
- Function mapping
|
|
379
|
+
- Type conversion
|
|
380
|
+
- Compatibility notes
|
|
381
|
+
|
|
382
|
+
**Example:**
|
|
383
|
+
```json
|
|
384
|
+
{
|
|
385
|
+
"sql": "SELECT DATE_FORMAT(created_at, '%Y-%m-%d') FROM orders",
|
|
386
|
+
"source_dialect": "mysql",
|
|
387
|
+
"target_dialect": "postgres"
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
"""
|
|
391
|
+
source_dialect = request.source_dialect.strip().lower()
|
|
392
|
+
target_dialect = request.target_dialect.strip().lower()
|
|
393
|
+
if source_dialect not in SUPPORTED_DIALECTS:
|
|
394
|
+
raise HTTPException(
|
|
395
|
+
status_code=400,
|
|
396
|
+
detail=f"Unsupported source dialect: {request.source_dialect}. Supported: {SUPPORTED_DIALECTS}"
|
|
397
|
+
)
|
|
398
|
+
if target_dialect not in SUPPORTED_DIALECTS:
|
|
399
|
+
raise HTTPException(
|
|
400
|
+
status_code=400,
|
|
401
|
+
detail=f"Unsupported target dialect: {request.target_dialect}. Supported: {SUPPORTED_DIALECTS}"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
result = transpiler.transpile(
|
|
405
|
+
request.sql,
|
|
406
|
+
source_dialect,
|
|
407
|
+
target_dialect,
|
|
408
|
+
request.pretty
|
|
409
|
+
)
|
|
410
|
+
return ConvertResponse(
|
|
411
|
+
success=result.success,
|
|
412
|
+
source_sql=result.source_sql,
|
|
413
|
+
target_sql=result.target_sql,
|
|
414
|
+
source_dialect=result.source_dialect,
|
|
415
|
+
target_dialect=result.target_dialect,
|
|
416
|
+
error=result.error,
|
|
417
|
+
error_code=result.error_code,
|
|
418
|
+
compatibility_notes=result.compatibility_notes,
|
|
419
|
+
transformations=result.transformations,
|
|
420
|
+
warnings=result.warnings,
|
|
421
|
+
target_validation_state=result.target_validation_state,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
@app.post("/api/parse", tags=["conversion"])
|
|
425
|
+
async def parse_sql(request: ParseRequest):
|
|
426
|
+
"""
|
|
427
|
+
Parse SQL and extract elements.
|
|
428
|
+
|
|
429
|
+
Extracts tables, columns, functions, and other elements from SQL.
|
|
430
|
+
"""
|
|
431
|
+
dialect = normalize_dialect(request.dialect)
|
|
432
|
+
parser = SQLParser(dialect)
|
|
433
|
+
elements = parser.extract_elements(request.sql)
|
|
434
|
+
return {
|
|
435
|
+
"success": True,
|
|
436
|
+
"dialect": dialect,
|
|
437
|
+
"elements": elements,
|
|
438
|
+
"timestamp": datetime.now().isoformat()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
@app.get("/api/functions", tags=["functions"])
|
|
442
|
+
async def list_functions(
|
|
443
|
+
search: Optional[str] = Query(None, description="Search term for function name"),
|
|
444
|
+
category: Optional[str] = Query(None, description="Filter by category (string, date, math, etc.)"),
|
|
445
|
+
limit: int = Query(50, ge=1, le=500, description="Maximum number of results")
|
|
446
|
+
):
|
|
447
|
+
"""
|
|
448
|
+
List or search SQL functions.
|
|
449
|
+
|
|
450
|
+
**Categories:** string, date, math, aggregate, window, conditional, conversion, json, array, system, geo
|
|
451
|
+
|
|
452
|
+
**Examples:**
|
|
453
|
+
- `/api/functions?search=date` - Search for date functions
|
|
454
|
+
- `/api/functions?category=string` - List all string functions
|
|
455
|
+
- `/api/functions?limit=100` - Get first 100 functions
|
|
456
|
+
"""
|
|
457
|
+
if search:
|
|
458
|
+
functions = func_encyclopedia.search(search, limit)
|
|
459
|
+
elif category:
|
|
460
|
+
functions = func_encyclopedia.list_by_category(category)
|
|
461
|
+
else:
|
|
462
|
+
functions = func_encyclopedia.functions[:limit]
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
"success": True,
|
|
466
|
+
"functions": functions,
|
|
467
|
+
"count": len(functions),
|
|
468
|
+
"total": len(func_encyclopedia.functions),
|
|
469
|
+
"stats": func_encyclopedia.get_stats()
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
@app.get("/api/functions/categories", tags=["functions"])
|
|
473
|
+
async def get_function_categories():
|
|
474
|
+
"""
|
|
475
|
+
Get all function categories with counts.
|
|
476
|
+
|
|
477
|
+
Returns list of categories and number of functions in each.
|
|
478
|
+
"""
|
|
479
|
+
categories = func_encyclopedia.get_all_categories()
|
|
480
|
+
return {
|
|
481
|
+
"success": True,
|
|
482
|
+
"categories": categories,
|
|
483
|
+
"count": len(categories)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
@app.get("/api/functions/{name}", tags=["functions"])
|
|
487
|
+
async def get_function(name: str):
|
|
488
|
+
"""
|
|
489
|
+
Get detailed function information.
|
|
490
|
+
|
|
491
|
+
Returns function details with syntax for all 12 database dialects.
|
|
492
|
+
"""
|
|
493
|
+
func = func_encyclopedia.get_function(name)
|
|
494
|
+
if not func:
|
|
495
|
+
raise HTTPException(
|
|
496
|
+
status_code=404,
|
|
497
|
+
detail=f"Function '{name}' not found. Try searching with /api/functions?search={name}"
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
comparison = func_encyclopedia.compare_dialects(name)
|
|
501
|
+
return {
|
|
502
|
+
"success": True,
|
|
503
|
+
"function": func,
|
|
504
|
+
"comparison": comparison
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
@app.get("/api/types", tags=["types"])
|
|
508
|
+
async def list_types(
|
|
509
|
+
source: Optional[str] = Query(None, description="Filter by source dialect"),
|
|
510
|
+
target: Optional[str] = Query(None, description="Filter by target dialect")
|
|
511
|
+
):
|
|
512
|
+
"""
|
|
513
|
+
Get type mapping matrix.
|
|
514
|
+
|
|
515
|
+
Returns the complete type mapping matrix (canonical type count × 12 databases).
|
|
516
|
+
|
|
517
|
+
**Type Categories:**
|
|
518
|
+
- String: STRING, VARCHAR, CHAR, TEXT, etc.
|
|
519
|
+
- Numeric: BIGINT, INT, DECIMAL, FLOAT, etc.
|
|
520
|
+
- Date/Time: DATE, TIME, TIMESTAMP, INTERVAL
|
|
521
|
+
- Complex: ARRAY, MAP, STRUCT, JSON
|
|
522
|
+
- Special: UUID, INET, GEOMETRY
|
|
523
|
+
"""
|
|
524
|
+
if source is not None:
|
|
525
|
+
source = normalize_dialect(source, "source dialect")
|
|
526
|
+
if target is not None:
|
|
527
|
+
target = normalize_dialect(target, "target dialect")
|
|
528
|
+
matrix = type_mapper.get_matrix(source, target)
|
|
529
|
+
return {
|
|
530
|
+
"success": True,
|
|
531
|
+
"mappings": matrix,
|
|
532
|
+
"dialects": type_mapper.DIALECTS,
|
|
533
|
+
"type_count": len(matrix),
|
|
534
|
+
"precision_warnings": type_mapper.get_precision_warnings()
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
@app.get("/api/types/{type_name}", tags=["types"])
|
|
538
|
+
async def get_type(type_name: str):
|
|
539
|
+
"""
|
|
540
|
+
Get type mapping details for a specific type.
|
|
541
|
+
|
|
542
|
+
Returns mapping for all 12 database dialects.
|
|
543
|
+
"""
|
|
544
|
+
comparison = type_mapper.compare_types(type_name)
|
|
545
|
+
if "error" in comparison:
|
|
546
|
+
raise HTTPException(
|
|
547
|
+
status_code=404,
|
|
548
|
+
detail=f"Type '{type_name}' not found. Available types: STRING, VARCHAR, INT, BIGINT, DECIMAL, DATE, TIMESTAMP, ARRAY, MAP, JSON, etc."
|
|
549
|
+
)
|
|
550
|
+
return {
|
|
551
|
+
"success": True,
|
|
552
|
+
**comparison
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
@app.post("/api/types/map", tags=["types"])
|
|
556
|
+
async def map_type(request: TypeMapRequest):
|
|
557
|
+
"""
|
|
558
|
+
Map a type from source to target dialect.
|
|
559
|
+
|
|
560
|
+
Returns the equivalent type in the target database with notes.
|
|
561
|
+
"""
|
|
562
|
+
source_dialect = normalize_dialect(request.source_dialect, "source dialect")
|
|
563
|
+
target_dialect = normalize_dialect(request.target_dialect, "target dialect")
|
|
564
|
+
|
|
565
|
+
result = type_mapper.suggest_type(
|
|
566
|
+
request.type_name,
|
|
567
|
+
source_dialect,
|
|
568
|
+
target_dialect,
|
|
569
|
+
)
|
|
570
|
+
return {
|
|
571
|
+
"success": True,
|
|
572
|
+
**result,
|
|
573
|
+
"timestamp": datetime.now().isoformat()
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
@app.post("/api/nl2sql", response_model=NL2SQLResponse, tags=["nl2sql"])
|
|
577
|
+
async def generate_sql(request: NL2SQLRequest):
|
|
578
|
+
"""
|
|
579
|
+
Generate SQL from natural language.
|
|
580
|
+
|
|
581
|
+
Supports both Chinese and English input.
|
|
582
|
+
|
|
583
|
+
**Examples:**
|
|
584
|
+
- "查询最近7天的订单" → SELECT * FROM orders WHERE created_at >= DATE_SUB(CURRENT_DATE, 7)
|
|
585
|
+
- "统计每个部门的员工数量" → SELECT dept, COUNT(*) FROM employees GROUP BY dept
|
|
586
|
+
- "Get top 10 users by score" → SELECT * FROM users ORDER BY score DESC LIMIT 10
|
|
587
|
+
"""
|
|
588
|
+
dialect = normalize_dialect(request.dialect)
|
|
589
|
+
result = nl2sql_generator.generate(
|
|
590
|
+
request.text,
|
|
591
|
+
dialect,
|
|
592
|
+
request.table_hint,
|
|
593
|
+
request.column_hints,
|
|
594
|
+
)
|
|
595
|
+
return NL2SQLResponse(
|
|
596
|
+
success=result.success,
|
|
597
|
+
input_text=result.input_text,
|
|
598
|
+
sql=result.sql,
|
|
599
|
+
dialect=dialect,
|
|
600
|
+
explanation=result.explanation,
|
|
601
|
+
confidence=result.confidence,
|
|
602
|
+
suggestions=result.suggestions
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
@app.get("/health", tags=["system"])
|
|
606
|
+
async def health_check():
|
|
607
|
+
"""Return process liveness without probing business dependencies."""
|
|
608
|
+
return {
|
|
609
|
+
"status": "alive",
|
|
610
|
+
"version": API_VERSION,
|
|
611
|
+
"timestamp": datetime.now().isoformat()
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
@app.get("/ready", tags=["system"])
|
|
615
|
+
async def readiness_check(request: Request):
|
|
616
|
+
"""Return readiness after authorized dependency probes."""
|
|
617
|
+
return await health_probe_response(request)
|
|
618
|
+
|
|
619
|
+
@app.get("/health/deep", tags=["system"])
|
|
620
|
+
async def deep_health_check(request: Request):
|
|
621
|
+
"""Return deep health using the shared authorized dependency probe service."""
|
|
622
|
+
return await health_probe_response(request)
|
|
623
|
+
|
|
624
|
+
@app.get("/api/stats", tags=["system"])
|
|
625
|
+
async def get_stats():
|
|
626
|
+
"""
|
|
627
|
+
📊 Get API Statistics
|
|
628
|
+
|
|
629
|
+
Returns comprehensive statistics about:
|
|
630
|
+
- Function encyclopedia
|
|
631
|
+
- Type mappings
|
|
632
|
+
- Conversion rules
|
|
633
|
+
- Supported dialects
|
|
634
|
+
"""
|
|
635
|
+
func_categories = func_encyclopedia.get_all_categories()
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
"success": True,
|
|
639
|
+
"stats": {
|
|
640
|
+
"overview": {
|
|
641
|
+
"total_functions": len(func_encyclopedia.functions),
|
|
642
|
+
"total_types": len(type_mapper.mappings),
|
|
643
|
+
"total_dialects": len(SUPPORTED_DIALECTS),
|
|
644
|
+
"conversion_rules": len(transpiler.post_processor.engine.rules)
|
|
645
|
+
},
|
|
646
|
+
"functions": {
|
|
647
|
+
"total": len(func_encyclopedia.functions),
|
|
648
|
+
"categories": func_categories,
|
|
649
|
+
"category_count": len(func_categories)
|
|
650
|
+
},
|
|
651
|
+
"types": {
|
|
652
|
+
"total": len(type_mapper.mappings),
|
|
653
|
+
"dialects": len(type_mapper.DIALECTS),
|
|
654
|
+
"categories": ["String", "Numeric", "Date/Time", "Complex", "Binary", "Special"]
|
|
655
|
+
},
|
|
656
|
+
"dialects": {
|
|
657
|
+
"list": SUPPORTED_DIALECTS,
|
|
658
|
+
"details": DIALECT_INFO
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
"timestamp": datetime.now().isoformat()
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
# Custom exception handler for better error responses
|
|
665
|
+
@app.exception_handler(HTTPException)
|
|
666
|
+
async def custom_http_exception_handler(request: Request, exc: HTTPException):
|
|
667
|
+
return JSONResponse(
|
|
668
|
+
status_code=exc.status_code,
|
|
669
|
+
content={
|
|
670
|
+
"success": False,
|
|
671
|
+
"error": {
|
|
672
|
+
"code": exc.status_code,
|
|
673
|
+
"message": exc.detail,
|
|
674
|
+
"type": "HTTPException"
|
|
675
|
+
},
|
|
676
|
+
"timestamp": datetime.now().isoformat()
|
|
677
|
+
}
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@app.exception_handler(SDMException)
|
|
682
|
+
async def sdm_exception_handler(request: Request, exc: SDMException):
|
|
683
|
+
"""Handle custom SDM exceptions."""
|
|
684
|
+
status_code = 400
|
|
685
|
+
if isinstance(exc, UnsupportedDialectError):
|
|
686
|
+
status_code = 400
|
|
687
|
+
|
|
688
|
+
error = exc.to_dict()
|
|
689
|
+
if "code" not in error:
|
|
690
|
+
error["code"] = exc.error_code.value if isinstance(exc.error_code, ErrorCode) else str(exc.error_code)
|
|
691
|
+
|
|
692
|
+
return JSONResponse(
|
|
693
|
+
status_code=status_code,
|
|
694
|
+
content={
|
|
695
|
+
"success": False,
|
|
696
|
+
"error": error,
|
|
697
|
+
"timestamp": datetime.now().isoformat()
|
|
698
|
+
}
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@app.exception_handler(Exception)
|
|
703
|
+
async def unhandled_exception_handler(request: Request, exc: Exception):
|
|
704
|
+
"""Return a generic 500 response while retaining request correlation internally."""
|
|
705
|
+
request_id = getattr(request.state, "request_id", None)
|
|
706
|
+
logger.exception("Unhandled API exception", extra={"request_id": request_id})
|
|
707
|
+
content = {
|
|
708
|
+
"success": False,
|
|
709
|
+
"error": {
|
|
710
|
+
"code": ErrorCode.INTERNAL_ERROR.value,
|
|
711
|
+
"message": "Internal server error",
|
|
712
|
+
},
|
|
713
|
+
"timestamp": datetime.now().isoformat(),
|
|
714
|
+
}
|
|
715
|
+
if request_id:
|
|
716
|
+
content["request_id"] = request_id
|
|
717
|
+
return JSONResponse(status_code=500, content=content)
|
|
718
|
+
|
|
719
|
+
# Run with: uvicorn backend.api.main:app --reload
|
|
720
|
+
if __name__ == "__main__":
|
|
721
|
+
import uvicorn
|
|
722
|
+
print(f"""
|
|
723
|
+
╔══════════════════════════════════════════════════════════╗
|
|
724
|
+
║ 🔄 SQL Dialect Master API v{API_VERSION} ║
|
|
725
|
+
║ ────────────────────────────────────────────────────── ║
|
|
726
|
+
║ 📚 Functions: {len(func_encyclopedia.functions):>3} │ 🗂️ Types: {len(type_mapper.mappings):>2} │ 💾 DBs: {len(SUPPORTED_DIALECTS):>2} ║
|
|
727
|
+
║ ────────────────────────────────────────────────────── ║
|
|
728
|
+
║ 📖 Docs: http://localhost:8000/docs ║
|
|
729
|
+
║ ❤️ Health: http://localhost:8000/health ║
|
|
730
|
+
╚══════════════════════════════════════════════════════════╝
|
|
731
|
+
""")
|
|
732
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|