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
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Language mappings for NL2SQL.
|
|
3
|
+
|
|
4
|
+
Contains Chinese-English keyword mappings for tables, columns, and SQL operations.
|
|
5
|
+
"""
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Chinese-English keyword mappings
|
|
10
|
+
KEYWORDS: Dict[str, str] = {
|
|
11
|
+
# Query operations
|
|
12
|
+
"查询": "SELECT", "获取": "SELECT", "查找": "SELECT", "显示": "SELECT", "列出": "SELECT",
|
|
13
|
+
"select": "SELECT", "get": "SELECT", "find": "SELECT", "show": "SELECT", "list": "SELECT", "fetch": "SELECT",
|
|
14
|
+
# Insert operations
|
|
15
|
+
"插入": "INSERT", "添加": "INSERT", "新增": "INSERT", "创建": "INSERT",
|
|
16
|
+
"insert": "INSERT", "add": "INSERT", "create": "INSERT",
|
|
17
|
+
# Update operations
|
|
18
|
+
"更新": "UPDATE", "修改": "UPDATE", "改变": "UPDATE", "设置": "UPDATE",
|
|
19
|
+
"update": "UPDATE", "modify": "UPDATE", "change": "UPDATE", "set": "UPDATE",
|
|
20
|
+
# Delete operations
|
|
21
|
+
"删除": "DELETE", "移除": "DELETE", "清除": "DELETE",
|
|
22
|
+
"delete": "DELETE", "remove": "DELETE", "drop": "DELETE",
|
|
23
|
+
# Aggregations
|
|
24
|
+
"统计": "COUNT", "计数": "COUNT", "数量": "COUNT", "多少": "COUNT", "几个": "COUNT",
|
|
25
|
+
"count": "COUNT", "total": "COUNT", "how many": "COUNT",
|
|
26
|
+
"求和": "SUM", "总和": "SUM", "合计": "SUM", "总计": "SUM",
|
|
27
|
+
"sum": "SUM", "total of": "SUM",
|
|
28
|
+
"平均": "AVG", "均值": "AVG", "平均值": "AVG",
|
|
29
|
+
"average": "AVG", "avg": "AVG", "mean": "AVG",
|
|
30
|
+
"最大": "MAX", "最高": "MAX", "最多": "MAX",
|
|
31
|
+
"max": "MAX", "maximum": "MAX", "highest": "MAX", "largest": "MAX",
|
|
32
|
+
"最小": "MIN", "最低": "MIN", "最少": "MIN",
|
|
33
|
+
"min": "MIN", "minimum": "MIN", "lowest": "MIN", "smallest": "MIN",
|
|
34
|
+
# Conditions
|
|
35
|
+
"大于": ">", "超过": ">", "高于": ">", "多于": ">",
|
|
36
|
+
"greater": ">", "more than": ">", "above": ">", "over": ">",
|
|
37
|
+
"小于": "<", "低于": "<", "少于": "<", "不足": "<",
|
|
38
|
+
"less": "<", "less than": "<", "below": "<", "under": "<",
|
|
39
|
+
"等于": "=", "是": "=", "为": "=",
|
|
40
|
+
"equals": "=", "equal": "=", "is": "=",
|
|
41
|
+
"不等于": "!=", "不是": "!=", "不为": "!=",
|
|
42
|
+
"not equal": "!=", "not": "!=", "isn't": "!=",
|
|
43
|
+
"包含": "LIKE", "含有": "LIKE", "像": "LIKE",
|
|
44
|
+
"contains": "LIKE", "like": "LIKE", "includes": "LIKE",
|
|
45
|
+
"开头": "LIKE_START", "以...开头": "LIKE_START",
|
|
46
|
+
"starts with": "LIKE_START", "beginning with": "LIKE_START",
|
|
47
|
+
"结尾": "LIKE_END", "以...结尾": "LIKE_END",
|
|
48
|
+
"ends with": "LIKE_END", "ending with": "LIKE_END",
|
|
49
|
+
"之间": "BETWEEN", "范围": "BETWEEN",
|
|
50
|
+
"between": "BETWEEN", "range": "BETWEEN",
|
|
51
|
+
"为空": "IS NULL", "空值": "IS NULL",
|
|
52
|
+
"is null": "IS NULL", "null": "IS NULL", "empty": "IS NULL",
|
|
53
|
+
"非空": "IS NOT NULL", "不为空": "IS NOT NULL",
|
|
54
|
+
"is not null": "IS NOT NULL", "not null": "IS NOT NULL", "not empty": "IS NOT NULL",
|
|
55
|
+
# Sorting
|
|
56
|
+
"排序": "ORDER BY", "按": "ORDER BY", "排列": "ORDER BY",
|
|
57
|
+
"sort": "ORDER BY", "order": "ORDER BY", "sorted": "ORDER BY",
|
|
58
|
+
"升序": "ASC", "从小到大": "ASC", "递增": "ASC",
|
|
59
|
+
"ascending": "ASC", "asc": "ASC", "increasing": "ASC",
|
|
60
|
+
"降序": "DESC", "从大到小": "DESC", "递减": "DESC",
|
|
61
|
+
"descending": "DESC", "desc": "DESC", "decreasing": "DESC",
|
|
62
|
+
# Grouping
|
|
63
|
+
"分组": "GROUP BY", "按...分组": "GROUP BY", "汇总": "GROUP BY",
|
|
64
|
+
"group": "GROUP BY", "group by": "GROUP BY", "grouped": "GROUP BY",
|
|
65
|
+
# Limiting
|
|
66
|
+
"前": "LIMIT", "限制": "LIMIT", "只要": "LIMIT", "仅": "LIMIT",
|
|
67
|
+
"top": "LIMIT", "limit": "LIMIT", "first": "LIMIT", "only": "LIMIT",
|
|
68
|
+
# Joins
|
|
69
|
+
"关联": "JOIN", "连接": "JOIN", "合并": "JOIN", "联合": "JOIN",
|
|
70
|
+
"join": "JOIN", "combine": "JOIN", "merge": "JOIN",
|
|
71
|
+
"左连接": "LEFT JOIN", "left join": "LEFT JOIN",
|
|
72
|
+
"右连接": "RIGHT JOIN", "right join": "RIGHT JOIN",
|
|
73
|
+
"内连接": "INNER JOIN", "inner join": "INNER JOIN",
|
|
74
|
+
# Time
|
|
75
|
+
"今天": "CURRENT_DATE", "today": "CURRENT_DATE",
|
|
76
|
+
"昨天": "DATE_SUB(CURRENT_DATE, 1)", "yesterday": "DATE_SUB(CURRENT_DATE, 1)",
|
|
77
|
+
"前天": "DATE_SUB(CURRENT_DATE, 2)", "day before yesterday": "DATE_SUB(CURRENT_DATE, 2)",
|
|
78
|
+
"本周": "WEEK", "this week": "WEEK",
|
|
79
|
+
"本月": "MONTH", "this month": "MONTH",
|
|
80
|
+
"本年": "YEAR", "this year": "YEAR",
|
|
81
|
+
"最近": ">=", "recent": ">=", "last": ">=", "past": ">=",
|
|
82
|
+
# Distinct
|
|
83
|
+
"去重": "DISTINCT", "唯一": "DISTINCT", "不重复": "DISTINCT",
|
|
84
|
+
"distinct": "DISTINCT", "unique": "DISTINCT",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# Common table patterns (expanded)
|
|
88
|
+
TABLE_PATTERNS: Dict[str, str] = {
|
|
89
|
+
# Users
|
|
90
|
+
"用户": "users", "user": "users", "users": "users", "会员": "users", "member": "users",
|
|
91
|
+
"账户": "accounts", "account": "accounts", "accounts": "accounts",
|
|
92
|
+
# Orders
|
|
93
|
+
"订单": "orders", "order": "orders", "orders": "orders", "购买": "orders",
|
|
94
|
+
"销售": "sales", "sale": "sales", "sales": "sales",
|
|
95
|
+
# Products
|
|
96
|
+
"产品": "products", "product": "products", "products": "products",
|
|
97
|
+
"商品": "items", "item": "items", "items": "items", "货物": "items",
|
|
98
|
+
"库存": "inventory", "inventory": "inventory", "stock": "inventory",
|
|
99
|
+
# Logs
|
|
100
|
+
"日志": "logs", "log": "logs", "logs": "logs", "记录": "logs",
|
|
101
|
+
"操作日志": "operation_logs", "审计": "audit_logs", "audit": "audit_logs",
|
|
102
|
+
# HR
|
|
103
|
+
"员工": "employees", "employee": "employees", "employees": "employees", "职员": "employees",
|
|
104
|
+
"部门": "departments", "department": "departments", "departments": "departments",
|
|
105
|
+
"薪资": "salaries", "salary": "salaries", "工资": "salaries",
|
|
106
|
+
# Customers
|
|
107
|
+
"客户": "customers", "customer": "customers", "customers": "customers",
|
|
108
|
+
"供应商": "suppliers", "supplier": "suppliers", "vendors": "suppliers",
|
|
109
|
+
# Finance
|
|
110
|
+
"交易": "transactions", "transaction": "transactions", "transactions": "transactions",
|
|
111
|
+
"支付": "payments", "payment": "payments", "payments": "payments",
|
|
112
|
+
"发票": "invoices", "invoice": "invoices", "invoices": "invoices",
|
|
113
|
+
# Events
|
|
114
|
+
"事件": "events", "event": "events", "events": "events",
|
|
115
|
+
"活动": "activities", "activity": "activities",
|
|
116
|
+
# Data
|
|
117
|
+
"数据": "data", "data": "data", "表": "table", "table": "table",
|
|
118
|
+
"配置": "configs", "config": "configs", "settings": "configs",
|
|
119
|
+
# Categories
|
|
120
|
+
"分类": "categories", "categories": "categories",
|
|
121
|
+
"标签": "tags", "tag": "tags", "tags": "tags",
|
|
122
|
+
# Messages
|
|
123
|
+
"消息": "messages", "message": "messages", "messages": "messages",
|
|
124
|
+
"通知": "notifications", "notification": "notifications",
|
|
125
|
+
"评论": "comments", "comment": "comments", "comments": "comments",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Column patterns (expanded)
|
|
129
|
+
COLUMN_PATTERNS: Dict[str, str] = {
|
|
130
|
+
# Identity
|
|
131
|
+
"id": "id", "编号": "id", "标识": "id", "主键": "id",
|
|
132
|
+
"uuid": "uuid", "guid": "guid",
|
|
133
|
+
# Personal info
|
|
134
|
+
"姓名": "name", "名字": "name", "name": "name", "名称": "name",
|
|
135
|
+
"用户名": "username", "username": "username", "账号": "username",
|
|
136
|
+
"邮箱": "email", "email": "email", "电子邮件": "email",
|
|
137
|
+
"电话": "phone", "phone": "phone", "手机": "mobile", "mobile": "mobile",
|
|
138
|
+
"地址": "address", "address": "address", "住址": "address",
|
|
139
|
+
"年龄": "age", "age": "age",
|
|
140
|
+
"性别": "gender", "gender": "gender", "sex": "gender",
|
|
141
|
+
"生日": "birthday", "birthday": "birthday", "出生日期": "birth_date",
|
|
142
|
+
# Financial
|
|
143
|
+
"金额": "amount", "amount": "amount", "总额": "total_amount",
|
|
144
|
+
"价格": "price", "price": "price", "单价": "unit_price",
|
|
145
|
+
"数量": "quantity", "quantity": "quantity", "qty": "quantity",
|
|
146
|
+
"成本": "cost", "cost": "cost",
|
|
147
|
+
"利润": "profit", "profit": "profit",
|
|
148
|
+
"折扣": "discount", "discount": "discount",
|
|
149
|
+
"税": "tax", "tax": "tax",
|
|
150
|
+
"余额": "balance", "balance": "balance",
|
|
151
|
+
"薪水": "salary", "salary": "salary", "工资": "salary",
|
|
152
|
+
# Time
|
|
153
|
+
"日期": "date", "date": "date",
|
|
154
|
+
"时间": "time", "time": "time",
|
|
155
|
+
"创建时间": "created_at", "created_at": "created_at", "create_time": "created_at",
|
|
156
|
+
"更新时间": "updated_at", "updated_at": "updated_at", "update_time": "updated_at",
|
|
157
|
+
"开始时间": "start_time", "start_time": "start_time", "start_date": "start_date",
|
|
158
|
+
"结束时间": "end_time", "end_time": "end_time", "end_date": "end_date",
|
|
159
|
+
"过期时间": "expire_time", "expire_at": "expire_at",
|
|
160
|
+
# Status
|
|
161
|
+
"状态": "status", "status": "status",
|
|
162
|
+
"类型": "type", "type": "type", "种类": "type",
|
|
163
|
+
"级别": "level", "level": "level", "等级": "level",
|
|
164
|
+
"优先级": "priority", "priority": "priority",
|
|
165
|
+
"是否": "is_", "是否有效": "is_valid", "是否删除": "is_deleted",
|
|
166
|
+
# Content
|
|
167
|
+
"标题": "title", "title": "title",
|
|
168
|
+
"内容": "content", "content": "content",
|
|
169
|
+
"描述": "description", "description": "description", "desc": "description",
|
|
170
|
+
"备注": "remark", "remark": "remark", "note": "note", "notes": "notes",
|
|
171
|
+
# Relations
|
|
172
|
+
"用户id": "user_id", "user_id": "user_id",
|
|
173
|
+
"订单id": "order_id", "order_id": "order_id",
|
|
174
|
+
"产品id": "product_id", "product_id": "product_id",
|
|
175
|
+
"部门id": "dept_id", "dept_id": "dept_id", "department_id": "department_id",
|
|
176
|
+
"父级": "parent_id", "parent_id": "parent_id",
|
|
177
|
+
# Categorization
|
|
178
|
+
"分类": "category", "category": "category",
|
|
179
|
+
# Metrics
|
|
180
|
+
"得分": "score", "score": "score", "分数": "score",
|
|
181
|
+
"评分": "rating", "rating": "rating",
|
|
182
|
+
"浏览量": "views", "views": "views", "view_count": "view_count",
|
|
183
|
+
"点击量": "clicks", "clicks": "clicks",
|
|
184
|
+
"下载量": "downloads", "downloads": "downloads",
|
|
185
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""NL2SQL relation extraction: joins, join-key inference, relational keywords.
|
|
3
|
+
|
|
4
|
+
The join-key knowledge base (``JOIN_KEY_PATTERNS``) is the single
|
|
5
|
+
canonical source for inter-table relationships. Unknown relationships
|
|
6
|
+
return ``None`` so the caller can fail closed instead of fabricating
|
|
7
|
+
conditions.
|
|
8
|
+
"""
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
# Canonical join-key knowledge base.
|
|
14
|
+
# Pairs are stored both ways so lookups are order-independent.
|
|
15
|
+
JOIN_KEY_PATTERNS: Dict[tuple, str] = {
|
|
16
|
+
("users", "orders"): "users.id = orders.user_id",
|
|
17
|
+
("orders", "users"): "orders.user_id = users.id",
|
|
18
|
+
("users", "transactions"): "users.id = transactions.user_id",
|
|
19
|
+
("transactions", "users"): "transactions.user_id = users.id",
|
|
20
|
+
("orders", "products"): "orders.product_id = products.id",
|
|
21
|
+
("products", "orders"): "products.id = orders.product_id",
|
|
22
|
+
("users", "products"): "users.id = products.user_id",
|
|
23
|
+
("products", "users"): "products.user_id = users.id",
|
|
24
|
+
("employees", "departments"): "employees.dept_id = departments.id",
|
|
25
|
+
("departments", "employees"): "departments.id = employees.dept_id",
|
|
26
|
+
("orders", "payments"): "orders.id = payments.order_id",
|
|
27
|
+
("payments", "orders"): "payments.order_id = orders.id",
|
|
28
|
+
("customers", "orders"): "customers.id = orders.customer_id",
|
|
29
|
+
("orders", "customers"): "orders.customer_id = customers.id",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Negative relational existence keywords -> NOT EXISTS
|
|
33
|
+
_NOT_EXISTS_KEYWORDS = [
|
|
34
|
+
"without", "not have", "no ",
|
|
35
|
+
"who don't have", "who doesn't have",
|
|
36
|
+
"who do not have", "who does not have",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# Positive relational existence keywords -> EXISTS
|
|
40
|
+
_EXISTS_KEYWORDS = [
|
|
41
|
+
"who have", "who has", "that have", "that has",
|
|
42
|
+
"with orders", "with customers",
|
|
43
|
+
"with users", "with employees",
|
|
44
|
+
"with invoices", "with payments", "with transactions",
|
|
45
|
+
"with suppliers", "with products",
|
|
46
|
+
"who placed", "who made", "who created", "who submitted",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Explicit physical JOIN keywords, each mapped to its structured kind.
|
|
51
|
+
# Matched as whole words / substrings against the full text — never as
|
|
52
|
+
# "the first token of the sentence" (the legacy split()[0] bug).
|
|
53
|
+
_JOIN_KIND_KEYWORDS: Dict[str, str] = {
|
|
54
|
+
"left join": "LEFT",
|
|
55
|
+
"右连接": "RIGHT",
|
|
56
|
+
"right join": "RIGHT",
|
|
57
|
+
"inner join": "INNER",
|
|
58
|
+
"内连接": "INNER",
|
|
59
|
+
"outer join": "OUTER",
|
|
60
|
+
"cross join": "CROSS",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class JoinSpec:
|
|
66
|
+
"""Structured physical-join intent parsed from natural language.
|
|
67
|
+
|
|
68
|
+
``kind`` is one of ``"LEFT" | "RIGHT" | "INNER" | "OUTER" | "CROSS"
|
|
69
|
+
| "JOIN"`` (JOIN = unqualified, defaults to inner). ``matched_text``
|
|
70
|
+
records which keyword fired, for evidence.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
kind: str
|
|
74
|
+
matched_text: str
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_join_spec(text: str) -> Optional[JoinSpec]:
|
|
78
|
+
"""Parse an explicit physical-join keyword into a structured JoinSpec.
|
|
79
|
+
|
|
80
|
+
Returns ``None`` when the text carries no physical-join intent
|
|
81
|
+
(e.g. relational-existence phrases like "users who have orders"), in
|
|
82
|
+
which case the caller falls back to its EXISTS/JOIN inference.
|
|
83
|
+
|
|
84
|
+
Keywords are located anywhere in the text via whole-phrase
|
|
85
|
+
matching — the join type is never inferred from the first word of the
|
|
86
|
+
sentence.
|
|
87
|
+
"""
|
|
88
|
+
lowered = text.lower()
|
|
89
|
+
for keyword, kind in _JOIN_KIND_KEYWORDS.items():
|
|
90
|
+
if keyword.isascii():
|
|
91
|
+
# English: whole-word, case-insensitive.
|
|
92
|
+
if re.search(r"\b" + re.escape(keyword) + r"\b", lowered):
|
|
93
|
+
return JoinSpec(kind=kind, matched_text=keyword)
|
|
94
|
+
else:
|
|
95
|
+
# Chinese (or any non-ASCII keyword): direct substring.
|
|
96
|
+
if keyword in text:
|
|
97
|
+
return JoinSpec(kind=kind, matched_text=keyword)
|
|
98
|
+
# Unqualified physical "join" that is not part of a known qualified
|
|
99
|
+
# keyword — default to inner.
|
|
100
|
+
if re.search(r"\bjoin\b", lowered):
|
|
101
|
+
return JoinSpec(kind="JOIN", matched_text="join")
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def guess_join_key(table1: str, table2: str) -> Optional[str]:
|
|
106
|
+
"""Return the canonical join condition between two tables, or None.
|
|
107
|
+
|
|
108
|
+
None means the relationship is unknown; the caller must not
|
|
109
|
+
fabricate a condition.
|
|
110
|
+
"""
|
|
111
|
+
return JOIN_KEY_PATTERNS.get((table1, table2))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def extract_joins(
|
|
115
|
+
text: str,
|
|
116
|
+
table_patterns: Dict[str, str],
|
|
117
|
+
) -> List[Dict]:
|
|
118
|
+
"""Extract structured join contexts from text.
|
|
119
|
+
|
|
120
|
+
Detects relational existence patterns (e.g. "users who have orders")
|
|
121
|
+
and returns one dict per additional table, keyed as:
|
|
122
|
+
|
|
123
|
+
type - "EXISTS", "NOT EXISTS" or "JOIN"
|
|
124
|
+
table - the related table name
|
|
125
|
+
subject - the main (first-appearing) table name
|
|
126
|
+
condition - join key string, or None for unknown relationships
|
|
127
|
+
relational - True when type is EXISTS/NOT EXISTS
|
|
128
|
+
"""
|
|
129
|
+
joins = []
|
|
130
|
+
# Collect unique tables in text-appearance order (first occurrence wins).
|
|
131
|
+
# Exclude "order" when it is part of an "order by" sorting clause.
|
|
132
|
+
table_occurrences = []
|
|
133
|
+
for pattern, table in table_patterns.items():
|
|
134
|
+
if pattern == "order" and re.search(r"\border\b\s+by\b", text, re.IGNORECASE):
|
|
135
|
+
continue
|
|
136
|
+
if re.search(r"\b" + re.escape(pattern) + r"\b", text) and table not in [t for _, t in table_occurrences]:
|
|
137
|
+
table_occurrences.append((text.find(pattern), table))
|
|
138
|
+
tables_found = [t for _, t in sorted(table_occurrences)]
|
|
139
|
+
if len(tables_found) >= 2:
|
|
140
|
+
join_spec = parse_join_spec(text)
|
|
141
|
+
if any(k in text for k in _NOT_EXISTS_KEYWORDS):
|
|
142
|
+
join_type = "NOT EXISTS"
|
|
143
|
+
relational = True
|
|
144
|
+
elif any(k in text for k in _EXISTS_KEYWORDS):
|
|
145
|
+
join_type = "EXISTS"
|
|
146
|
+
relational = True
|
|
147
|
+
elif join_spec is not None:
|
|
148
|
+
# Structured physical-join intent. Unqualified "JOIN" normalizes
|
|
149
|
+
# to inner; qualified kinds map directly to their SQL keyword.
|
|
150
|
+
join_type = "INNER" if join_spec.kind == "JOIN" else join_spec.kind
|
|
151
|
+
relational = False
|
|
152
|
+
else:
|
|
153
|
+
join_type = "JOIN"
|
|
154
|
+
relational = False
|
|
155
|
+
|
|
156
|
+
main_table = tables_found[0]
|
|
157
|
+
for other_table in tables_found[1:]:
|
|
158
|
+
join_key = guess_join_key(main_table, other_table)
|
|
159
|
+
record = {
|
|
160
|
+
"type": join_type,
|
|
161
|
+
"table": other_table,
|
|
162
|
+
"subject": main_table,
|
|
163
|
+
"condition": join_key,
|
|
164
|
+
"relational": relational,
|
|
165
|
+
"join_spec": join_spec,
|
|
166
|
+
}
|
|
167
|
+
if join_key is None:
|
|
168
|
+
# Unknown relationship - do not fabricate a condition.
|
|
169
|
+
# Carry both sides of the pair so downstream fail-closed
|
|
170
|
+
# messages can name the actual unresolved relationship.
|
|
171
|
+
joins.append(record)
|
|
172
|
+
continue
|
|
173
|
+
joins.append(record)
|
|
174
|
+
return joins
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def has_relational_keyword(text: str) -> bool:
|
|
178
|
+
"""Check if text contains any relational existence keyword."""
|
|
179
|
+
all_keywords = _NOT_EXISTS_KEYWORDS + _EXISTS_KEYWORDS
|
|
180
|
+
return any(pattern in text for pattern in all_keywords)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Query Template module for NL2SQL.
|
|
3
|
+
|
|
4
|
+
Provides pattern-based SQL generation using prioritized templates.
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Optional, Dict, Any, Callable
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Clause markers that mean a single-purpose or catch-all template may truncate
|
|
15
|
+
# part of the user's intent. Composed queries must flow to enhanced parsing.
|
|
16
|
+
_COMPOSITION_MARKERS = (
|
|
17
|
+
"order by", "sort", "排序", "排列",
|
|
18
|
+
"group by", "grouped by", "分组", "汇总",
|
|
19
|
+
"limit", "top ", "top\t", "only ", "前",
|
|
20
|
+
" and ", " or ", "且", "并且", "或者", "或",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_TEMPLATE_BLOCKERS = {
|
|
24
|
+
"condition_query": _COMPOSITION_MARKERS,
|
|
25
|
+
"aggregate_query": _COMPOSITION_MARKERS,
|
|
26
|
+
"time_range_query": _COMPOSITION_MARKERS,
|
|
27
|
+
"select_with_columns": _COMPOSITION_MARKERS,
|
|
28
|
+
"simple_select": _COMPOSITION_MARKERS,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Keep template captures bounded. These patterns run against natural-language
|
|
32
|
+
# query text, so unbounded adjacent wildcards can cause excessive backtracking
|
|
33
|
+
# on adversarial input. 512 characters is ample for each individual template
|
|
34
|
+
# field while making the regex work bounded by construction.
|
|
35
|
+
_TEMPLATE_CAPTURE_LIMIT = 512
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class QueryTemplate:
|
|
40
|
+
"""Template for pattern-based SQL generation with priority.
|
|
41
|
+
|
|
42
|
+
Templates are matched in priority order (highest first).
|
|
43
|
+
Each template has a regex pattern and a SQL template with placeholders.
|
|
44
|
+
"""
|
|
45
|
+
name: str
|
|
46
|
+
pattern: str
|
|
47
|
+
sql_template: str
|
|
48
|
+
priority: int = 50
|
|
49
|
+
extractor: Optional[Callable] = None
|
|
50
|
+
_compiled_pattern: re.Pattern = field(init=False, repr=False, default=None)
|
|
51
|
+
|
|
52
|
+
def __post_init__(self):
|
|
53
|
+
"""Pre-compile regex pattern for better performance."""
|
|
54
|
+
try:
|
|
55
|
+
self._compiled_pattern = re.compile(self.pattern, re.IGNORECASE)
|
|
56
|
+
except re.error as e:
|
|
57
|
+
logger.warning(f"Failed to compile pattern for template {self.name}: {e}")
|
|
58
|
+
self._compiled_pattern = None
|
|
59
|
+
|
|
60
|
+
def match(self, text: str) -> Optional[Dict[str, Any]]:
|
|
61
|
+
"""Try to match this template against input text."""
|
|
62
|
+
normalized = text.lower()
|
|
63
|
+
blockers = _TEMPLATE_BLOCKERS.get(self.name, ())
|
|
64
|
+
if any(marker in normalized for marker in blockers):
|
|
65
|
+
logger.debug(
|
|
66
|
+
"Skipping template '%s' because composition markers require enhanced parsing",
|
|
67
|
+
self.name,
|
|
68
|
+
)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
if self._compiled_pattern is None:
|
|
72
|
+
match = re.search(self.pattern, text, re.IGNORECASE)
|
|
73
|
+
else:
|
|
74
|
+
match = self._compiled_pattern.search(text)
|
|
75
|
+
|
|
76
|
+
if match:
|
|
77
|
+
return match.groupdict() if match.groupdict() else {"match": match.group(0)}
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
DEFAULT_QUERY_TEMPLATES = [
|
|
82
|
+
QueryTemplate(
|
|
83
|
+
name="top_n_query",
|
|
84
|
+
pattern=rf"(?:查询|获取|get|show|find)?\s*(?:前|top|bottom|最低|最少|lowest|smallest)\s*(\d+)\s*(?:个|条|名)?\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}}?)(?:按|by)?\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}})?\s*(?:排序|排列|order)?",
|
|
85
|
+
sql_template="SELECT * FROM {table} ORDER BY {order_col} DESC LIMIT {n}",
|
|
86
|
+
priority=90,
|
|
87
|
+
),
|
|
88
|
+
QueryTemplate(
|
|
89
|
+
name="count_by_group",
|
|
90
|
+
pattern=rf"(?:统计|计算|count)\s*(?:每个|各个|each)?\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}}?)\s*(?:的|的数量|数量|有多少)",
|
|
91
|
+
sql_template="SELECT {group_col}, COUNT(*) AS count FROM {table} GROUP BY {group_col}",
|
|
92
|
+
priority=85,
|
|
93
|
+
),
|
|
94
|
+
QueryTemplate(
|
|
95
|
+
name="time_range_query",
|
|
96
|
+
pattern=r"(?:查询|获取|get|find)?\s*(?:最近|过去|last|past)\s*(\d+)\s*(天|周|月|年|days?|weeks?|months?|years?)\s*(?:的|内的)?\s*(.+)",
|
|
97
|
+
sql_template="SELECT * FROM {table} WHERE {date_col} >= DATE_SUB(CURRENT_DATE, {interval})",
|
|
98
|
+
priority=85,
|
|
99
|
+
),
|
|
100
|
+
QueryTemplate(
|
|
101
|
+
name="aggregate_query",
|
|
102
|
+
pattern=rf"(?:计算|求|get)?\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}}?)\s*(?:的)?\s*(平均|总和|最大|最小|average|sum|max|min)\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}})",
|
|
103
|
+
sql_template="SELECT {agg_func}({col}) FROM {table}",
|
|
104
|
+
priority=80,
|
|
105
|
+
),
|
|
106
|
+
QueryTemplate(
|
|
107
|
+
name="condition_query",
|
|
108
|
+
pattern=rf"(?:查询|获取|find|get)?\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}}?)\s*(大于|小于|等于|超过|不等于|greater(?:\s+than)?|less(?:\s+than)?|equal(?:\s+to)?|>|<|=)\s*(\d+\.?\d*)\s*(?:的)?\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}})?",
|
|
109
|
+
sql_template="SELECT * FROM {table} WHERE {col} {op} {value}",
|
|
110
|
+
priority=75,
|
|
111
|
+
),
|
|
112
|
+
QueryTemplate(
|
|
113
|
+
name="join_query",
|
|
114
|
+
pattern=rf"(?:查询|获取)?\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}}?)\s*(?:和|与|关联|连接|join)\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}}?)(?:的|数据)?",
|
|
115
|
+
sql_template="SELECT * FROM {table1} JOIN {table2} ON {join_condition}",
|
|
116
|
+
priority=70,
|
|
117
|
+
),
|
|
118
|
+
QueryTemplate(
|
|
119
|
+
name="select_with_columns",
|
|
120
|
+
pattern=rf"(?:查询|获取|显示|select|get|show)\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}}?)\s*(?:的|from)?\s*(.{{1,{_TEMPLATE_CAPTURE_LIMIT}}}?)(?:表|table)?$",
|
|
121
|
+
sql_template="SELECT {columns} FROM {table}",
|
|
122
|
+
priority=60,
|
|
123
|
+
),
|
|
124
|
+
QueryTemplate(
|
|
125
|
+
name="simple_select",
|
|
126
|
+
pattern=r"(?:查询|获取|显示|列出|select|get|show|list|find)\s*(?:所有|全部|all)?\s*(.+)",
|
|
127
|
+
sql_template="SELECT * FROM {table}",
|
|
128
|
+
priority=40,
|
|
129
|
+
),
|
|
130
|
+
]
|