sql-dialect-master 1.4.0__tar.gz → 1.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/PKG-INFO +2 -2
  2. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/README.md +1 -1
  3. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/metadata.py +2 -2
  4. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_components/boolean_conditions.py +118 -14
  5. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_components/core_extract.py +9 -0
  6. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/predicates.py +131 -52
  7. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/templates.py +0 -11
  8. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/pyproject.toml +1 -1
  9. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/sql_dialect_master.egg-info/PKG-INFO +2 -2
  10. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/LICENSE +0 -0
  11. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/__init__.py +0 -0
  12. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/api/__init__.py +0 -0
  13. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/api/auth.py +0 -0
  14. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/api/client_identity.py +0 -0
  15. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/api/main.py +0 -0
  16. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/api/metrics.py +0 -0
  17. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/api/middleware.py +0 -0
  18. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/api/rate_limit_store.py +0 -0
  19. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/api/readiness.py +0 -0
  20. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/api/workers.py +0 -0
  21. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/__init__.py +0 -0
  22. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/cache.py +0 -0
  23. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/column_hint_validation.py +0 -0
  24. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/config.py +0 -0
  25. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/constants.py +0 -0
  26. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/exceptions.py +0 -0
  27. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/function_call_scanner.py +0 -0
  28. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/function_semantics.py +0 -0
  29. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/functions_db.json +0 -0
  30. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/functions_lookup.py +0 -0
  31. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql.py +0 -0
  32. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_components/__init__.py +0 -0
  33. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_components/aggregations.py +0 -0
  34. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_components/evidence.py +0 -0
  35. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_components/mappings.py +0 -0
  36. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_components/relations.py +0 -0
  37. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_components/templates.py +0 -0
  38. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_components/tokenizer.py +0 -0
  39. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_legacy.py +0 -0
  40. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_llm.py +0 -0
  41. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/__init__.py +0 -0
  42. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
  43. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
  44. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/dialect.py +0 -0
  45. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/intent.py +0 -0
  46. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/ir.py +0 -0
  47. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/models.py +0 -0
  48. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/orchestrator.py +0 -0
  49. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/renderer.py +0 -0
  50. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/resolution.py +0 -0
  51. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
  52. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_pipeline/validation.py +0 -0
  53. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/nl2sql_schema.py +0 -0
  54. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/p1_sql_scanner.py +0 -0
  55. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/p2_data_validation.py +0 -0
  56. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/parser.py +0 -0
  57. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/post_processor.py +0 -0
  58. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/rules.py +0 -0
  59. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/semantic_diff.py +0 -0
  60. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/sql_statements.py +0 -0
  61. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/transpiler.py +0 -0
  62. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/type_mapping.json +0 -0
  63. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/core/type_mapping.py +0 -0
  64. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/utils/__init__.py +0 -0
  65. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/backend/utils/validators.py +0 -0
  66. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/setup.cfg +0 -0
  67. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/sql_dialect_master.egg-info/SOURCES.txt +0 -0
  68. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
  69. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/sql_dialect_master.egg-info/requires.txt +0 -0
  70. {sql_dialect_master-1.4.0 → sql_dialect_master-1.4.1}/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.4.0
3
+ Version: 1.4.1
4
4
  Summary: SQL Dialect Master - Multi-database SQL conversion engine
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -38,7 +38,7 @@ Dynamic: license-file
38
38
 
39
39
  Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
40
40
 
41
- Current release: 1.4.0
41
+ Current release: 1.4.1
42
42
 
43
43
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
44
44
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -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.4.0
5
+ Current release: 1.4.1
6
6
 
7
7
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -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.4.0"
21
- API_VERSION = "1.4.0"
20
+ PROJECT_VERSION = "1.4.1"
21
+ API_VERSION = "1.4.1"
22
22
  # Frontend v2 architecture — independent of the product release version.
23
23
  FRONTEND_ARCHITECTURE_VERSION = 2
24
24
 
@@ -7,39 +7,41 @@ user-supplied parentheses.
7
7
  import re
