sql-dialect-master 1.4.1__tar.gz → 1.5.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.4.1 → sql_dialect_master-1.5.0}/PKG-INFO +2 -2
  2. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/README.md +1 -1
  3. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/metadata.py +2 -2
  4. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/orchestrator.py +445 -11
  5. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/pyproject.toml +1 -1
  6. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/PKG-INFO +2 -2
  7. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/LICENSE +0 -0
  8. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/__init__.py +0 -0
  9. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/__init__.py +0 -0
  10. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/auth.py +0 -0
  11. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/client_identity.py +0 -0
  12. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/main.py +0 -0
  13. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/metrics.py +0 -0
  14. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/middleware.py +0 -0
  15. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/rate_limit_store.py +0 -0
  16. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/readiness.py +0 -0
  17. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/workers.py +0 -0
  18. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/__init__.py +0 -0
  19. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/cache.py +0 -0
  20. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/column_hint_validation.py +0 -0
  21. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/config.py +0 -0
  22. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/constants.py +0 -0
  23. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/exceptions.py +0 -0
  24. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/function_call_scanner.py +0 -0
  25. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/function_semantics.py +0 -0
  26. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/functions_db.json +0 -0
  27. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/functions_lookup.py +0 -0
  28. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql.py +0 -0
  29. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/__init__.py +0 -0
  30. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/aggregations.py +0 -0
  31. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
  32. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/core_extract.py +0 -0
  33. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/evidence.py +0 -0
  34. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/mappings.py +0 -0
  35. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/relations.py +0 -0
  36. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/templates.py +0 -0
  37. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/tokenizer.py +0 -0
  38. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_legacy.py +0 -0
  39. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_llm.py +0 -0
  40. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/__init__.py +0 -0
  41. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
  42. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
  43. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/dialect.py +0 -0
  44. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/intent.py +0 -0
  45. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/ir.py +0 -0
  46. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/models.py +0 -0
  47. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/predicates.py +0 -0
  48. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/renderer.py +0 -0
  49. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/resolution.py +0 -0
  50. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
  51. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/templates.py +0 -0
  52. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/validation.py +0 -0
  53. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_schema.py +0 -0
  54. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/p1_sql_scanner.py +0 -0
  55. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/p2_data_validation.py +0 -0
  56. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/parser.py +0 -0
  57. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/post_processor.py +0 -0
  58. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/rules.py +0 -0
  59. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/semantic_diff.py +0 -0
  60. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/sql_statements.py +0 -0
  61. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/transpiler.py +0 -0
  62. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/type_mapping.json +0 -0
  63. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/type_mapping.py +0 -0
  64. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/utils/__init__.py +0 -0
  65. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/utils/validators.py +0 -0
  66. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/setup.cfg +0 -0
  67. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/SOURCES.txt +0 -0
  68. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
  69. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/requires.txt +0 -0
  70. {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.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.4.1
3
+ Version: 1.5.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.4.1
41
+ Current release: 1.5.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.4.1
5
+ Current release: 1.5.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.4.1"
21
- API_VERSION = "1.4.1"
20
+ PROJECT_VERSION = "1.5.0"
21
+ API_VERSION = "1.5.0"
22
22
  # Frontend v2 architecture — independent of the product release version.
23
23
  FRONTEND_ARCHITECTURE_VERSION = 2
24
24
 
@@ -98,6 +98,18 @@ _DATE_INTERVAL = re.compile(
98
98
  re.IGNORECASE,
99
99
  )
100
100
  MAX_NL2SQL_DATE_INTERVAL_DAYS = 36525
101
+ _SCHEMA_GROUP_LABEL_COLUMNS = {
102
+ "部门": ("department", "dept"),
103
+ "状态": ("status",),
104
+ "类型": ("type",),
105
+ "分类": ("category",),
106
+ "日期": ("date",),
107
+ "地区": ("region",),
108
+ "区域": ("region",),
109
+ "品牌": ("brand",),
110
+ "级别": ("level",),
111
+ "优先级": ("priority",),
112
+ }
101
113
 
102
114
 
103
115
  class PipelineNL2SQLGenerator:
@@ -626,14 +638,20 @@ class PipelineNL2SQLGenerator:
626
638
  return [table for _, table in sorted(found.values(), key=lambda item: item[0])]
627
639
 
628
640
  def _schema_unknown_table_patterns(
629
- self, text: str, schema: SchemaContext
641
+ self,
642
+ text: str,
643
+ schema: SchemaContext,
644
+ ignored_spans: tuple[tuple[int, int], ...] = (),
630
645
  ) -> list[str]:
631
646
  """Return mapped table names mentioned by the request but absent in schema."""
647
+ for start, end in ignored_spans:
648
+ text = text[:start] + " " * (end - start) + text[end:]
632
649
  unknown: list[str] = []
633
650
  for pattern, mapped in self.TABLE_PATTERNS.items():
634
651
  if pattern == "order" and re.search(r"\border\b\s+by\b", text, re.IGNORECASE):
635
652
  continue
636
- if not self._schema_text_match(text, pattern):
653
+ match = self._schema_text_match(text, pattern)
654
+ if not match:
637
655
  continue
638
656
  if schema.table_candidates(mapped):
639
657
  continue
@@ -642,6 +660,152 @@ class PipelineNL2SQLGenerator:
642
660
  unknown.append(mapped)
643
661
  return sorted(set(unknown))
644
662
 
663
+ def _schema_entity_counter_tables(self, text: str) -> set[str]:
664
+ """Find entity counters without treating data words as counts."""
665
+ return {
666
+ mapped for pattern, mapped in self.TABLE_PATTERNS.items()
667
+ if re.search(re.escape(pattern) + r"数(?!据|字|组)", text, re.IGNORECASE)
668
+ }
669
+
670
+ def _schema_aggregation_intent(self, text: str) -> bool:
671
+ return bool(re.search(
672
+ r"\b(?:number\s+of|count(?:\s+of)?|how\s+many|sum|average|avg|maximum|minimum|max|min)\b|"
673
+ r"统计|数量|多少|几个|总数|个数|条数|件数|行数|人数|记录数|"
674
+ r"平均|总和|汇总|最大|最小|最多|最少",
675
+ text,
676
+ re.IGNORECASE,
677
+ )) or bool(self._schema_entity_counter_tables(text))
678
+
679
+ def _schema_resolve_english_group_label(
680
+ self, text: str, schema: SchemaContext
681
+ ) -> tuple[SchemaTableContext, str, tuple[int, int]] | None:
682
+ if not self._schema_aggregation_intent(text):
683
+ return None
684
+ match = re.search(
685
+ r"\b(?:per|each)\s+([A-Za-z_][A-Za-z0-9_$]*)\b",
686
+ text,
687
+ re.IGNORECASE,
688
+ )
689
+ if match is None:
690
+ return None
691
+ label = match.group(1)
692
+ if text[match.end():].strip(" \t\r\n,.;,。;"):
693
+ raise SchemaValidationError("multiple schema group labels are unsupported")
694
+ return self._schema_resolve_group_label(
695
+ text, label, (match.start(), match.end()), schema
696
+ )
697
+
698
+ def _schema_resolve_chinese_group_label(
699
+ self, text: str, schema: SchemaContext
700
+ ) -> tuple[SchemaTableContext, str, tuple[int, int]] | None:
701
+ if not self._schema_aggregation_intent(text):
702
+ return None
703
+ match = re.search(
704
+ r"(?:每个|各个|每位)\s*([\u4e00-\u9fff]{1,8}?)"
705
+ r"(?=的|员工|订单|产品|客户|交易|支付|日志|记录|数量|用户|$)",
706
+ text,
707
+ )
708
+ if match is None:
709
+ return None
710
+ if re.search(r"每个|各个|每位", text[match.end():]):
711
+ raise SchemaValidationError("multiple schema group labels are unsupported")
712
+ return self._schema_resolve_group_label(
713
+ text, match.group(1), (match.start(), match.end()), schema
714
+ )
715
+
716
+ def _schema_resolve_group_label(
717
+ self,
718
+ text: str,
719
+ label: str,
720
+ span: tuple[int, int],
721
+ schema: SchemaContext,
722
+ ) -> tuple[SchemaTableContext, str, tuple[int, int]]:
723
+ """Resolve one consumed label without treating its table as a JOIN."""
724
+ masked = text[:span[0]] + " " * (span[1] - span[0]) + text[span[1]:]
725
+ mentioned = self._schema_table_mentions(masked, schema)
726
+ if len(mentioned) != 1:
727
+ raise SchemaValidationError("schema group label subject is ambiguous")
728
+ data_table = mentioned[0]
729
+ column_names = (label, *_SCHEMA_GROUP_LABEL_COLUMNS.get(label, ()))
730
+ column_keys = {name.casefold() for name in column_names}
731
+ direct_columns = {
732
+ column for column in data_table.columns
733
+ if column.casefold() in column_keys
734
+ }
735
+ if len(direct_columns) > 1:
736
+ raise SchemaValidationError("schema group label column is ambiguous")
737
+ if direct_columns:
738
+ return data_table, direct_columns.pop(), span
739
+
740
+ label_tables = schema.table_candidates(label)
741
+ if not label_tables:
742
+ mapped_labels = [
743
+ mapped for pattern, mapped in self.TABLE_PATTERNS.items()
744
+ if pattern.casefold() == label.casefold()
745
+ ]
746
+ candidates = [
747
+ candidate
748
+ for mapped in mapped_labels
749
+ for candidate in schema.table_candidates(mapped)
750
+ ]
751
+ label_tables = tuple({candidate.key: candidate for candidate in candidates}.values())
752
+ if len(label_tables) != 1:
753
+ raise SchemaValidationError("schema group label is not a unique table")
754
+ label_table = label_tables[0]
755
+ if label_table.key == data_table.key:
756
+ raise SchemaValidationError("schema group label cannot be the subject table")
757
+
758
+ # Only an explicitly declared data-table -> label-table relationship
759
+ # proves that the data table carries the grouping foreign key. The
760
+ # relationship index also exposes synthetic reverse entries for join
761
+ # resolution, which must not be used here: accepting one would make a
762
+ # request such as "users per order" group users by their primary key.
763
+ relationships = tuple(
764
+ relationship
765
+ for relationship in schema.relationships
766
+ if relationship.from_table.casefold() == data_table.key
767
+ and relationship.to_table.casefold() == label_table.key
768
+ )
769
+ if len(relationships) == 1:
770
+ label_singular = (
771
+ label[:-1] if label.casefold().endswith("s") else label
772
+ )
773
+ table_basename = label_table.name.rsplit(".", 1)[-1]
774
+ table_singular = (
775
+ table_basename[:-1]
776
+ if table_basename.casefold().endswith("s")
777
+ else table_basename
778
+ )
779
+ allowed_foreign_keys = {
780
+ f"{label_singular}_id".casefold(),
781
+ f"{table_singular}_id".casefold(),
782
+ f"{table_basename}_id".casefold(),
783
+ }
784
+ from_column = relationships[0].from_column
785
+ if from_column.casefold() not in allowed_foreign_keys:
786
+ raise SchemaValidationError(
787
+ "schema group label relationship has an unsupported foreign key"
788
+ )
789
+ return data_table, from_column, span
790
+ if len(relationships) > 1:
791
+ raise SchemaValidationError("schema group label relationship is ambiguous")
792
+
793
+ table_basename = label_table.name.rsplit(".", 1)[-1]
794
+ singular = (
795
+ table_basename[:-1]
796
+ if table_basename.casefold().endswith("s")
797
+ else table_basename
798
+ )
799
+ foreign_keys = [
800
+ column for column in data_table.columns
801
+ if column.casefold() == (singular + "_id").casefold()
802
+ ]
803
+ if len(foreign_keys) > 1:
804
+ raise SchemaValidationError("schema group label foreign key is ambiguous")
805
+ if foreign_keys:
806
+ return data_table, foreign_keys[0], span
807
+ raise SchemaValidationError("schema group label relationship is missing")
808
+
645
809
  def _schema_resolve_table(
646
810
  self,
647
811
  text: str,
@@ -668,6 +832,7 @@ class PipelineNL2SQLGenerator:
668
832
  table: SchemaTableContext,
669
833
  column_hints: Optional[List[str]],
670
834
  schema: SchemaContext,
835
+ suppress_quantity_token: bool = False,
671
836
  ) -> list[str]:
672
837
  if column_hints:
673
838
  resolved: list[str] = []
@@ -689,10 +854,63 @@ class PipelineNL2SQLGenerator:
689
854
  )
