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.
Files changed (86) hide show
  1. backend/__init__.py +1 -0
  2. backend/api/__init__.py +1 -0
  3. backend/api/main.py +732 -0
  4. backend/api/middleware.py +274 -0
  5. backend/api/rate_limit_store.py +105 -0
  6. backend/api/readiness.py +178 -0
  7. backend/core/__init__.py +57 -0
  8. backend/core/cache.py +261 -0
  9. backend/core/column_hint_validation.py +22 -0
  10. backend/core/config.py +546 -0
  11. backend/core/constants.py +84 -0
  12. backend/core/exceptions.py +124 -0
  13. backend/core/function_call_scanner.py +64 -0
  14. backend/core/function_semantics.py +148 -0
  15. backend/core/functions_db.json +601 -0
  16. backend/core/functions_lookup.py +332 -0
  17. backend/core/metadata.py +71 -0
  18. backend/core/nl2sql.py +85 -0
  19. backend/core/nl2sql_components/__init__.py +72 -0
  20. backend/core/nl2sql_components/aggregations.py +35 -0
  21. backend/core/nl2sql_components/boolean_conditions.py +279 -0
  22. backend/core/nl2sql_components/core_extract.py +106 -0
  23. backend/core/nl2sql_components/evidence.py +122 -0
  24. backend/core/nl2sql_components/mappings.py +185 -0
  25. backend/core/nl2sql_components/relations.py +180 -0
  26. backend/core/nl2sql_components/templates.py +130 -0
  27. backend/core/nl2sql_components/tokenizer.py +187 -0
  28. backend/core/nl2sql_legacy.py +22 -0
  29. backend/core/nl2sql_pipeline/__init__.py +13 -0
  30. backend/core/nl2sql_pipeline/aggregations.py +26 -0
  31. backend/core/nl2sql_pipeline/clause_extract.py +97 -0
  32. backend/core/nl2sql_pipeline/dialect.py +139 -0
  33. backend/core/nl2sql_pipeline/intent.py +85 -0
  34. backend/core/nl2sql_pipeline/ir.py +70 -0
  35. backend/core/nl2sql_pipeline/models.py +30 -0
  36. backend/core/nl2sql_pipeline/orchestrator.py +576 -0
  37. backend/core/nl2sql_pipeline/predicates.py +182 -0
  38. backend/core/nl2sql_pipeline/renderer.py +248 -0
  39. backend/core/nl2sql_pipeline/resolution.py +69 -0
  40. backend/core/nl2sql_pipeline/suggestions.py +30 -0
  41. backend/core/nl2sql_pipeline/templates.py +371 -0
  42. backend/core/nl2sql_pipeline/validation.py +28 -0
  43. backend/core/p1_sql_scanner.py +196 -0
  44. backend/core/p2_data_validation.py +72 -0
  45. backend/core/parser.py +280 -0
  46. backend/core/post_processor.py +129 -0
  47. backend/core/rules.py +808 -0
  48. backend/core/semantic_diff.py +385 -0
  49. backend/core/transpiler.py +791 -0
  50. backend/core/type_mapping.json +593 -0
  51. backend/core/type_mapping.py +765 -0
  52. backend/utils/__init__.py +2 -0
  53. backend/utils/validators.py +407 -0
  54. frontend/__init__.py +10 -0
  55. frontend/app_context_v2.py +201 -0
  56. frontend/core/__init__.py +33 -0
  57. frontend/core/design_tokens.py +127 -0
  58. frontend/core/escaping.py +13 -0
  59. frontend/core/navigation.py +135 -0
  60. frontend/core/state.py +612 -0
  61. frontend/core/styles.py +523 -0
  62. frontend/core/themes.py +69 -0
  63. frontend/core/viewmodels.py +499 -0
  64. frontend/pages/__init__.py +0 -0
  65. frontend/pages/convert.py +283 -0
  66. frontend/pages/diff.py +157 -0
  67. frontend/pages/functions.py +100 -0
  68. frontend/pages/history.py +144 -0
  69. frontend/pages/lineage.py +168 -0
  70. frontend/pages/nl2sql.py +305 -0
  71. frontend/pages/query_analysis.py +120 -0
  72. frontend/pages/runtime.py +66 -0
  73. frontend/pages/settings.py +60 -0
  74. frontend/pages/templates.py +67 -0
  75. frontend/pages/types.py +110 -0
  76. frontend/templates_v2.py +53 -0
  77. frontend/ui/__init__.py +0 -0
  78. frontend/ui/command_palette.py +153 -0
  79. frontend/ui/diff.py +275 -0
  80. frontend/ui/editor.py +102 -0
  81. frontend/ui/status.py +255 -0
  82. sql_dialect_master-1.1.1.dist-info/METADATA +360 -0
  83. sql_dialect_master-1.1.1.dist-info/RECORD +86 -0
  84. sql_dialect_master-1.1.1.dist-info/WHEEL +5 -0
  85. sql_dialect_master-1.1.1.dist-info/licenses/LICENSE +21 -0
  86. sql_dialect_master-1.1.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,279 @@
