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.
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/PKG-INFO +2 -2
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/README.md +1 -1
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/metadata.py +2 -2
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/orchestrator.py +445 -11
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/pyproject.toml +1 -1
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/PKG-INFO +2 -2
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/LICENSE +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/__init__.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/__init__.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/auth.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/client_identity.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/main.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/metrics.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/middleware.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/rate_limit_store.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/readiness.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/api/workers.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/__init__.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/cache.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/column_hint_validation.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/config.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/constants.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/exceptions.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/function_call_scanner.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/function_semantics.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/functions_db.json +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/functions_lookup.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/__init__.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/aggregations.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/core_extract.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/evidence.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/mappings.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/relations.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/templates.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/tokenizer.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_legacy.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_llm.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/__init__.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/dialect.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/intent.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/ir.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/models.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/predicates.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/renderer.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/resolution.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/templates.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/validation.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_schema.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/p1_sql_scanner.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/p2_data_validation.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/parser.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/post_processor.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/rules.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/semantic_diff.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/sql_statements.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/transpiler.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/type_mapping.json +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/type_mapping.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/utils/__init__.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/utils/validators.py +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/setup.cfg +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/SOURCES.txt +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
- {sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
41
|
+
Current release: 1.5.0
|
|
42
42
|
|
|
43
43
|
[](https://www.python.org/downloads/)
|
|
44
44
|
[](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
|
+
Current release: 1.5.0
|
|
6
6
|
|
|
7
7
|
[](https://www.python.org/downloads/)
|
|
8
8
|
[](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.
|
|
21
|
-
API_VERSION = "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
|
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/orchestrator.py
RENAMED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
1332
|
+
render_columns,
|
|
899
1333
|
conditions,
|
|
900
1334
|
aggregations,
|
|
901
1335
|
group_by,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-dialect-master
|
|
3
|
-
Version: 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.
|
|
41
|
+
Current release: 1.5.0
|
|
42
42
|
|
|
43
43
|
[](https://www.python.org/downloads/)
|
|
44
44
|
[](https://opensource.org/licenses/MIT)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/column_hint_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/__init__.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/aggregations.py
RENAMED
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/core_extract.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/evidence.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/mappings.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/relations.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/templates.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_components/tokenizer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/__init__.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/aggregations.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/clause_extract.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/dialect.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/intent.py
RENAMED
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/models.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/predicates.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/renderer.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/resolution.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/suggestions.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/templates.py
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/backend/core/nl2sql_pipeline/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/requires.txt
RENAMED
|
File without changes
|
{sql_dialect_master-1.4.1 → sql_dialect_master-1.5.0}/sql_dialect_master.egg-info/top_level.txt
RENAMED
|
File without changes
|