sql-dialect-master 1.5.0__tar.gz → 1.6.0__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.5.0 → sql_dialect_master-1.6.0}/PKG-INFO +2 -2
  2. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/README.md +1 -1
  3. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/metadata.py +2 -2
  4. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/orchestrator.py +387 -6
  5. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/pyproject.toml +1 -1
  6. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/sql_dialect_master.egg-info/PKG-INFO +2 -2
  7. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/LICENSE +0 -0
  8. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/__init__.py +0 -0
  9. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/api/__init__.py +0 -0
  10. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/api/auth.py +0 -0
  11. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/api/client_identity.py +0 -0
  12. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/api/main.py +0 -0
  13. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/api/metrics.py +0 -0
  14. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/api/middleware.py +0 -0
  15. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/api/rate_limit_store.py +0 -0
  16. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/api/readiness.py +0 -0
  17. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/api/workers.py +0 -0
  18. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/__init__.py +0 -0
  19. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/cache.py +0 -0
  20. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/column_hint_validation.py +0 -0
  21. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/config.py +0 -0
  22. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/constants.py +0 -0
  23. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/exceptions.py +0 -0
  24. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/function_call_scanner.py +0 -0
  25. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/function_semantics.py +0 -0
  26. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/functions_db.json +0 -0
  27. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/functions_lookup.py +0 -0
  28. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql.py +0 -0
  29. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_components/__init__.py +0 -0
  30. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_components/aggregations.py +0 -0
  31. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
  32. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_components/core_extract.py +0 -0
  33. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_components/evidence.py +0 -0
  34. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_components/mappings.py +0 -0
  35. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_components/relations.py +0 -0
  36. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_components/templates.py +0 -0
  37. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_components/tokenizer.py +0 -0
  38. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_legacy.py +0 -0
  39. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_llm.py +0 -0
  40. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/__init__.py +0 -0
  41. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
  42. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
  43. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/dialect.py +0 -0
  44. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/intent.py +0 -0
  45. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/ir.py +0 -0
  46. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/models.py +0 -0
  47. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/predicates.py +0 -0
  48. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/renderer.py +0 -0
  49. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/resolution.py +0 -0
  50. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
  51. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/templates.py +0 -0
  52. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_pipeline/validation.py +0 -0
  53. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/nl2sql_schema.py +0 -0
  54. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/p1_sql_scanner.py +0 -0
  55. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/p2_data_validation.py +0 -0
  56. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/parser.py +0 -0
  57. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/post_processor.py +0 -0
  58. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/rules.py +0 -0
  59. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/semantic_diff.py +0 -0
  60. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/sql_statements.py +0 -0
  61. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/transpiler.py +0 -0
  62. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/type_mapping.json +0 -0
  63. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/core/type_mapping.py +0 -0
  64. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/utils/__init__.py +0 -0
  65. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/backend/utils/validators.py +0 -0
  66. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/setup.cfg +0 -0
  67. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/sql_dialect_master.egg-info/SOURCES.txt +0 -0
  68. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
  69. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/sql_dialect_master.egg-info/requires.txt +0 -0
  70. {sql_dialect_master-1.5.0 → sql_dialect_master-1.6.0}/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.5.0
3
+ Version: 1.6.0
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.5.0
41
+ Current release: 1.6.0
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.5.0
5
+ Current release: 1.6.0
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.5.0"
21
- API_VERSION = "1.5.0"
20
+ PROJECT_VERSION = "1.6.0"
21
+ API_VERSION = "1.6.0"
22
22
  # Frontend v2 architecture — independent of the product release version.
23
23
  FRONTEND_ARCHITECTURE_VERSION = 2
24
24
 
@@ -98,6 +98,33 @@ _DATE_INTERVAL = re.compile(
98
98
  re.IGNORECASE,
99
99
  )
100
100
  MAX_NL2SQL_DATE_INTERVAL_DAYS = 36525