8
8
  from typing import List, Optional, Tuple
9
9
 
10
+ from ..p1_sql_scanner import mask_non_executable
11
+
10
12
 
11
13
  _COMPARISON_PATTERNS = (
12
14
  re.compile(
13
15
  r"(?<![A-Za-z0-9_])(price|quantity|amount|age|score|rating|views|clicks)\s+"
14
16
  r"(greater(?:\s+than)?|more\s+than|above|over|less(?:\s+than)?|"
15
17
  r"below|under|equal(?:\s+to)?|equals)\s+"
16
- r"([0-9]+(?:\.[0-9]+)?)",
18
+ r"(-?[0-9]+(?:\.[0-9]+)?)",
17
19
  re.IGNORECASE,
18
20
  ),
19
21
  re.compile(
20
22
  r"(?<![A-Za-z0-9_])(price|quantity|amount|age|score|rating|views|clicks)\s*"
21
- r"(>=|<=|!=|=|>|<)\s*([0-9]+(?:\.[0-9]+)?)",
23
+ r"(>=|<=|!=|<>|=|>|<)\s*(-?[0-9]+(?:\.[0-9]+)?)",
22
24
  re.IGNORECASE,
23
25
  ),
24
26
  re.compile(
25
27
  r"(价格|数量|金额|年龄|得分|评分|浏览量|点击量)\s*"
26
28
  r"(大于等于|小于等于|不等于|大于|小于|超过|低于|等于)\s*"
27
- r"([0-9]+(?:\.[0-9]+)?)"
29
+ r"(-?[0-9]+(?:\.[0-9]+)?)"
28
30
  ),
29
31
  )
30
32
 
31
33
  _RANGE_PATTERN = re.compile(
32
34
  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]+)?))",
35
+ r"(?:between\s+(-?[0-9]+(?:\.[0-9]+)?)\s+and\s+(-?[0-9]+(?:\.[0-9]+)?)|"
36
+ r"in\s+(?:the\s+)?range\s+(-?[0-9]+(?:\.[0-9]+)?)\s*(?:to|-)\s*"
37
+ r"(-?[0-9]+(?:\.[0-9]+)?))",
36
38
  re.IGNORECASE,
37
39
  )
38
40
 
39
41
  _CN_RANGE_PATTERN = re.compile(
40
42
  r"(价格|数量|金额|年龄|得分|评分|浏览量|点击量)\s*"
41
- r"(?:在|介于)\s*([0-9]+(?:\.[0-9]+)?)\s*(?:和|到|至|-)\s*"
42
- r"([0-9]+(?:\.[0-9]+)?)\s*(?:之间|范围内|以内)?"
43
+ r"(?:在|介于)\s*(-?[0-9]+(?:\.[0-9]+)?)\s*(?:和|到|至|-)\s*"
44
+ r"(-?[0-9]+(?:\.[0-9]+)?)\s*(?:之间|范围内|以内)?"
43
45
  )
44
46
 