690
855
  return resolved
691
856
 
692
- columns = self._extract_columns(text)
857
+ projection_source = text
858
+ if suppress_quantity_token:
859
+ projection_source = re.sub(r"数量", " " * len("数量"), projection_source)
860
+ projection_text = mask_non_executable(self._schema_projection_text(projection_source))
861
+ candidates: list[tuple[int, int, str]] = []
862
+ column_names = list(table.columns)
863
+ for column in column_names:
864
+ variants = [column]
865
+ if not column.casefold().endswith("s"):
866
+ variants.append(column + "s")
867
+ for variant in variants:
868
+ pattern = re.escape(variant)
869
+ if not re.search(r"[\u4e00-\u9fff]", variant):
870
+ pattern = rf"(?<![\w$]){pattern}(?![\w$])"
871
+ for match in re.finditer(pattern, projection_text, re.IGNORECASE):
872
+ candidates.append((match.start(), match.end(), column))
873
+ qualified = re.escape(table.name) + r"\." + re.escape(variant)
874
+ for match in re.finditer(qualified, projection_text, re.IGNORECASE):
875
+ candidates.append((match.start(), match.end(), column))
876
+
877
+ mapped_columns = self._extract_columns(projection_text)
878
+ if mapped_columns != ["*"]:
879
+ for mapped_column in mapped_columns:
880
+ canonical = schema.canonical_column(table, mapped_column)
881
+ if canonical is None:
882
+ raise SchemaValidationError("mapped projection column is outside the schema")
883
+ aliases = [
884
+ alias for alias, value in self.COLUMN_PATTERNS.items()
885
+ if value.casefold() == mapped_column.casefold()
886
+ ] + [canonical]
887
+ for alias in aliases:
888
+ pattern = re.escape(alias)
889
+ if not re.search(r"[\u4e00-\u9fff]", alias):
890
+ pattern = rf"(?<![\w$]){pattern}(?![\w$])"
891
+ for match in re.finditer(pattern, projection_text, re.IGNORECASE):
892
+ candidates.append((match.start(), match.end(), canonical))
893
+
894
+ selected: list[tuple[int, int, str]] = []
895
+ for candidate in sorted(candidates, key=lambda item: (item[0], -(item[1] - item[0]))):
896
+ if any(candidate[0] < existing[1] and existing[0] < candidate[1] for existing in selected):
897
+ continue
898
+ selected.append(candidate)
899
+ if selected:
900
+ deduplicated: list[str] = []
901
+ seen_columns: set[str] = set()
902
+ for _, _, column in selected:
903
+ key = column.casefold()
904
+ if key in seen_columns:
905
+ continue
906
+ seen_columns.add(key)
907
+ deduplicated.append(column)
908
+ return deduplicated
909
+
910
+ columns = self._extract_columns(projection_text)
693
911
  if columns == ["*"]:
694
912
  for candidate in table.columns:
695
- if self._schema_text_match(text, candidate):
913
+ if self._schema_text_match(projection_text, candidate):
696
914
  columns.append(candidate)
697
915
  if len(columns) > 1:
698
916
  columns.remove("*")
@@ -709,6 +927,187 @@ class PipelineNL2SQLGenerator:
709
927
  resolved.append(canonical)
710
928
  return resolved
711
929
 
930
+ @staticmethod
931
+ def _schema_projection_text(text: str) -> str:
932
+ """Mask explicit ordering clauses so their columns are not projected."""
933
+ match = re.search(
934
+ r"\b(?:order(?:ed)?|sort(?:ed)?)\s+by\b",
935
+ text,
936
+ re.IGNORECASE,
937
+ )
938
+ if match is None:
939
+ return text
940
+ return text[:match.start()] + " " * (len(text) - match.start())
941
+
942
+ def _schema_resolve_ordering(
943
+ self,
944
+ text: str,
945
+ table: SchemaTableContext,
946
+ schema: SchemaContext,
947
+ ) -> Optional[Tuple[str, str]]:
948
+ """Resolve one explicit schema-aware ORDER BY target."""
949
+ match = re.search(
950
+ r"\b(?:order(?:ed)?|sort(?:ed)?)\s+by\s+"
951
+ r"([A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)?)"
952
+ r"(.*)$",
953
+ text,
954
+ re.IGNORECASE,
955
+ )
956
+ if match is None:
957
+ return self._extract_ordering(text)
958
+ raw_column, tail = match.groups()
959
+ direction = "ASC"
960
+ direction_match = re.match(
961
+ r"\s+(ascending|descending|asc|desc|升序|降序)\b",
962
+ tail,
963
+ re.IGNORECASE,
964
+ )
965
+ if direction_match:
966
+ direction = "DESC" if direction_match.group(1).casefold() in {"descending", "desc", "降序"} else "ASC"
967
+ tail = tail[direction_match.end():]
968
+ if tail.strip(" \t\r\n,.;,。;"):
969
+ raise SchemaValidationError("multiple schema-aware ordering columns are unsupported")
970
+ parts = raw_column.split(".")
971
+ column_name = parts[-1]
972
+ if len(parts) > 1:
973
+ candidates = schema.table_candidates(".".join(parts[:-1]))
974
+ if len(candidates) != 1 or candidates[0].key != table.key:
975
+ raise SchemaValidationError("ordering column table is not the selected schema table")
976
+ canonical = schema.canonical_column(table, column_name)
977
+ if canonical is None:
978
+ raise SchemaValidationError("ordering column is outside the schema")
979
+ return canonical, direction
980
+
981
+ def _schema_resolve_count_metric(
982
+ self,
983
+ text: str,
984
+ table: SchemaTableContext,
985
+ schema: SchemaContext,
986
+ ) -> tuple[str, str | None] | None:
987
+ count_intent = bool(re.search(
988
+ r"统计|数量|多少|几个|总数|个数|条数|件数|行数|人数|记录数|"
989
+ r"\bnumber\s+of\b|\bcount(?:\s+of)?\b|\bhow\s+many\b",
990
+ text,
991
+ re.IGNORECASE,
992
+ ))
993
+ if not count_intent:
994
+ for mapped in self._schema_entity_counter_tables(text):
995
+ candidates = schema.table_candidates(mapped)
996
+ if len(candidates) == 1 and candidates[0].key == table.key:
997
+ count_intent = True
998
+ break
999
+ measure_table: str | None = None
1000
+ if "库存数量" in text:
1001
+ measure_table = "inventory"
1002
+ elif "购买数量" in text:
1003
+ measure_table = "orders"
1004
+ if measure_table is not None:
1005
+ if table.name.rsplit(".", 1)[-1].casefold() != measure_table:
1006
+ raise SchemaValidationError("quantity measure table is not selected")
1007
+ quantity = schema.canonical_column(table, "quantity")
1008
+ if quantity is None:
1009
+ raise SchemaValidationError("quantity measure is outside the schema")
1010
+ return "sum", quantity
1011
+ if count_intent:
1012
+ return "count", None
1013
+ return None
1014
+
1015
+ def _schema_extract_aggregations(
1016
+ self,
1017
+ text: str,
1018
+ table: SchemaTableContext | None = None,
1019
+ schema: SchemaContext | None = None,
1020
+ count_metric: tuple[str, str | None] | None = None,
1021
+ ) -> list[str]:
1022
+ existing = self._extract_aggregations(text)
1023
+ if count_metric is not None and count_metric[0] == "sum":
1024
+ return [f"SUM({count_metric[1]})"]
1025
+ if count_metric is not None and count_metric[0] == "count" and not any(
1026
+ aggregation.upper().startswith("COUNT(") for aggregation in existing
1027
+ ):
1028
+ existing = [*existing, "COUNT(*) AS count"]
1029
+ if table is None or schema is None or not table.has_column_metadata:
1030
+ return existing
1031
+
1032
+ group_match = re.search(r"\b(?:group(?:ed)?\s+by|per|each|by)\b", text, re.IGNORECASE)
1033
+ metric_text = text if group_match is None else text[:group_match.start()]
1034
+ metric_candidates: list[tuple[int, str]] = []
1035
+ for column in table.columns:
1036
+ pattern = rf"(?<![\w$]){re.escape(column)}(?![\w$])"
1037
+ match = re.search(pattern, metric_text, re.IGNORECASE)
1038
+ if match:
1039
+ metric_candidates.append((match.start(), column))
1040
+ if metric_candidates:
1041
+ metric = min(metric_candidates, key=lambda item: item[0])[1]
1042
+ existing = [
1043
+ re.sub(r"^(AVG|SUM|MAX|MIN)\([^)]*\)$", lambda m: f"{m.group(1)}({metric})", aggregation, flags=re.IGNORECASE)
1044
+ if re.match(r"^(AVG|SUM|MAX|MIN)\(", aggregation, re.IGNORECASE)
1045
+ else aggregation
1046
+ for aggregation in existing
1047
+ ]
1048
+ return existing
1049
+
1050
+ def _schema_resolve_group_by(
1051
+ self,
1052
+ text: str,
1053
+ table: SchemaTableContext,
1054
+ schema: SchemaContext,
1055
+ aggregations: list[str],
1056
+ ) -> list[str]:
1057
+ lowered = text.lower()
1058
+ match = re.search(
1059
+ r"\b(?:group\s+by|grouped\s+by)\s+"
1060
+ r"([A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)?)(.*)$",
1061
+ lowered,
1062
+ re.IGNORECASE,
1063
+ )
1064
+ match_kind = "group"
1065
+ if match is None and aggregations:
1066
+ if not re.search(r"\b(?:ordered|sorted|order|sort)\s+by\b", lowered):
1067
+ match = re.search(
1068
+ r"\b(?:per|each|by)\s+"
1069
+ r"([A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)?)(.*)$",
1070
+ lowered,
1071
+ re.IGNORECASE,
1072
+ )
1073
+ match_kind = "aggregation-group"
1074
+ chinese_group_intent = bool(re.search(r"每个|各个|每位|分组|汇总|按[^\s,,。;]+(?:分组|汇总|统计)", text))
1075
+ if match is None:
1076
+ if chinese_group_intent:
1077
+ raise SchemaValidationError("schema group column is not explicitly resolvable")
1078
+ return []
1079
+ raw_column, tail = match.groups()
1080
+ if tail.strip(" \t\r\n,.;,。;"):
1081
+ raise SchemaValidationError("multiple schema group columns are unsupported")
1082
+ parts = raw_column.split(".")
1083
+ if len(parts) > 1:
1084
+ candidates = schema.table_candidates(".".join(parts[:-1]))
1085
+ if len(candidates) != 1 or candidates[0].key != table.key:
1086
+ raise SchemaValidationError("group column table is not the selected schema table")
1087
+ canonical = schema.canonical_column(table, parts[-1])
1088
+ if canonical is None:
1089
+ if not table.has_column_metadata and match_kind == "group":
1090
+ return [parts[-1]]
1091
+ raise SchemaValidationError("group column is outside the schema")
1092
+ return [canonical]
1093
+
1094
+ @staticmethod
1095
+ def _schema_has_explicit_ordering(text: str) -> bool:
1096
+ lowered = text.lower()
1097
+ phrases = (
1098
+ "order by", "ordered by", "sort by", "sorted by",
1099
+ "ascending", "descending", "asc", "desc", "sort", "sorted",
1100
+ "top", "highest", "lowest", "max", "min",
1101
+ "升序", "降序", "排序", "最大", "最小", "最多", "最少",
1102
+ )
1103
+ for phrase in phrases:
1104
+ if phrase.isascii():
1105
+ if re.search(r"(?<![\w$])" + re.escape(phrase) + r"(?![\w$])", lowered):
1106
+ return True
1107
+ elif phrase in text:
1108
+ return True
1109
+ return False
1110
+
712
1111
  def _schema_resolve_conditions(
713
1112
  self,
714
1113
  text: str,
@@ -867,16 +1266,38 @@ class PipelineNL2SQLGenerator:
867
1266
  ) -> NL2SQLResult:
868
1267
  """Generate through the enhanced path with strict schema resolution."""
869
1268
  try:
870
- unknown_tables = self._schema_unknown_table_patterns(text, schema)
1269
+ 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)
1273
+ ignored_label_spans = (label_resolution[2],) if label_resolution else ()
1274
+ unknown_tables = self._schema_unknown_table_patterns(
1275
+ text, schema, ignored_spans=ignored_label_spans
1276
+ )
871
1277
  if unknown_tables:
872
1278
  return self._schema_failure(
873
1279
  text,
874
1280
  dialect,
875
1281
  "The request references tables outside the provided schema.",
876
1282
  )
877
- table_context, joins = self._schema_resolve_joins(text, table_hint, schema)
1283
+ if label_resolution is not None:
1284
+ table_context = label_resolution[0]
1285
+ label_group_column = label_resolution[1]
1286
+ joins = []
1287
+ start, end = label_resolution[2]
1288
+ metric_text = text_lower[:start] + " " * (end - start) + text_lower[end:]
1289
+ else:
1290
+ table_context, joins = self._schema_resolve_joins(text, table_hint, schema)
1291
+ metric_text = text_lower
1292
+ count_metric = self._schema_resolve_count_metric(
1293
+ text_lower, table_context, schema
1294
+ )
878
1295
  columns = self._schema_resolve_columns(
879
- text_lower, table_context, column_hints, schema
1296
+ metric_text,
1297
+ table_context,
1298
+ column_hints,
1299
+ schema,
1300
+ suppress_quantity_token=count_metric is not None,
880
1301
  )