101
+ _SCHEMA_WHOSE_PREFIX = re.compile(
102
+ r"\bwhose\s+"
103
+ r"([A-Za-z_][A-Za-z0-9_$]*|"
104
+ r"[A-Za-z_][A-Za-z0-9_$]*\.[A-Za-z_][A-Za-z0-9_$]*|"
105
+ r"[A-Za-z_][A-Za-z0-9_$]*\.[A-Za-z_][A-Za-z0-9_$]*\.[A-Za-z_][A-Za-z0-9_$]*)\s+"
106
+ r"(is\s+not|isn't|is|equals|<>|!=|>=|<=|=|>|<)\s*",
107
+ re.IGNORECASE,
108
+ )
109
+ _SCHEMA_WHOSE_REJECTED_VALUES = {
110
+ "and", "or", "not", "is", "null", "true", "false", "unknown",
111
+ "select", "from", "where", "group", "order", "by", "limit",
112
+ "current_date", "current_timestamp", "now",
113
+ }
114
+ _SCHEMA_WHOSE_CJK_CONNECTORS = {"和", "或", "且", "并", "与", "跟", "及"}
115
+ _SCHEMA_JOINED_ON_PREFIX = re.compile(
116
+ r"\b(?:(inner|left|right)\s+)?joined\s+on\b",
117
+ re.IGNORECASE,
118
+ )
119
+ _SCHEMA_JOINED_ON_OPERAND = re.compile(
120
+ r"[A-Za-z_][A-Za-z0-9_$]*\.[A-Za-z_][A-Za-z0-9_$]*"
121
+ r"(?:\.[A-Za-z_][A-Za-z0-9_$]*)?"
122
+ )
123
+ _SCHEMA_JOINED_WITH_PREFIX = re.compile(
124
+ r"\b(?:(inner|left|right)\s+)?joined\s+with\s+"
125
+ r"([A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)?)\b",
126
+ re.IGNORECASE,
127
+ )
101
128
  _SCHEMA_GROUP_LABEL_COLUMNS = {
102
129
  "部门": ("department", "dept"),
103
130
  "状态": ("status",),
@@ -1135,6 +1162,325 @@ class PipelineNL2SQLGenerator:
1135
1162
  resolved.append(rewritten)
1136
1163
  return resolved
1137
1164
 
1165
+ @staticmethod
1166
+ def _schema_mask_span(
1167
+ text: str, span: tuple[int, int]
1168
+ ) -> str:
1169
+ start, end = span
1170
+ return text[:start] + " " * (end - start) + text[end:]
1171
+
1172
+ def _schema_resolve_whose(
1173
+ self,
1174
+ text: str,
1175
+ table: SchemaTableContext,
1176
+ schema: SchemaContext,
1177
+ ) -> tuple[str, tuple[int, int]] | None:
1178
+ """Resolve one schema-aware ``whose`` predicate in linear time."""
1179
+ executable = mask_non_executable(text)
1180
+ match = _SCHEMA_WHOSE_PREFIX.search(executable)
1181
+ if match is None:
1182
+ return None
1183
+ whose_matches = list(_SCHEMA_WHOSE_PREFIX.finditer(executable))
1184
+ if len(whose_matches) != 1:
1185
+ raise SchemaValidationError("multiple whose predicates are unsupported")
1186
+ original_match = _SCHEMA_WHOSE_PREFIX.match(text, match.start())
1187
+ if original_match is None:
1188
+ raise SchemaValidationError("whose predicate is not executable")
1189
+ match = original_match
1190
+
1191
+ raw_column, raw_operator = match.groups()
1192
+ parts = raw_column.split(".")
1193
+ column_name = parts[-1]
1194
+ if len(parts) > 1:
1195
+ candidates = schema.table_candidates(".".join(parts[:-1]))
1196
+ if len(candidates) != 1 or candidates[0].key != table.key:
1197
+ raise SchemaValidationError("whose column table is not the selected table")
1198
+ canonical = schema.canonical_column(table, column_name)
1199
+ if canonical is None:
1200
+ if table.has_column_metadata:
1201
+ raise SchemaValidationError("whose column is outside the schema")
1202
+ canonical = column_name
1203
+
1204
+ position = match.end()
1205
+ if position >= len(text):
1206
+ raise SchemaValidationError("whose predicate value is missing")
1207
+ quote = text[position] if text[position] in "'\"" else None
1208
+ value_end = position
1209
+ if quote is not None:
1210
+ position += 1
1211
+ value_chars: list[str] = []
1212
+ closed = False
1213
+ while position < len(text):
1214
+ character = text[position]
1215
+ if character == "\\" and position + 1 < len(text):
1216
+ value_chars.extend((character, text[position + 1]))
1217
+ position += 2
1218
+ continue
1219
+ if character == quote:
1220
+ if position + 1 < len(text) and text[position + 1] == quote:
1221
+ value_chars.append(quote)
1222
+ position += 2
1223
+ continue
1224
+ closed = True
1225
+ position += 1
1226
+ break
1227
+ value_chars.append(character)
1228
+ position += 1
1229
+ if not closed:
1230
+ raise SchemaValidationError("whose quoted value is not closed")
1231
+ value = "".join(value_chars)
1232
+ literal = "'" + value.replace("'", "''") + "'"
1233
+ value_end = position
1234
+ else:
1235
+ numeric = re.match(
1236
+ r"-?(?:\d+(?:\.\d*)?|\.\d+)(?![\w$])",
1237
+ text[position:],
1238
+ )
1239
+ if numeric is not None:
1240
+ literal = numeric.group(0)
1241
+ value_end = position + numeric.end()
1242
+ else:
1243
+ null_match = re.match(r"null(?![\w$])", text[position:], re.IGNORECASE)
1244
+ if null_match is not None:
1245
+ literal = "NULL"
1246
+ value_end = position + null_match.end()
1247
+ else:
1248
+ cjk_token = re.match(r"[\u4e00-\u9fff]+", text[position:])
1249
+ if cjk_token is not None:
1250
+ value = cjk_token.group(0)
1251
+ if any(connector in value for connector in _SCHEMA_WHOSE_CJK_CONNECTORS):
1252
+ raise SchemaValidationError("whose CJK value contains a connector")
1253
+ literal = "'" + value.replace("'", "''") + "'"
1254
+ value_end = position + cjk_token.end()
1255
+ else:
1256
+ token = re.match(r"[A-Za-z_][A-Za-z0-9_$]*(?![\w$])", text[position:])
1257
+ if token is None:
1258
+ raise SchemaValidationError("whose value is not a supported literal")
1259
+ value = token.group(0)
1260
+ if value.casefold() in _SCHEMA_WHOSE_REJECTED_VALUES:
1261
+ raise SchemaValidationError("whose value is a reserved word")
1262
+ after_token = text[position + token.end():].lstrip()
1263
+ if after_token.startswith("("):
1264
+ raise SchemaValidationError("whose function values are unsupported")
1265
+ literal = "'" + value.replace("'", "''") + "'"
1266
+ value_end = position + token.end()
1267
+
1268
+ operator = raw_operator.casefold()
1269
+ if literal == "NULL":
1270
+ if operator in {"is", "is not", "isn't"}:
1271
+ predicate = f"{canonical} IS {'NOT ' if operator != 'is' else ''}NULL"
1272
+ else:
1273
+ raise SchemaValidationError("comparison with NULL is unsupported")
1274
+ else:
1275
+ normalized = {
1276
+ "is": "=", "equals": "=", "is not": "!=", "isn't": "!=",
1277
+ "<>": "!=",
1278
+ }.get(operator, raw_operator)
1279
+ predicate = f"{canonical} {normalized} {literal}"
1280
+
1281
+ tail = executable[value_end:]
1282
+ if re.search(r"\bor\b|\bwhose\b|[()]", tail, re.IGNORECASE):
1283
+ raise SchemaValidationError("complex whose predicates are unsupported")
1284
+ stripped_tail = tail.strip()
1285
+ if stripped_tail and not re.match(r"^and\b", stripped_tail, re.IGNORECASE):
1286
+ raise SchemaValidationError("unexpected text after whose predicate")
1287
+ span = (match.start(), value_end)
1288
+ return predicate, span
1289
+
1290
+ def _schema_resolve_joined_on(
1291
+ self,
1292
+ text: str,
1293
+ table_hint: Optional[str],
1294
+ schema: SchemaContext,
1295
+ ) -> tuple[SchemaTableContext, list[dict], tuple[int, int]] | None:
1296
+ """Resolve one schema-aware qualified equality after ``joined on``."""
1297
+ executable = mask_non_executable(text)
1298
+ prefixes = list(_SCHEMA_JOINED_ON_PREFIX.finditer(executable))
1299
+ if not prefixes:
1300
+ return None
1301
+ if re.search(r"\bjoined\s+with\b", executable, re.IGNORECASE):
1302
+ raise SchemaValidationError("mixed joined-on and joined-with clauses are unsupported")
1303
+ if len(prefixes) != 1:
1304
+ raise SchemaValidationError("multiple joined-on clauses are unsupported")
1305
+ if re.search(r"\b(?:per|each)\b|每个|各个|每位", executable, re.IGNORECASE):
1306
+ raise SchemaValidationError("joined-on with group labels is unsupported")
1307
+ prefix = prefixes[0]
1308
+ original_prefix = _SCHEMA_JOINED_ON_PREFIX.match(text, prefix.start())
1309
+ if original_prefix is None:
1310
+ raise SchemaValidationError("joined-on clause is not executable")
1311
+ prefix = original_prefix
1312
+ kind = (prefix.group(1) or "inner").upper()
1313
+ before_mentions = self._schema_table_mentions(text[:prefix.start()], schema)
1314
+ if table_hint:
1315
+ subject_candidates = schema.table_candidates(table_hint)
1316
+ if len(subject_candidates) != 1:
1317
+ raise SchemaValidationError("joined-on subject hint is ambiguous")
1318
+ subject = subject_candidates[0]
1319
+ elif before_mentions:
1320
+ subject = before_mentions[0]
1321
+ else:
1322
+ raise SchemaValidationError("joined-on subject is missing")
1323
+
1324
+ remainder = executable[prefix.end():]
1325
+ left_match = _SCHEMA_JOINED_ON_OPERAND.match(remainder.lstrip())
1326
+ if left_match is None:
1327
+ raise SchemaValidationError("joined-on left endpoint is invalid")
1328
+ left_start = prefix.end() + (len(remainder) - len(remainder.lstrip()))
1329
+ left_raw = left_match.group(0)
1330
+ after_left = remainder.lstrip()[left_match.end():]
1331
+ equals = re.match(r"\s*=\s*", after_left)
1332
+ if equals is None:
1333
+ raise SchemaValidationError("joined-on requires one equality")
1334
+ right_offset = left_start + left_match.end() + equals.end()
1335
+ right_match = _SCHEMA_JOINED_ON_OPERAND.match(executable[right_offset:])
1336
+ if right_match is None:
1337
+ raise SchemaValidationError("joined-on right endpoint is invalid")
1338
+ right_raw = right_match.group(0)
1339
+ value_end = right_offset + right_match.end()
1340
+
1341
+ tail = executable[value_end:]
1342
+ stripped_tail = tail.strip()
1343
+ if stripped_tail and not re.match(r"^(?:where|whose)\b", stripped_tail, re.IGNORECASE):
1344
+ raise SchemaValidationError("unexpected text after joined-on equality")
1345
+
1346
+ def resolve_endpoint(raw: str) -> tuple[SchemaTableContext, str]:
1347
+ parts = raw.split(".")
1348
+ table_name = ".".join(parts[:-1])
1349
+ candidates = schema.table_candidates(table_name)
1350
+ if len(candidates) != 1:
1351
+ raise SchemaValidationError("joined-on table is not unique")
1352
+ table = candidates[0]
1353
+ column = schema.canonical_column(table, parts[-1])
1354
+ if column is None:
1355
+ raise SchemaValidationError("joined-on column is outside the schema")
1356
+ return table, column
1357
+
1358
+ left_table, left_column = resolve_endpoint(left_raw)
1359
+ right_table, right_column = resolve_endpoint(right_raw)
1360
+ if left_table.key == right_table.key:
1361
+ raise SchemaValidationError("self joined-on is unsupported")
1362
+ endpoint_tables = {left_table.key, right_table.key}
1363
+ if subject.key not in endpoint_tables:
1364
+ raise SchemaValidationError("joined-on subject is not an endpoint")
1365
+ target_keys = endpoint_tables - {subject.key}
1366
+ if len(target_keys) != 1:
1367
+ raise SchemaValidationError("joined-on target is ambiguous")
1368
+ target = right_table if right_table.key in target_keys else left_table
1369
+ mentions = self._schema_table_mentions(text, schema)
1370
+ if {table.key for table in mentions} - endpoint_tables:
1371
+ raise SchemaValidationError("joined-on contains an extra table")
1372
+
1373
+ raw_relationships = [
1374
+ relationship for relationship in schema.relationships
1375
+ if {
1376
+ relationship.from_table.casefold(), relationship.to_table.casefold()
1377
+ } == endpoint_tables
1378
+ ]
1379
+ if raw_relationships:
1380
+ def matches_relationship(relationship) -> bool:
1381
+ declared = {
1382
+ (relationship.from_table.casefold(), relationship.from_column.casefold()),
1383
+ (relationship.to_table.casefold(), relationship.to_column.casefold()),
1384
+ }
1385
+ supplied = {
1386
+ (left_table.key, left_column.casefold()),
1387
+ (right_table.key, right_column.casefold()),
1388
+ }
1389
+ return declared == supplied
1390
+
1391
+ if sum(matches_relationship(item) for item in raw_relationships) != 1:
1392
+ raise SchemaValidationError("joined-on does not match a unique relationship")
1393
+
1394
+ condition = (
1395
+ f"{left_table.name}.{left_column} = "
1396
+ f"{right_table.name}.{right_column}"
1397
+ )
1398
+ span = (prefix.start(), value_end)
1399
+ return subject, [{
1400
+ "type": kind,
1401
+ "table": target.name,
1402
+ "subject": subject.name,
1403
+ "condition": condition,
1404
+ "relational": False,
1405
+ "join_spec": JoinSpec(kind=kind, matched_text="joined on"),
1406
+ }], span
1407
+
1408
+ def _schema_resolve_joined_with(
1409
+ self,
1410
+ text: str,
1411
+ table_hint: Optional[str],
1412
+ schema: SchemaContext,
1413
+ ) -> tuple[SchemaTableContext, list[dict], tuple[int, int]] | None:
1414
+ """Resolve one relationship-backed physical ``joined with`` clause."""
1415
+ executable = mask_non_executable(text)
1416
+ matches = list(_SCHEMA_JOINED_WITH_PREFIX.finditer(executable))
1417
+ if not matches:
1418
+ return None
1419
+ if len(matches) != 1 or re.search(r"\bjoined\s+on\b", executable, re.IGNORECASE):
1420
+ raise SchemaValidationError("multiple or mixed joined clauses are unsupported")
1421
+ match = matches[0]
1422
+ original_match = _SCHEMA_JOINED_WITH_PREFIX.match(text, match.start())
1423
+ if original_match is None:
1424
+ raise SchemaValidationError("joined-with clause is not executable")
1425
+ match = original_match
1426
+ if re.search(
1427
+ r"\b(?:per|each|group\s+by|grouped\s+by)\b|每个|各个|每位",
1428
+ executable,
1429
+ re.IGNORECASE,
1430
+ ):
1431
+ raise SchemaValidationError("joined-with grouping is unsupported")
1432
+
1433
+ kind = (match.group(1) or "inner").upper()
1434
+ target_name = match.group(2)
1435
+ target_candidates = schema.table_candidates(target_name)
1436
+ if len(target_candidates) != 1:
1437
+ raise SchemaValidationError("joined-with target is not unique")
1438
+ target = target_candidates[0]
1439
+ before_mentions = self._schema_table_mentions(text[:match.start()], schema)
1440
+ if table_hint:
1441
+ subject_candidates = schema.table_candidates(table_hint)
1442
+ if len(subject_candidates) != 1:
1443
+ raise SchemaValidationError("joined-with subject hint is ambiguous")
1444
+ subject = subject_candidates[0]
1445
+ elif before_mentions:
1446
+ subject = before_mentions[0]
1447
+ else:
1448
+ raise SchemaValidationError("joined-with subject is missing")
1449
+ if self._schema_text_match(text[:match.start()], target_name):
1450
+ raise SchemaValidationError("joined-with target is repeated before the clause")
1451
+ before_keys = {table.key for table in before_mentions}
1452
+ if before_keys - {subject.key, target.key}:
1453
+ raise SchemaValidationError("joined-with contains an extra table")
1454
+ if subject.key == target.key:
1455
+ raise SchemaValidationError("self joined-with is unsupported")
1456
+
1457
+ tail = executable[match.end():]
1458
+ stripped_tail = tail.strip()
1459
+ if stripped_tail and not re.match(r"^(?:where|whose)\b", stripped_tail, re.IGNORECASE):
1460
+ raise SchemaValidationError("unexpected text after joined-with target")
1461
+ relationships = [
1462
+ relationship for relationship in schema.relationships
1463
+ if {
1464
+ relationship.from_table.casefold(), relationship.to_table.casefold()
1465
+ } == {subject.key, target.key}
1466
+ ]
1467
+ if len(relationships) != 1:
1468
+ raise SchemaValidationError("joined-with relationship is not unique")
1469
+ relationship = relationships[0]
1470
+ condition = (
1471
+ f"{relationship.from_table}.{relationship.from_column} = "
1472
+ f"{relationship.to_table}.{relationship.to_column}"
1473
+ )
1474
+ span = (match.start(), match.end())
1475
+ return subject, [{
1476
+ "type": kind,
1477
+ "table": target.name,
1478
+ "subject": subject.name,
1479
+ "condition": condition,
1480
+ "relational": False,
1481
+ "join_spec": JoinSpec(kind=kind, matched_text="joined with"),
1482
+ }], span
1483
+
1138
1484
  def _schema_resolve_joins(
1139
1485
  self,
1140
1486
  text: str,
@@ -1267,9 +1613,19 @@ class PipelineNL2SQLGenerator:
1267
1613
  """Generate through the enhanced path with strict schema resolution."""
1268
1614
  try:
1269
1615
  executable_text = mask_non_executable(text)
1270
- label_resolution = self._schema_resolve_english_group_label(executable_text, schema)
1271
- if label_resolution is None:
1272
- label_resolution = self._schema_resolve_chinese_group_label(executable_text, schema)
1616
+ joined_on_resolution = self._schema_resolve_joined_on(
1617
+ text, table_hint, schema
1618
+ )
1619
+ joined_with_resolution = None
1620
+ if joined_on_resolution is None:
1621
+ joined_with_resolution = self._schema_resolve_joined_with(
1622
+ text, table_hint, schema
1623
+ )
1624
+ label_resolution = None
1625
+ if joined_on_resolution is None and joined_with_resolution is None:
1626
+ label_resolution = self._schema_resolve_english_group_label(executable_text, schema)
1627
+ if label_resolution is None:
1628
+ label_resolution = self._schema_resolve_chinese_group_label(executable_text, schema)
1273
1629
  ignored_label_spans = (label_resolution[2],) if label_resolution else ()
1274
1630
  unknown_tables = self._schema_unknown_table_patterns(
1275
1631
  text, schema, ignored_spans=ignored_label_spans
@@ -1280,7 +1636,15 @@ class PipelineNL2SQLGenerator:
1280
1636
  dialect,
1281
1637
  "The request references tables outside the provided schema.",
1282
1638
  )
1283
- if label_resolution is not None:
1639
+ joined_on_span = None
1640
+ joined_with_span = None
1641
+ if joined_on_resolution is not None:
1642
+ table_context, joins, joined_on_span = joined_on_resolution
1643
+ metric_text = self._schema_mask_span(text_lower, joined_on_span)
1644
+ elif joined_with_resolution is not None:
1645
+ table_context, joins, joined_with_span = joined_with_resolution
1646
+ metric_text = self._schema_mask_span(text_lower, joined_with_span)
1647
+ elif label_resolution is not None:
1284
1648
  table_context = label_resolution[0]
1285
1649
  label_group_column = label_resolution[1]
1286
1650
  joins = []
@@ -1289,6 +1653,21 @@ class PipelineNL2SQLGenerator:
1289
1653
  else:
1290
1654
  table_context, joins = self._schema_resolve_joins(text, table_hint, schema)
1291
1655
  metric_text = text_lower
1656
+ whose_resolution = self._schema_resolve_whose(text, table_context, schema)
1657
+ whose_condition = None
1658
+ condition_text = text_lower
1659
+ condition_original = text
1660
+ if joined_on_span is not None:
1661
+ condition_text = self._schema_mask_span(condition_text, joined_on_span)
1662
+ condition_original = self._schema_mask_span(condition_original, joined_on_span)
1663
+ elif joined_with_span is not None:
1664
+ condition_text = self._schema_mask_span(condition_text, joined_with_span)
1665
+ condition_original = self._schema_mask_span(condition_original, joined_with_span)
1666
+ if whose_resolution is not None:
1667
+ whose_condition, whose_span = whose_resolution
1668
+ metric_text = self._schema_mask_span(metric_text, whose_span)
1669
+ condition_text = self._schema_mask_span(condition_text, whose_span)
1670
+ condition_original = self._schema_mask_span(condition_original, whose_span)
1292
1671
  count_metric = self._schema_resolve_count_metric(
1293
1672
  text_lower, table_context, schema
1294
1673
  )
@@ -1300,11 +1679,13 @@ class PipelineNL2SQLGenerator:
1300
1679
  suppress_quantity_token=count_metric is not None,
1301
1680
  )
1302
1681
  conditions = self._schema_resolve_conditions(
1303
- text_lower,
1304
- self._extract_conditions_enhanced(text_lower, text),
1682
+ condition_text,
1683
+ self._extract_conditions_enhanced(condition_text, condition_original),
1305
1684
  table_context,
1306
1685
  schema,
1307
1686
  )
1687
+ if whose_condition is not None:
1688
+ conditions = [*conditions, whose_condition]
1308
1689
  parsed = self._parse_text(text_lower, text)
1309
1690
  parsed.update(analysis)
1310
1691
  operation = self._detect_operation(text_lower)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql-dialect-master"
3
- version = "1.5.0"
3
+ version = "1.6.0"
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.5.0
3
+ Version: 1.6.0
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.5.0
41
+ Current release: 1.6.0
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)