1
+ """Boolean condition extraction for NL2SQL.
2
+
3
+ Extract explicit comparison, range, text, null, status, and set-membership
4
+ predicates while preserving normal SQL AND-over-OR precedence and
5
+ user-supplied parentheses.
6
+ """
7
+ import re
8
+ from typing import List, Optional, Tuple
9
+
10
+
11
+ _COMPARISON_PATTERNS = (
12
+ re.compile(
13
+ r"(?<![A-Za-z0-9_])(price|quantity|amount|age|score|rating|views|clicks)\s+"
14
+ r"(greater(?:\s+than)?|more\s+than|above|over|less(?:\s+than)?|"
15
+ r"below|under|equal(?:\s+to)?|equals)\s+"
16
+ r"([0-9]+(?:\.[0-9]+)?)",
17
+ re.IGNORECASE,
18
+ ),
19
+ re.compile(
20
+ r"(?<![A-Za-z0-9_])(price|quantity|amount|age|score|rating|views|clicks)\s*"
21
+ r"(>=|<=|!=|=|>|<)\s*([0-9]+(?:\.[0-9]+)?)",
22
+ re.IGNORECASE,
23
+ ),
24
+ re.compile(
25
+ r"(价格|数量|金额|年龄|得分|评分|浏览量|点击量)\s*"
26
+ r"(大于等于|小于等于|不等于|大于|小于|超过|低于|等于)\s*"
27
+ r"([0-9]+(?:\.[0-9]+)?)"
28
+ ),
29
+ )
30
+
31
+ _RANGE_PATTERN = re.compile(
32
+ r"(?<![A-Za-z0-9_])(price|quantity|amount|age|score|rating|views|clicks)\s+"
33
+ r"(?:between\s+([0-9]+(?:\.[0-9]+)?)\s+and\s+([0-9]+(?:\.[0-9]+)?)|"
34
+ r"in\s+(?:the\s+)?range\s+([0-9]+(?:\.[0-9]+)?)\s*(?:to|-)\s*"
35
+ r"([0-9]+(?:\.[0-9]+)?))",
36
+ re.IGNORECASE,
37
+ )
38
+
39
+ _CN_RANGE_PATTERN = re.compile(
40
+ r"(价格|数量|金额|年龄|得分|评分|浏览量|点击量)\s*"
41
+ r"(?:在|介于)\s*([0-9]+(?:\.[0-9]+)?)\s*(?:和|到|至|-)\s*"
42
+ r"([0-9]+(?:\.[0-9]+)?)\s*(?:之间|范围内|以内)?"
43
+ )
44
+
45
+ _TEXT_PATTERN = re.compile(
46
+ r"(?<![A-Za-z0-9_])(name|title|description|category|brand)\s+"
47
+ r"(?:contains|containing|includes|including)\s+"
48
+ r"(?:the\s+)?[\"']?([^\"']+?)[\"']?(?=\s+(?:and|or)\s+|$)",
49
+ re.IGNORECASE,
50
+ )
51
+
52
+ _CN_TEXT_PATTERN = re.compile(
53
+ r"(名称|名字|标题|描述|分类|品牌)\s*(?:包含|含有|包括)\s*[“\"]?([^”\"]+?)[”\"]?(?=(?:且|并且|或者|或|和)|$)"
54
+ )
55
+
56
+ _NULL_PATTERN = re.compile(
57
+ r"(?<![A-Za-z0-9_])(name|title|description|category|brand|price|quantity|amount|"
58
+ r"age|score|rating|views|clicks)\s+(is\s+(?:not\s+)?null)\b",
59
+ re.IGNORECASE,
60
+ )
61
+
62
+ _CN_NULL_PATTERN = re.compile(
63
+ r"(名称|名字|标题|描述|分类|品牌|价格|数量|金额|年龄|得分|评分|浏览量|点击量)\s*"
64
+ r"(为空|为\s*空|是空|不为空|不为\s*空|非空)"
65
+ )
66
+
67
+ _SET_PREFIX_PATTERN = re.compile(
68
+ r"(?<![A-Za-z0-9_])(category|type|department|region|status)\s+"
69
+ r"(?:(not)\s+)?in\s*\(",
70
+ re.IGNORECASE,
71
+ )
72
+
73
+ _CN_SET_PREFIX_PATTERN = re.compile(
74
+ r"(类别|类型|部门|地区|状态)\s*(不在|在)\s*"
75
+ )
76
+
77
+ _STATUS_PATTERN = re.compile(
78
+ r"(?:\bstatus\s+|状态\s*(?:为|是|=)\s*)"
79
+ r"(active|enabled|valid|inactive|disabled|invalid|completed|done|pending|paid|unpaid|"
80
+ r"有效|无效|已完成|未完成|已支付|未支付)",
81
+ re.IGNORECASE,
82
+ )
83
+
84
+ _STATUS_CONDITIONS = {
85
+ "active": "status = 'active'",
86
+ "enabled": "status = 'active'",
87
+ "valid": "status = 'active'",
88
+ "有效": "status = 'active'",
89
+ "inactive": "status = 'inactive'",
90
+ "disabled": "status = 'inactive'",
91
+ "invalid": "status = 'inactive'",
92
+ "无效": "status = 'inactive'",
93
+ "completed": "status = 'completed'",
94
+ "done": "status = 'completed'",
95
+ "已完成": "status = 'completed'",
96
+ "pending": "status = 'pending'",
97
+ "未完成": "status = 'pending'",
98
+ "paid": "status = 'paid'",
99
+ "已支付": "status = 'paid'",
100
+ "unpaid": "status = 'unpaid'",
101
+ "未支付": "status = 'unpaid'",
102
+ }
103
+
104
+ _CN_COLUMNS = {
105
+ "价格": "price",
106
+ "数量": "quantity",
107
+ "金额": "amount",
108
+ "年龄": "age",
109
+ "得分": "score",
110
+ "评分": "rating",
111
+ "浏览量": "views",
112
+ "点击量": "clicks",
113
+ "名称": "name",
114
+ "名字": "name",
115
+ "标题": "title",
116
+ "描述": "description",
117
+ "分类": "category",
118
+ "品牌": "brand",
119
+ }
120
+
121
+ _CN_SET_COLUMNS = {
122
+ "类别": "category",
123
+ "类型": "type",
124
+ "部门": "department",
125
+ "地区": "region",
126
+ "状态": "status",
127
+ }
128
+
129
+ _CN_OPERATORS = {
130
+ "大于等于": ">=",
131
+ "小于等于": "<=",
132
+ "不等于": "!=",
133
+ "大于": ">",
134
+ "小于": "<",
135
+ "超过": ">",
136
+ "低于": "<",
137
+ "等于": "=",
138
+ }
139
+
140
+ _CONNECTOR_PATTERN = re.compile(r"\b(?:and|or)\b|以及|并且|且|或者|或|和", re.IGNORECASE)
141
+
142
+
143
+ def _normalize_operator(raw: str) -> str:
144
+ normalized = raw.lower()
145
+ if normalized.startswith(("greater", "more", "above", "over")):
146
+ return ">"
147
+ if normalized.startswith(("less", "below", "under")):
148
+ return "<"
149
+ if normalized.startswith(("equal", "equals")):
150
+ return "="
151
+ return _CN_OPERATORS.get(raw, normalized.upper())
152
+
153
+
154
+ def _text_sql(column: str, value: str) -> str:
155
+ escaped = value.strip().replace("'", "''")
156
+ return f"{column} LIKE '%{escaped}%'"
157
+
158
+
159
+ def _format_set_values(raw_values: str) -> str:
160
+ values = []
161
+ for raw in re.split(r"[,,]", raw_values):
162
+ value = raw.strip().strip("\"'")
163
+ if not value:
164
+ continue
165
+ values.append("'" + value.replace("'", "''") + "'")
166
+ return ", ".join(values)
167
+
168
+
169
+ def _comparison_matches(text: str) -> List[Tuple[int, int, str]]:
170
+ matches: List[Tuple[int, int, str]] = []
171
+
172
+ for pattern in _COMPARISON_PATTERNS:
173
+ for match in pattern.finditer(text):
174
+ first, raw_operator, value = match.groups()
175
+ column = _CN_COLUMNS.get(first, first)
176
+ matches.append((match.start(), match.end(), f"{column} {_normalize_operator(raw_operator)} {value}"))
177
+
178
+ for pattern in (_RANGE_PATTERN, _CN_RANGE_PATTERN):
179
+ for match in pattern.finditer(text):
180
+ groups = match.groups()
181
+ if len(groups) == 5:
182
+ first, low1, high1, low2, high2 = groups
183
+ low, high = low1 or low2, high1 or high2
184
+ else:
185
+ first, low, high = groups
186
+ column = _CN_COLUMNS.get(first, first)
187
+ matches.append((match.start(), match.end(), f"{column} BETWEEN {low} AND {high}"))
188
+
189
+ for pattern in (_TEXT_PATTERN, _CN_TEXT_PATTERN):
190
+ for match in pattern.finditer(text):
191
+ first, value = match.groups()
192
+ column = _CN_COLUMNS.get(first, first)
193
+ matches.append((match.start(), match.end(), _text_sql(column, value)))
194
+
195
+ for pattern in (_NULL_PATTERN, _CN_NULL_PATTERN):
196
+ for match in pattern.finditer(text):
197
+ first, raw_predicate = match.groups()
198
+ column = _CN_COLUMNS.get(first, first)
199
+ normalized = raw_predicate.replace(" ", "").lower()
200
+ is_not_null = normalized.startswith(("isnotnull", "不为空", "非空", "不为"))
201
+ matches.append((match.start(), match.end(), f"{column} IS {'NOT ' if is_not_null else ''}NULL"))
202
+
203
+ for match in _SET_PREFIX_PATTERN.finditer(text):
204
+ close_paren = text.find(")", match.end())
205
+ if close_paren == -1:
206
+ continue
207
+ column, negation = match.groups()
208
+ raw_values = text[match.end() : close_paren]
209
+ values = _format_set_values(raw_values)
210
+ if values:
211
+ operator = "NOT IN" if negation else "IN"
212
+ matches.append((match.start(), close_paren + 1, f"{column.lower()} {operator} ({values})"))
213
+
214
+ for match in _CN_SET_PREFIX_PATTERN.finditer(text):
215
+ column, operator_text = match.groups()
216
+ value_end = len(text)
217
+ connector = _CONNECTOR_PATTERN.search(text, match.end())
218
+ if connector is not None:
219
+ value_end = connector.start()
220
+ raw_values = text[match.end() : value_end].strip()
221
+ for suffix in ("之中", "其中", "中"):
222
+ if raw_values.endswith(suffix):
223
+ raw_values = raw_values[: -len(suffix)].rstrip()
224
+ break
225
+ if not raw_values or any(char in raw_values for char in "()"):
226
+ continue
227
+ values = _format_set_values(raw_values)
228
+ if values:
229
+ operator = "NOT IN" if operator_text == "不在" else "IN"
230
+ matches.append((match.start(), value_end, f"{_CN_SET_COLUMNS[column]} {operator} ({values})"))
231
+
232
+ for match in _STATUS_PATTERN.finditer(text):
233
+ status = match.group(1).lower()
234
+ matches.append((match.start(), match.end(), _STATUS_CONDITIONS[status]))
235
+
236
+ matches.sort(key=lambda item: (item[0], item[1]))
237
+ deduped: List[Tuple[int, int, str]] = []
238
+ for item in matches:
239
+ if deduped and item[0] == deduped[-1][0] and item[1] <= deduped[-1][1]:
240
+ continue
241
+ deduped.append(item)
242
+ return deduped
243
+
244
+
245
+ def extract_boolean_conditions(text: str) -> Optional[List[str]]:
246
+ """Return one boolean SQL expression preserving connector precedence/grouping."""
247
+ matches = _comparison_matches(text)
248
+ if len(matches) < 2:
249
+ return None
250
+
251
+ tokens: List[str] = []
252
+ prefix = text[: matches[0][0]]
253
+ tokens.extend(char for char in prefix if char in "()")
254
+
255
+ for index, current in enumerate(matches):
256
+ tokens.append(f"({current[2]})")
257
+ if index == len(matches) - 1:
258
+ suffix = text[current[1] :]
259
+ tokens.extend(char for char in suffix if char in "()")
260
+ continue
261
+
262
+ separator = text[current[1] : matches[index + 1][0]]
263
+ events = []
264
+ for paren in re.finditer(r"[()]", separator):
265
+ events.append((paren.start(), paren.group(0)))
266
+ connector_matches = list(_CONNECTOR_PATTERN.finditer(separator))
267
+ if len(connector_matches) != 1:
268
+ return None
269
+ connector = connector_matches[0]
270
+ events.append((connector.start(), connector.group(0)))
271
+ events.sort(key=lambda item: item[0])
272
+ for _, event in events:
273
+ if event == "(" or event == ")":
274
+ tokens.append(event)
275
+ elif event:
276
+ normalized = event.lower()
277
+ tokens.append("OR" if normalized in {"or", "或者", "或"} else "AND")
278
+
279
+ return [" ".join(tokens)]
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """Core NL2SQL extraction primitives: operation, table, column, limit, distinct.
3
+
4
+ These are stateless, dependency-free functions that operate on raw text
5
+ and the shared mapping dicts (KEYWORDS, TABLE_PATTERNS, COLUMN_PATTERNS
6
+ from ``backend.core.nl2sql_components.mappings``).
7
+ """
8
+ import re
9
+ from typing import Dict, List, Optional
10
+
11
+
12
+ def detect_operation(text: str, keywords: Dict[str, str]) -> str:
13
+ """Detect SQL operation type from text.
14
+
15
+ Scans the keyword mapping for operation keywords present in the text
16
+ and returns the first matching operation (SELECT/INSERT/UPDATE/DELETE).
17
+ Defaults to SELECT.
18
+ """
19
+ for keyword, op in keywords.items():
20
+ if keyword in text and op in ["SELECT", "INSERT", "UPDATE", "DELETE"]:
21
+ return op
22
+ return "SELECT"
23
+
24
+
25
+ def extract_table(text: str, table_patterns: Dict[str, str]) -> str:
26
+ """Extract a single table name from text.
27
+
28
+ Prefers the longest matching pattern; ties broken by first position.
29
+ Returns the fallback name "table_name" when no pattern matches.
30
+ """
31
+ tables_found = []
32
+ for pattern, table in table_patterns.items():
33
+ if pattern in text:
34
+ tables_found.append((text.find(pattern), table, len(pattern)))
35
+ if tables_found:
36
+ tables_found.sort(key=lambda item: (-item[2], item[0]))
37
+ return tables_found[0][1]
38
+ return "table_name"
39
+
40
+
41
+ def extract_table_subject(text: str, table_patterns: Dict[str, str]) -> Optional[str]:
42
+ """Extract the subject table (first appearing in text) for D2 relational queries.
43
+
44
+ For queries like 'users who have orders', returns 'users' (the subject)
45
+ rather than 'orders' (the relation). Uses first occurrence position.
46
+ """
47
+ candidates = []
48
+ for pattern, table in table_patterns.items():
49
+ if table not in candidates:
50
+ pos = text.find(pattern)
51
+ if pos != -1:
52
+ candidates.append((pos, table))
53
+ if candidates:
54
+ candidates.sort(key=lambda x: x[0])
55
+ return candidates[0][1]
56
+ return None
57
+
58
+
59
+ def named_tables(text: str, table_patterns: Dict[str, str]) -> List[str]:
60
+ """Return the distinct table names the text mentions, in order of first appearance."""
61
+ occurrences = []
62
+ seen = set()
63
+ for pattern, table in table_patterns.items():
64
+ if table in seen:
65
+ continue
66
+ pos = text.find(pattern)
67
+ if pos != -1:
68
+ occurrences.append((pos, table))
69
+ seen.add(table)
70
+ return [t for _, t in sorted(occurrences)]
71
+
72
+
73
+ def extract_columns(text: str, column_patterns: Dict[str, str]) -> List[str]:
74
+ """Extract column names from text.
75
+
76
+ Returns a list of distinct column names, or ["*"] when no column
77
+ keyword is present.
78
+ """
79
+ columns = []
80
+ for pattern, column in column_patterns.items():
81
+ if pattern in text and column not in columns:
82
+ columns.append(column)
83
+ return columns if columns else ["*"]
84
+
85
+
86
+ def extract_limit(text: str) -> Optional[int]:
87
+ """Extract LIMIT value from text.
88
+
89
+ Supports Chinese and English cardinality phrasings such as
90
+ '前10', 'top 10', 'limit 10', '10条', '10行'.
91
+ """
92
+ patterns = [
93
+ r"前\s*(\d+)", r"top\s*(\d+)", r"limit\s*(\d+)",
94
+ r"first\s*(\d+)", r"(\d+)\s*条", r"(\d+)\s*个",
95
+ r"(\d+)\s*行", r"(\d+)\s*rows?", r"only\s*(\d+)",
96
+ ]
97
+ for pattern in patterns:
98
+ match = re.search(pattern, text, re.IGNORECASE)
99
+ if match:
100
+ return int(match.group(1))
101
+ return None
102
+
103
+
104
+ def check_distinct(text: str) -> bool:
105
+ """Check if DISTINCT is requested in the text."""
106
+ return any(k in text for k in ["去重", "唯一", "不重复", "distinct", "unique"])
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env python3
2
+ """Evidence-based confidence model for NL2SQL generation.
3
+
4
+ Replaces opaque ``confidence += 0.1`` heuristic increments with a
5
+ structured list of named evidence items. Each item records:
6
+
7
+ evidence_id - stable identifier for the evidence source
8
+ label - human-readable description
9
+ weight - contribution to the confidence score (signed)
10
+
11
+ The confidence score is the sum of all item weights, clamped to [0, 1],
12
+ exactly matching the previous heuristic's numeric output for the
13
+ same inputs. Consumers can inspect the items list to understand
14
+ *why* a result was given its score.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from typing import List, Optional
20
+
21
+
22
+ @dataclass
23
+ class EvidenceItem:
24
+ """A single evidence item contributing to the confidence score.
25
+
26
+ Attributes:
27
+ evidence_id: Stable identifier, e.g. "table_known" or "parse_ok".
28
+ label: Human-readable description shown to the user.
29
+ weight: Signed contribution to the confidence score.
30
+ detail: Optional machine-readable context (table name, parse error, etc.).
31
+ """
32
+ evidence_id: str
33
+ label: str
34
+ weight: float
35
+ detail: Optional[str] = None
36
+
37
+
38
+ @dataclass
39
+ class GenerationEvidence:
40
+ """Structured evidence backing a generated SQL confidence score.
41
+
42
+ Holds the full list of EvidenceItems and a computed score.
43
+ The score is the sum of all weights, clamped to [0.0, 1.0],
44
+ matching the previous heuristic confidence exactly.
45
+
46
+ Use ``GenerationEvidence.score`` as the ``confidence`` value and
47
+ pass ``GenerationEvidence.items`` as the evidence trail.
48
+ """
49
+ items: List[EvidenceItem] = field(default_factory=list)
50
+
51
+ @property
52
+ def score(self) -> float:
53
+ """Clamped sum of all item weights (matches legacy heuristic range)."""
54
+ return max(0.0, min(1.0, sum(i.weight for i in self.items)))
55
+
56
+ def to_dict(self) -> dict:
57
+ return {
58
+ "score": self.score,
59
+ "items": [
60
+ {
61
+ "evidence_id": i.evidence_id,
62
+ "label": i.label,
63
+ "weight": i.weight,
64
+ "detail": i.detail,
65
+ }
66
+ for i in self.items
67
+ ],
68
+ }
69
+
70
+
71
+ # ── Evidence ID constants ──────────────────────────────────────────────
72
+ # Use these instead of raw string literals so the IDs are stable
73
+ # across refactors and can be grepped.
74
+
75
+ # Table / schema evidence
76
+ EVIDENCE_TABLE_KNOWN = "table_known"
77
+ EVIDENCE_TABLE_UNKNOWN = "table_unknown"
78
+
79
+ # Column / aggregate evidence
80
+ EVIDENCE_COLUMNS_EXPLICIT = "columns_explicit"
81
+ EVIDENCE_COLUMNS_WILDCARD = "columns_wildcard"
82
+
83
+ # Predicate / condition evidence
84
+ EVIDENCE_CONDITIONS_PRESENT = "conditions_present"
85
+
86
+ # Relationship / join evidence
87
+ EVIDENCE_JOIN_KNOWN = "join_known"
88
+ EVIDENCE_JOIN_UNKNOWN = "join_unknown"
89
+
90
+ # Aggregation evidence
91
+ EVIDENCE_AGGREGATION_PRESENT = "aggregation_present"
92
+
93
+ # Ordering / limit / group-by evidence
94
+ EVIDENCE_GROUP_BY = "group_by"
95
+ EVIDENCE_ORDERING = "ordering"
96
+ EVIDENCE_LIMIT = "limit"
97
+
98
+ # Generated SQL structural evidence
99
+ EVIDENCE_PARSE_OK = "target_parse_ok"
100
+ EVIDENCE_PARSE_FAIL = "target_parse_fail"
101
+
102
+ # Template evidence
103
+ EVIDENCE_TEMPLATE_MATCHED = "template_matched"
104
+
105
+ __all__ = [
106
+ "EvidenceItem",
107
+ "GenerationEvidence",
108
+ "EVIDENCE_TABLE_KNOWN",
109
+ "EVIDENCE_TABLE_UNKNOWN",
110
+ "EVIDENCE_COLUMNS_EXPLICIT",
111
+ "EVIDENCE_COLUMNS_WILDCARD",
112
+ "EVIDENCE_CONDITIONS_PRESENT",
113
+ "EVIDENCE_JOIN_KNOWN",
114
+ "EVIDENCE_JOIN_UNKNOWN",
115
+ "EVIDENCE_AGGREGATION_PRESENT",
116
+ "EVIDENCE_GROUP_BY",
117
+ "EVIDENCE_ORDERING",
118
+ "EVIDENCE_LIMIT",
119
+ "EVIDENCE_PARSE_OK",
120
+ "EVIDENCE_PARSE_FAIL",
121
+ "EVIDENCE_TEMPLATE_MATCHED",
122
+ ]