45
47
  _TEXT_PATTERN = re.compile(
@@ -101,6 +103,29 @@ _STATUS_CONDITIONS = {
101
103
  "未支付": "status = 'unpaid'",
102
104
  }
103
105
 
106
+ _STATUS_VALUES = {
107
+ "active": "active",
108
+ "inactive": "inactive",
109
+ "enabled": "active",
110
+ "disabled": "inactive",
111
+ "valid": "active",
112
+ "invalid": "inactive",
113
+ "completed": "completed",
114
+ "done": "completed",
115
+ "pending": "pending",
116
+ "paid": "paid",
117
+ "unpaid": "unpaid",
118
+ "有效": "active",
119
+ "无效": "inactive",
120
+ "已完成": "completed",
121
+ "未完成": "pending",
122
+ "已支付": "paid",
123
+ "未支付": "unpaid",
124
+ }
125
+ _STATUS_VALUE_PATTERN = "(?:" + "|".join(
126
+ sorted((re.escape(value) for value in _STATUS_VALUES), key=len, reverse=True)
127
+ ) + ")"
128
+
104
129
  _CN_COLUMNS = {
105
130
  "价格": "price",
106
131
  "数量": "quantity",
@@ -168,15 +193,16 @@ def _format_set_values(raw_values: str) -> str:
168
193
 
169
194
  def _comparison_matches(text: str) -> List[Tuple[int, int, str]]:
170
195
  matches: List[Tuple[int, int, str]] = []
196
+ executable = mask_non_executable(text)
171
197
 
172
198
  for pattern in _COMPARISON_PATTERNS:
173
- for match in pattern.finditer(text):
199
+ for match in pattern.finditer(executable):
174
200
  first, raw_operator, value = match.groups()
175
201
  column = _CN_COLUMNS.get(first, first)
176
202
  matches.append((match.start(), match.end(), f"{column} {_normalize_operator(raw_operator)} {value}"))
177
203
 
178
204
  for pattern in (_RANGE_PATTERN, _CN_RANGE_PATTERN):
179
- for match in pattern.finditer(text):
205
+ for match in pattern.finditer(executable):
180
206
  groups = match.groups()
181
207
  if len(groups) == 5:
182
208
  first, low1, high1, low2, high2 = groups
@@ -193,7 +219,7 @@ def _comparison_matches(text: str) -> List[Tuple[int, int, str]]:
193
219
  matches.append((match.start(), match.end(), _text_sql(column, value)))
194
220
 
195
221
  for pattern in (_NULL_PATTERN, _CN_NULL_PATTERN):
196
- for match in pattern.finditer(text):
222
+ for match in pattern.finditer(executable):
197
223
  first, raw_predicate = match.groups()
198
224
  column = _CN_COLUMNS.get(first, first)
199
225
  normalized = raw_predicate.replace(" ", "").lower()
@@ -229,9 +255,7 @@ def _comparison_matches(text: str) -> List[Tuple[int, int, str]]:
229
255
  operator = "NOT IN" if operator_text == "不在" else "IN"
230
256
  matches.append((match.start(), value_end, f"{_CN_SET_COLUMNS[column]} {operator} ({values})"))
231
257
 
232
- for match in _STATUS_PATTERN.finditer(text):
233
- status = match.group(1).lower()
234
- matches.append((match.start(), match.end(), _STATUS_CONDITIONS[status]))
258
+ matches.extend(_status_matches(text))
235
259
 
236
260
  matches.sort(key=lambda item: (item[0], item[1]))
237
261
  deduped: List[Tuple[int, int, str]] = []
@@ -242,6 +266,86 @@ def _comparison_matches(text: str) -> List[Tuple[int, int, str]]:
242
266
  return deduped
243
267
 
244
268
 
269
+ def _status_matches(text: str) -> List[Tuple[int, int, str]]:
270
+ """Extract status predicates without matching quoted values."""
271
+ executable = mask_non_executable(text)
272
+ matches: list[Tuple[int, int, str]] = []
273
+ explicit_patterns = [
274
+ (
275
+ re.compile(
276
+ rf"\bstatus\s*(?:is\s+not|isn't|not|!=|<>)\s*({_STATUS_VALUE_PATTERN})\b",
277
+ re.IGNORECASE,
278
+ ),
279
+ "!=",
280
+ ),
281
+ (
282
+ re.compile(rf"\bstatus\s*(?:is|=)\s*({_STATUS_VALUE_PATTERN})\b", re.IGNORECASE),
283
+ "=",
284
+ ),
285
+ (
286
+ re.compile(rf"\bstatus\s+({_STATUS_VALUE_PATTERN})\b", re.IGNORECASE),
287
+ "=",
288
+ ),
289
+ (
290
+ re.compile(rf"状态\s*(?:不是|不为|!=|<>)\s*({_STATUS_VALUE_PATTERN})"),
291
+ "!=",
292
+ ),
293
+ (
294
+ re.compile(rf"状态\s*(?:为|是|=)\s*({_STATUS_VALUE_PATTERN})"),
295
+ "=",
296
+ ),
297
+ ]
298
+ for pattern, operator in explicit_patterns:
299
+ for match in pattern.finditer(executable):
300
+ raw_value = match.group(1)
301
+ canonical = _STATUS_VALUES[raw_value.casefold() if raw_value.isascii() else raw_value]
302
+ matches.append((match.start(), match.end(), f"status {operator} '{canonical}'"))
303
+
304
+ occupied = [(start, end) for start, end, _ in matches]
305
+ text_spans = [
306
+ (match.start(), match.end())
307
+ for pattern in (_TEXT_PATTERN, _CN_TEXT_PATTERN)
308
+ for match in pattern.finditer(executable)
309
+ ]
310
+ for raw_value, canonical in sorted(_STATUS_VALUES.items(), key=lambda item: -len(item[0])):
311
+ if raw_value.isascii():
312
+ pattern = re.compile(rf"(?<![\w$]){re.escape(raw_value)}(?![\w$])", re.IGNORECASE)
313
+ else:
314
+ pattern = re.compile(re.escape(raw_value))
315
+ for match in pattern.finditer(executable):
316
+ if any(start <= match.start() < end or match.start() <= start < match.end() for start, end in occupied):
317
+ continue
318
+ if any(start <= match.start() < end for start, end in text_spans):
319
+ continue
320
+ prefix = executable[:match.start()]
321
+ negative = bool(
322
+ re.search(r"(?:\b(?:not|except|excluding)\s*)$", prefix, re.IGNORECASE)
323
+ or re.search(r"(?:不是|不为|非)\s*$", prefix)
324
+ )
325
+ operator = "!=" if negative else "="
326
+ matches.append((match.start(), match.end(), f"status {operator} '{canonical}'"))
327
+ occupied.append((match.start(), match.end()))
328
+
329
+ deleted_patterns = [
330
+ (
331
+ re.compile(r"(?:未删除|没有删除|不是删除|非删除|\bnot\s+deleted\b)", re.IGNORECASE),
332
+ "is_deleted = 0",
333
+ ),
334
+ (re.compile(r"(?:已删除|\bdeleted\b|\bremoved\b)", re.IGNORECASE), "is_deleted = 1"),
335
+ ]
336
+ for pattern, condition in deleted_patterns:
337
+ for match in pattern.finditer(executable):
338
+ if any(start <= match.start() < end or match.start() <= start < match.end() for start, end, _ in matches):
339
+ continue
340
+ matches.append((match.start(), match.end(), condition))
341
+ return matches
342
+
343
+
344
+ def extract_status_conditions(text: str) -> list[str]:
345
+ """Return standalone status predicates for the single-condition path."""
346
+ return [condition for _, _, condition in _status_matches(text)]
347
+
348
+
245
349
  def extract_boolean_conditions(text: str) -> Optional[List[str]]:
246
350
  """Return one boolean SQL expression preserving connector precedence/grouping."""
247
351
  matches = _comparison_matches(text)
@@ -38,6 +38,15 @@ def detect_operation(text: str, keywords: Dict[str, str]) -> str:
38
38
  Defaults to SELECT.
39
39
  """
40
40
  intent_text = mask_non_executable(text)
41
+ # Deletion-state phrases describe a SELECT predicate. Mask only the
42
+ # phrase so an independent leading "删除" in e.g. "删除已删除用户"
43
+ # remains a genuine DELETE intent.
44
+ intent_text = re.sub(
45
+ r"(?:已删除|未删除|没有删除|不是删除|非删除|already\s+deleted|not\s+deleted)",
46
+ lambda match: " " * len(match.group(0)),
47
+ intent_text,
48
+ flags=re.IGNORECASE,
49
+ )
41
50
  for keyword, op in keywords.items():
42
51
  if _pattern_match(intent_text, keyword) and op in ["SELECT", "INSERT", "UPDATE", "DELETE"]:
43
52
  return op
@@ -11,7 +11,79 @@ the instance attribute.
11
11
  import re
12
12
  from typing import Dict, List, Optional
13
13
 
14
- from ..nl2sql_components.boolean_conditions import extract_boolean_conditions
14
+ from ..nl2sql_components.boolean_conditions import (
15
+ extract_boolean_conditions,
16
+ extract_status_conditions,
17
+ )
18
+ from ..p1_sql_scanner import mask_non_executable
19
+
20
+ _QUOTED_COMPARISON_PREFIX = re.compile(
21
+ r"(?<![\w$])([A-Za-z_][A-Za-z0-9_$]*)\s*"
22
+ r"(is\s+not|isn't|not|is|<>|!=|>=|<=|=|>|<)\s*",
23
+ re.IGNORECASE,
24
+ )
25
+
26
+
27
+ def _column_for_text(text: str, column_patterns: Dict[str, str]) -> Optional[str]:
28
+ for pattern, column in sorted(column_patterns.items(), key=lambda item: -len(item[0])):
29
+ expression = re.escape(pattern)
30
+ if not re.search(r"[\u4e00-\u9fff]", pattern):
31
+ expression = rf"(?<![\w$]){expression}(?![\w$])"
32
+ if re.search(expression, text, re.IGNORECASE):
33
+ return column
34
+ return None
35
+
36
+
37
+ def _quoted_comparisons(
38
+ original: str, column_patterns: Dict[str, str]
39
+ ) -> list[str]:
40
+ quoted = []
41
+ executable = mask_non_executable(original)
42
+ known_columns = {value.casefold(): value for value in column_patterns.values()}
43
+ for match in _QUOTED_COMPARISON_PREFIX.finditer(original):
44
+ if executable[match.start():match.end()] != original[match.start():match.end()]:
45
+ continue
46
+ position = match.end()
47
+ if position >= len(original) or original[position] not in "'\"":
48
+ continue
49
+ quote = original[position]
50
+ position += 1
51
+ value_chars: list[str] = []
52
+ closed = False
53
+ while position < len(original):
54
+ character = original[position]
55
+ if character == "\\" and position + 1 < len(original):
56
+ value_chars.extend((character, original[position + 1]))
57
+ position += 2
58
+ continue
59
+ if character == quote:
60
+ if position + 1 < len(original) and original[position + 1] == quote:
61
+ value_chars.append(quote)
62
+ position += 2
63
+ continue
64
+ closed = True
65
+ position += 1
66
+ break
67
+ value_chars.append(character)
68
+ position += 1
69
+ if not closed:
70
+ continue
71
+ raw_column, operator = match.groups()
72
+ value = "".join(value_chars)
73
+ column = known_columns.get(raw_column.casefold(), raw_column)
74
+ if quote == "'":
75
+ literal = quote + value.replace("'", "''") + quote
76
+ else:
77
+ literal = "'" + value.replace("'", "''") + "'"
78
+ normalized_operator = {
79
+ "is": "=",
80
+ "isn't": "!=",
81
+ "is not": "!=",
82
+ "not": "!=",
83
+ "<>": "!=",
84
+ }.get(operator.casefold(), operator)
85
+ quoted.append(f"{column} {normalized_operator} {literal}")
86
+ return quoted
15
87
 
16
88
 
17
89
  def extract_conditions(
@@ -30,13 +102,11 @@ def extract_conditions(
30
102
  if boolean_conditions:
31
103
  return boolean_conditions
32
104
 
33
- conditions = []
34
- numbers = re.findall(r"\d+\.?\d*", original)
35
- condition_column = None
36
- for pattern, column in column_patterns.items():
37
- if pattern in text:
38
- condition_column = column
39
- break
105
+ conditions = _quoted_comparisons(original, column_patterns)
106
+ executable_original = mask_non_executable(original)
107
+ executable_text = mask_non_executable(text)
108
+ numbers = re.findall(r"-?\d+(?:\.\d*)?", executable_original)
109
+ condition_column = _column_for_text(executable_text, column_patterns)
40
110
 
41
111
  # Handle IN/NOT IN predicate (must come before generic comparison handling)
42
112
  in_matched_columns: set = set()
@@ -79,9 +149,9 @@ def extract_conditions(
79
149
  in_matched_columns.add(col.lower())
80
150
 
81
151
  comparisons = [
82
- (["大于等于", "不小于", "至少", "greater than or equal to", "larger than or equal to"], ">="),
83
- (["小于等于", "不大于", "最多", "less than or equal to", "smaller than or equal to"], "<="),
84
- (["不等于", "不是", "不为", "not equal", "isn't", "doesn't equal"], "!="),
152
+ ([">=", "大于等于", "不小于", "至少", "greater than or equal to", "larger than or equal to"], ">="),
153
+ (["<=", "小于等于", "不大于", "最多", "less than or equal to", "smaller than or equal to"], "<="),
154
+ (["<>", "!=", "不等于", "不是", "不为", "not equal", "isn't", "doesn't equal"], "!="),
85
155
  (["大于", "超过", "高于", "多于", "greater", "more than", "above", "over", ">"], ">"),
86
156
  (["小于", "低于", "少于", "不足", "less", "less than", "below", "under", "<"], "<"),
87
157
  (["等于", "是", "为", "equals", "equal", "="], "="),
@@ -109,11 +179,11 @@ def extract_conditions(
109
179
  # word itself (Chinese-style adjacency, e.g. '大于100').
110
180
  # Longest keyword first so '大于等于' never yields to '大于'.
111
181
  for keyword in sorted(keywords, key=len, reverse=True):
112
- if keyword not in original:
182
+ if keyword not in executable_original:
113
183
  continue
114
184
  m = re.search(
115
- re.escape(keyword) + r"\s*(\d+(?:\.\d+)?)",
116
- original,
185
+ re.escape(keyword) + r"\s*(-?\d+(?:\.\d*)?)",
186
+ executable_original,
117
187
  )
118
188
  if m:
119
189
  bound_value = m.group(1)
@@ -126,12 +196,12 @@ def extract_conditions(
126
196
  # "10" in "top 10 …") must never win this slot.
127
197
  if bound_value is None:
128
198
  for keyword in sorted(keywords, key=len, reverse=True):
129
- if keyword not in original:
199
+ if keyword not in executable_original:
130
200
  continue
131
201
  m = re.search(
132
202
  re.escape(keyword)
133
- + r"(?:\s+than\s+|\s+or\s+equal\s+to\s+|\s+than\s+or\s+equal\s+to\s+)?(\d+(?:\.\d+)?)",
134
- original,
203
+ + r"(?:\s+than\s+|\s+or\s+equal\s+to\s+|\s+than\s+or\s+equal\s+to\s+)?(-?\d+(?:\.\d*)?)",
204
+ executable_original,
135
205
  re.IGNORECASE,
136
206
  )
137
207
  if m:
@@ -147,19 +217,43 @@ def extract_conditions(
147
217
  # Fallback: operator word present but no number is directly or
148
218
  # tail-adjacent to it — bind the column named before the operator
149
219
  # word to the first available number.
150
- if any(k in original for k in keywords) and numbers:
220
+ if any(k in executable_original for k in keywords) and numbers:
151
221
  col = condition_column or "column"
152
222
  conditions.append(f"{col} {op} {numbers[0]}")
153
223
  break
154
224
 
155
- if any(keyword in text for keyword in ["之间", "范围", "between", "from...to"]):
225
+ if any(keyword in executable_text for keyword in ["之间", "范围", "between", "from...to"]):
156
226
  if len(numbers) >= 2:
157
227
  col = condition_column or "column"
158
228
  conditions.append(f"{col} BETWEEN {numbers[0]} AND {numbers[1]}")
159
229
 
160
- if any(keyword in text for keyword in ["包含", "含有", "contains", "like", "includes"]):
230
+ if any(keyword in executable_text for keyword in ["包含", "含有", "contains", "like", "includes"]):
231
+ text_match = re.search(
232
+ r"(?<![\w$])([A-Za-z_][A-Za-z0-9_$]*)\s+"
233
+ r"(?:contains|containing|includes|including)\s+"
234
+ r"(?:the\s+)?([^,;,。;]+?)(?=\s+(?:and|or)\b|[,;,。;]|$)",
235
+ original,
236
+ re.IGNORECASE,
237
+ )
238
+ cn_text_match = re.search(
239
+ r"(名称|名字|标题|描述|分类|品牌)\s*(?:包含|含有|包括)\s*([^且并或者和,,。;]+)",
240
+ original,
241
+ )
161
242
  match = re.search(r"[\"']([^\"']+)[\"']", original)
162
- if match:
243
+ if text_match:
244
+ raw_column, raw_value = text_match.groups()
245
+ column = next(
246
+ (value for value in column_patterns.values() if value.casefold() == raw_column.casefold()),
247
+ raw_column,
248
+ )
249
+ value = raw_value.strip().strip("'\"")
250
+ if value:
251
+ conditions.append(f"{column} LIKE '%{value.replace(chr(39), chr(39) * 2)}%'")
252
+ elif cn_text_match:
253
+ raw_column, raw_value = cn_text_match.groups()
254
+ aliases = {"名称": "name", "名字": "name", "标题": "title", "描述": "description", "分类": "category", "品牌": "brand"}
255
+ conditions.append(f"{aliases[raw_column]} LIKE '%{raw_value.strip().replace(chr(39), chr(39) * 2)}%'")
256
+ elif match:
163
257
  col = condition_column or "column"
164
258
  conditions.append(f"{col} LIKE '%{match.group(1)}%'")
165
259
 
@@ -172,14 +266,14 @@ def extract_conditions(
172
266
  elif any(keyword in text for keyword in ["为空", "空值", "is null", "null", "empty"]):
173
267
  conditions.append(f"{condition_column or 'column'} IS NULL")
174
268
 
175
- date_col = "created_at" if "created_at" in text else "date"
176
- if "今天" in text or "today" in text:
269
+ date_col = "created_at" if "created_at" in executable_text else "date"
270
+ if "今天" in executable_text or "today" in executable_text:
177
271
  conditions.append(f"{date_col} = CURRENT_DATE")
178
- elif "昨天" in text or "yesterday" in text:
272
+ elif "昨天" in executable_text or "yesterday" in executable_text:
179
273
  conditions.append(f"{date_col} = DATE_SUB(CURRENT_DATE, 1)")
180
- elif "前天" in text or "day before" in text:
274
+ elif "前天" in executable_text or "day before" in executable_text:
181
275
  conditions.append(f"{date_col} = DATE_SUB(CURRENT_DATE, 2)")
182
- elif "明天" in text or "tomorrow" in text:
276
+ elif "明天" in executable_text or "tomorrow" in executable_text:
183
277
  conditions.append(f"{date_col} = DATE_ADD(CURRENT_DATE, 1)")
184
278
 
185
279
  time_patterns = [
@@ -189,7 +283,7 @@ def extract_conditions(
189
283
  (r"(最近|过去|last|past)\s*(\d+)\s*(年|year)", "YEAR"),
190
284
  ]
191
285
  for pattern, unit in time_patterns:
192
- match = re.search(pattern, text, re.IGNORECASE)
286
+ match = re.search(pattern, executable_text, re.IGNORECASE)
193
287
  if match:
194
288
  n = match.group(2)
195
289
  if unit == "DAY":
@@ -204,39 +298,24 @@ def extract_conditions(
204
298
 
205
299
  # Bare period expressions without explicit count
206
300
  if not any(c for c in conditions if "DATE_SUB" in c or "DATE_ADD" in c or "ADD_MONTHS" in c or "CURRENT_DATE" in c):
207
- if ("last week" in text or "本周" in text) and "this week" not in text and "本周" not in text:
301
+ if ("last week" in executable_text or "本周" in executable_text) and "this week" not in executable_text and "本周" not in executable_text:
208
302
  conditions.append(f"{date_col} >= DATE_SUB(CURRENT_DATE, 7)")
209
- if "last month" in text:
303
+ if "last month" in executable_text:
210
304
  conditions.append(f"{date_col} >= ADD_MONTHS(CURRENT_DATE, -1)")
211
- if "last year" in text:
305
+ if "last year" in executable_text:
212
306
  conditions.append(f"{date_col} >= ADD_MONTHS(CURRENT_DATE, -12)")
213
307
 
214
- if "本周" in text or "this week" in text:
308
+ if "本周" in executable_text or "this week" in executable_text:
215
309
  conditions.append(f"WEEKOFYEAR({date_col}) = WEEKOFYEAR(CURRENT_DATE)")
216
- if "本月" in text or "this month" in text:
310
+ if "本月" in executable_text or "this month" in executable_text:
217
311
  conditions.append(
218
312
  f"MONTH({date_col}) = MONTH(CURRENT_DATE) AND YEAR({date_col}) = YEAR(CURRENT_DATE)"
219
313
  )
220
- if "本年" in text or "this year" in text:
314
+ if "本年" in executable_text or "this year" in executable_text:
221
315
  conditions.append(f"YEAR({date_col}) = YEAR(CURRENT_DATE)")
222
316
 
223
- status_patterns = [
224
- (["有效", "active", "enabled", "valid"], "status = 'active'"),
225
- (["无效", "inactive", "disabled", "invalid"], "status = 'inactive'"),
226
- (["已删除", "deleted", "removed"], "is_deleted = 1"),
227
- (["未删除", "not deleted"], "is_deleted = 0"),
228
- (["已完成", "completed", "done", "finished"], "status = 'completed'"),
229
- (["未完成", "pending", "incomplete"], "status = 'pending'"),
230
- (["已支付", "paid"], "status = 'paid'"),
231
- (["未支付", "unpaid"], "status = 'unpaid'"),
232
- ]
233
- for keywords, condition in status_patterns:
234
- # Skip status pattern if the column was already matched via IN predicate
235
- col_match = re.match(r"^(\w+)\s*=", condition)
236
- col_name = col_match.group(1).lower() if col_match else None
237
- if col_name and col_name in in_matched_columns:
238
- continue
239
- if any(keyword in text for keyword in keywords):
240
- conditions.append(condition)
241
- break
317
+ status_conditions = extract_status_conditions(text)
318
+ for status_condition in status_conditions:
319
+ if status_condition.split(" ", 1)[0] not in in_matched_columns:
320
+ conditions.append(status_condition)
242
321
  return conditions
@@ -316,17 +316,6 @@ def generate_from_template(
316
316
  # never re-append them here as a second WHERE.
317
317
  if template.name == "top_n_query" and "DATE_SUB" not in c and "ADD_MONTHS" not in c and "DATE_ADD" not in c:
318
318
  continue
319
- if not any(
320
- kw in text_lower
321
- for kw in [
322
- "today", "yesterday", "tomorrow", "last", "past",
323
- "本周", "本月", "本年", "今天", "昨天", "明天"
324
- ]
325
- ) and c in [
326
- "status = 'active'", "status = 'inactive'",
327
- "is_deleted = 1", "is_deleted = 0"
328
- ]:
329
- continue
330
319
  merged_conditions.append(c)
331
320
  if merged_conditions:
332
321
  if "WHERE" in sql.upper():
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql-dialect-master"
3
- version = "1.4.0"
3
+ version = "1.4.1"
4
4
  description = "SQL Dialect Master - Multi-database SQL conversion engine"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sql-dialect-master
3
- Version: 1.4.0
3
+ Version: 1.4.1
4
4
  Summary: SQL Dialect Master - Multi-database SQL conversion engine
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -38,7 +38,7 @@ Dynamic: license-file
38
38
 
39
39
  Enterprise-grade multi-database SQL conversion engine supporting 12 database dialects.
40
40
 
41
- Current release: 1.4.0
41
+ Current release: 1.4.1
42
42
 
43
43
  [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
44
44
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)