881
1302
  conditions = self._schema_resolve_conditions(
882
1303
  text_lower,
@@ -887,15 +1308,28 @@ class PipelineNL2SQLGenerator:
887
1308
  parsed = self._parse_text(text_lower, text)
888
1309
  parsed.update(analysis)
889
1310
  operation = self._detect_operation(text_lower)
890
- aggregations = self._extract_aggregations(text_lower)
891
- group_by = self._extract_group_by(text_lower)
892
- ordering = self._extract_ordering(text_lower)
1311
+ aggregations = self._schema_extract_aggregations(
1312
+ metric_text, table_context, schema, count_metric
1313
+ )
1314
+ group_by = (
1315
+ [label_group_column]
1316
+ if label_resolution is not None
1317
+ else self._schema_resolve_group_by(
1318
+ text_lower, table_context, schema, aggregations
1319
+ )
1320
+ )
1321
+ ordering = (
1322
+ self._schema_resolve_ordering(text_lower, table_context, schema)
1323
+ if self._schema_has_explicit_ordering(text_lower)
1324
+ else None
1325
+ )
893
1326
  limit = self._extract_limit(text_lower)
894
1327
  distinct = self._check_distinct(text_lower)
1328
+ render_columns = ["*"] if aggregations and not group_by else columns
895
1329
  sql, explanation, confidence, evidence = self._build_sql_enhanced(
896
1330
  operation,
897
1331
  table_context.name,
898
- columns,
1332
+ render_columns,
899
1333
  conditions,
900
1334
  aggregations,
901
1335
  group_by,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sql-dialect-master"
3
- version = "1.4.1"
3
+ version = "1.5.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.4.1
3
+ Version: 1.5.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.4.1
41
+ Current release: 1.5.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)