sql-dialect-master 1.3.0__tar.gz → 1.4.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.3.0 → sql_dialect_master-1.4.0}/PKG-INFO +15 -2
  2. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/README.md +14 -1
  3. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/api/main.py +58 -20
  4. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/metadata.py +2 -2
  5. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_llm.py +11 -115
  6. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/orchestrator.py +376 -0
  7. sql_dialect_master-1.4.0/backend/core/nl2sql_schema.py +403 -0
  8. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/pyproject.toml +1 -1
  9. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/sql_dialect_master.egg-info/PKG-INFO +15 -2
  10. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/sql_dialect_master.egg-info/SOURCES.txt +1 -0
  11. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/LICENSE +0 -0
  12. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/__init__.py +0 -0
  13. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/api/__init__.py +0 -0
  14. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/api/auth.py +0 -0
  15. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/api/client_identity.py +0 -0
  16. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/api/metrics.py +0 -0
  17. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/api/middleware.py +0 -0
  18. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/api/rate_limit_store.py +0 -0
  19. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/api/readiness.py +0 -0
  20. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/api/workers.py +0 -0
  21. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/__init__.py +0 -0
  22. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/cache.py +0 -0
  23. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/column_hint_validation.py +0 -0
  24. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/config.py +0 -0
  25. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/constants.py +0 -0
  26. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/exceptions.py +0 -0
  27. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/function_call_scanner.py +0 -0
  28. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/function_semantics.py +0 -0
  29. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/functions_db.json +0 -0
  30. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/functions_lookup.py +0 -0
  31. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql.py +0 -0
  32. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_components/__init__.py +0 -0
  33. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_components/aggregations.py +0 -0
  34. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_components/boolean_conditions.py +0 -0
  35. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_components/core_extract.py +0 -0
  36. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_components/evidence.py +0 -0
  37. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_components/mappings.py +0 -0
  38. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_components/relations.py +0 -0
  39. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_components/templates.py +0 -0
  40. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_components/tokenizer.py +0 -0
  41. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_legacy.py +0 -0
  42. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/__init__.py +0 -0
  43. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/aggregations.py +0 -0
  44. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/clause_extract.py +0 -0
  45. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/dialect.py +0 -0
  46. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/intent.py +0 -0
  47. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/ir.py +0 -0
  48. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/models.py +0 -0
  49. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/predicates.py +0 -0
  50. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/renderer.py +0 -0
  51. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/resolution.py +0 -0
  52. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/suggestions.py +0 -0
  53. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/templates.py +0 -0
  54. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/nl2sql_pipeline/validation.py +0 -0
  55. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/p1_sql_scanner.py +0 -0
  56. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/p2_data_validation.py +0 -0
  57. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/parser.py +0 -0
  58. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/post_processor.py +0 -0
  59. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/rules.py +0 -0
  60. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/semantic_diff.py +0 -0
  61. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/sql_statements.py +0 -0
  62. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/transpiler.py +0 -0
  63. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/type_mapping.json +0 -0
  64. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/core/type_mapping.py +0 -0
  65. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/utils/__init__.py +0 -0
  66. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/backend/utils/validators.py +0 -0
  67. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/setup.cfg +0 -0
  68. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/sql_dialect_master.egg-info/dependency_links.txt +0 -0
  69. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.0}/sql_dialect_master.egg-info/requires.txt +0 -0
  70. {sql_dialect_master-1.3.0 → sql_dialect_master-1.4.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.0
3
+ Version: 1.4.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.3.0
41
+ Current release: 1.4.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)
@@ -177,6 +177,19 @@ print(result.sql)
177
177
  # Output: SELECT * FROM orders WHERE created_at >= DATE_SUB(CURRENT_DATE, 7)
178
178
  ```
179
179
 
180
+ An optional request `schema` can ground both deterministic and LLM
181
+ generation. It contains `tables` with optional `columns`, plus explicit
182
+ `relationships`. Table and column references that the deterministic parser maps
183
+ to the schema are matched case-insensitively; ambiguous mapped references and
184
+ multi-table requests without a declared relationship fail closed instead of
185
+ falling back to hardcoded table or column names. Empty column metadata means
186
+ that the table's columns are unrestricted. Natural-language column names
187
+ outside the deterministic mapping may be ignored; use `column_hints` or the
188
+ LLM path for those.
189
+ The response field `schema_used` is true when a successful deterministic or LLM
190
+ generation was validated against the supplied schema. With no schema, the
191
+ existing deterministic behavior is unchanged.
192
+
180
193
  #### Optional LLM (OpenRouter)
181
194
 
182
195
  `POST /api/nl2sql` can optionally call an OpenRouter-compatible chat-completions
@@ -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.3.0
5
+ Current release: 1.4.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)
@@ -141,6 +141,19 @@ print(result.sql)
141
141
  # Output: SELECT * FROM orders WHERE created_at >= DATE_SUB(CURRENT_DATE, 7)
142
142
  ```
143
143
 
144
+ An optional request `schema` can ground both deterministic and LLM
145
+ generation. It contains `tables` with optional `columns`, plus explicit
146
+ `relationships`. Table and column references that the deterministic parser maps
147
+ to the schema are matched case-insensitively; ambiguous mapped references and
148
+ multi-table requests without a declared relationship fail closed instead of
149
+ falling back to hardcoded table or column names. Empty column metadata means
150
+ that the table's columns are unrestricted. Natural-language column names
151
+ outside the deterministic mapping may be ignored; use `column_hints` or the
152
+ LLM path for those.
153
+ The response field `schema_used` is true when a successful deterministic or LLM
154
+ generation was validated against the supplied schema. With no schema, the
155
+ existing deterministic behavior is unchanged.
156
+
144
157
  #### Optional LLM (OpenRouter)
145
158
 
146
159
  `POST /api/nl2sql` can optionally call an OpenRouter-compatible chat-completions
@@ -53,6 +53,7 @@ from backend.core.functions_lookup import FunctionEncyclopedia
53
53
  from backend.core.nl2sql import NL2SQLGenerator, NL2SQLResult
54
54
  from backend.core.nl2sql_components.evidence import EvidenceItem, GenerationEvidence
55
55
  from backend.core.nl2sql_llm import LLMError, LLMN2SQLClient, validate_llm_sql
56
+ from backend.core.nl2sql_schema import SchemaContext
56
57
  from backend.core.nl2sql_pipeline.orchestrator import SAFE_TABLE_HINT
57
58
  from backend.core.parser import SUPPORTED_DIALECTS, SQLParser
58
59
  from backend.core.semantic_diff import diff_sql_ast
@@ -468,12 +469,19 @@ class SchemaRelationship(BaseModel):
468
469
 
469
470
  class SchemaDefinition(BaseModel):
470
471
  tables: List[SchemaTable] = Field(default_factory=list, max_length=50)
471
- relationships: List[SchemaRelationship] = Field(default_factory=list)
472
+ relationships: List[SchemaRelationship] = Field(default_factory=list, max_length=200)
472
473
 
473
474
  @model_validator(mode="after")
474
475
  def validate_total_columns(self):
475
476
  if sum(len(table.columns) for table in self.tables) > 1000:
476
477
  raise ValueError("Schema must contain at most 1000 columns in total")
478
+ table_names = {table.name.casefold() for table in self.tables}
479
+ for relationship in self.relationships:
480
+ if (
481
+ relationship.from_table.casefold() not in table_names
482
+ or relationship.to_table.casefold() not in table_names
483
+ ):
484
+ raise ValueError("Schema relationship references an unknown table")
477
485
  return self
478
486
 
479
487
 
@@ -979,6 +987,14 @@ async def generate_sql(request: NL2SQLRequest):
979
987
  "dotted identifier"
980
988
  ),
981
989
  )
990
+ schema_payload = (
991
+ request.schema_definition.model_dump(exclude_none=True)
992
+ if request.schema_definition is not None else None
993
+ )
994
+ schema_context = (
995
+ SchemaContext.from_mapping(schema_payload)
996
+ if schema_payload is not None else None
997
+ )
982
998
  use_llm = (
983
999
  settings.nl2sql_llm_enabled
984
1000
  and bool(request.text.strip())
@@ -987,18 +1003,25 @@ async def generate_sql(request: NL2SQLRequest):
987
1003
  and nl2sql_generator._detect_operation(request.text.lower()) not in {"INSERT", "UPDATE", "DELETE"}
988
1004
  )
989
1005
  if not use_llm:
990
- result = await _run_cpu_bound(
991
- nl2sql_generator.generate,
992
- request.text,
993
- dialect,
994
- request.table_hint,
995
- request.column_hints,
996
- )
1006
+ if schema_context is None:
1007
+ result = await _run_cpu_bound(
1008
+ nl2sql_generator.generate,
1009
+ request.text,
1010
+ dialect,
1011
+ request.table_hint,
1012
+ request.column_hints,
1013
+ )
1014
+ else:
1015
+ result = await _run_cpu_bound(
1016
+ nl2sql_generator.generate,
1017
+ request.text,
1018
+ dialect,
1019
+ request.table_hint,
1020
+ request.column_hints,
1021
+ schema_context,
1022
+ )
997
1023
  else:
998
- schema = (
999
- request.schema_definition.model_dump(exclude_none=True)
1000
- if request.schema_definition is not None else None
1001
- )
1024
+ schema = schema_payload
1002
1025
  started = time.perf_counter()
1003
1026
  try:
1004
1027
  # Keep the legacy no-schema client call compatible with direct
@@ -1013,17 +1036,32 @@ async def generate_sql(request: NL2SQLRequest):
1013
1036
  record_nl2sql_llm_request(exc.reason, time.perf_counter() - started)
1014
1037
  if settings.nl2sql_llm_fallback:
1015
1038
  record_nl2sql_llm_fallback()
1016
- result = copy.copy(await _run_cpu_bound(
1017
- nl2sql_generator.generate,
1018
- request.text,
1019
- dialect,
1020
- request.table_hint,
1021
- request.column_hints,
1022
- ))
1039
+ if schema_context is None:
1040
+ deterministic_result = await _run_cpu_bound(
1041
+ nl2sql_generator.generate,
1042
+ request.text,
1043
+ dialect,
1044
+ request.table_hint,
1045
+ request.column_hints,
1046
+ )
1047
+ else:
1048
+ deterministic_result = await _run_cpu_bound(
1049
+ nl2sql_generator.generate,
1050
+ request.text,
1051
+ dialect,
1052
+ request.table_hint,
1053
+ request.column_hints,
1054
+ schema_context,
1055
+ )
1056
+ result = copy.copy(deterministic_result)
1023
1057
  result.provider = "deterministic"
1024
1058
  result.model = None
1025
1059
  result.fallback_reason = exc.reason
1026
- result.schema_used = False
1060
+ result.schema_used = bool(
1061
+ schema_context is not None
1062
+ and deterministic_result.success
1063
+ and deterministic_result.schema_used
1064
+ )
1027
1065
  else:
1028
1066
  result = NL2SQLResult(
1029
1067
  input_text=request.text,
@@ -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.3.0"
21
- API_VERSION = "1.3.0"
20
+ PROJECT_VERSION = "1.4.0"
21
+ API_VERSION = "1.4.0"
22
22
  # Frontend v2 architecture — independent of the product release version.
23
23
  FRONTEND_ARCHITECTURE_VERSION = 2
24
24
 
@@ -1,19 +1,18 @@
1
1
  """Opt-in OpenRouter-compatible generation with strict SQL output validation."""
2
2
  from __future__ import annotations
3
3
 
4
- from collections.abc import Iterable, Sequence
5
4
  from contextvars import ContextVar
6
5
  from dataclasses import dataclass
7
6
  import json
8
7
  import logging
9
8
  import re
10
- from typing import cast
11
9
 
12
10
  import httpx
13
11
  from sqlglot import exp
14
- from sqlglot.optimizer.scope import Scope, traverse_scope
12
+ from sqlglot.optimizer.scope import traverse_scope
15
13
 
16
14
  from .config import AppSettings, settings
15
+ from .nl2sql_schema import SchemaContext, validate_schema_references
17
16
  from .sql_statements import parse_single_sql
18
17
 
19
18
 
@@ -24,7 +23,6 @@ _ERROR_REASONS = frozenset({
24
23
  _VALIDATING_LLM_SQL = ContextVar("validating_llm_sql", default=False)
25
24
  _SQLGLOT_LOGGER = logging.getLogger("sqlglot")
26
25
  _SQL_FENCE = re.compile(r"```(?:sql)?[ \t]*\r?\n(.*?)\r?\n?```", re.IGNORECASE | re.DOTALL)
27
- _SCHEMA_IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_$]*")
28
26
 
29
27
 
30
28
  class LLMError(Exception):
@@ -48,118 +46,16 @@ class _LLMValidationLogFilter(logging.Filter):
48
46
  return not _VALIDATING_LLM_SQL.get()
49
47
 
50
48
 
51
- def _schema_reference_name(parts: Sequence[exp.Expr]) -> str:
52
- # A quoted literal name such as "public.users" must not be confused
53
- # with two separate schema/table identifiers, public.users.
54
- if not parts or any(
55
- not isinstance(part, exp.Identifier) or not _SCHEMA_IDENTIFIER.fullmatch(part.name)
56
- for part in parts
57
- ):
58
- raise LLMError("invalid_sql")
59
- return ".".join(part.name for part in parts).casefold()
60
-
61
-
62
49
  def _validate_schema_references(statement: exp.Expression, schema: dict) -> None:
63
- """Validate physical sources and resolve aliases within their SQL scopes."""
64
- table_columns = {
65
- table["name"].casefold(): {column["name"].casefold() for column in table.get("columns", [])}
66
- for table in schema.get("tables", [])
67
- }
68
- all_columns = set().union(*table_columns.values()) if table_columns else set()
69
- scopes = traverse_scope(statement)
70
- sources_by_scope: dict[int, dict[str, exp.Table | Scope]] = {}
71
- for scope in scopes:
72
- sources: dict[str, exp.Table | Scope] = {}
73
- for name, (_, source) in scope.selected_sources.items():
74
- alias = name.casefold()
75
- if alias in sources:
76
- raise LLMError("invalid_sql")
77
- sources[alias] = source
78
- if isinstance(source, exp.Table):
79
- table_name = _schema_reference_name(source.parts)
80
- if table_name not in table_columns:
81
- raise LLMError("invalid_sql")
82
- # A fully qualified column must match the complete source name,
83
- # and cannot resolve an unrelated schema through a shared basename.
84
- if not source.alias:
85
- sources[table_name] = source
86
- # SEMI/ANTI join sources are omitted by selected_sources, but still
87
- # reference physical tables and must obey the same schema constraints.
88
- for table in scope.tables:
89
- table_source = scope.sources.get(table.alias_or_name)
90
- if isinstance(table_source, exp.Table):
91
- table_name = _schema_reference_name(table_source.parts)
92
- if table_name not in table_columns:
93
- raise LLMError("invalid_sql")
94
- sources.setdefault(table.alias_or_name.casefold(), table_source)
95
- sources_by_scope[id(scope)] = sources
96
-
97
- def resolve_source(scope: Scope, qualifier: str) -> exp.Table | Scope:
98
- current: Scope | None = scope
99
- while current is not None:
100
- source = sources_by_scope.get(id(current), {}).get(qualifier)
101
- if source is not None:
102
- return source
103
- if not current.can_be_correlated:
104
- break
105
- current = current.parent
106
- raise LLMError("invalid_sql")
107
-
108
- output_cache: dict[int, set[str] | None] = {}
109
-
110
- def source_columns(
111
- source: exp.Table | Scope, visiting: frozenset[int] = frozenset(),
112
- ) -> set[str] | None:
113
- if isinstance(source, exp.Table):
114
- name = _schema_reference_name(source.parts)
115
- return table_columns[name] or None
116
- scope_id = id(source)
117
- if scope_id in output_cache:
118
- return output_cache[scope_id]
119
- if scope_id in visiting:
120
- # Recursive projections cannot be grounded without additional
121
- # type/lineage analysis. Fail closed instead of guessing.
122
- raise LLMError("invalid_sql")
123
- visiting = visiting | {scope_id}
124
- columns: set[str] | None
125
- if source.outer_columns:
126
- columns = {name.casefold() for name in source.outer_columns}
127
- elif source.union_scopes:
128
- columns = source_columns(source.union_scopes[0], visiting)
129
- else:
130
- columns = set()
131
- # Projection expansion expects a query scope; other scope nodes
132
- # retain the existing fail-closed behavior in validate_llm_sql.
133
- for projection in cast(exp.Query, source.expression).selects:
134
- if not projection.is_star:
135
- if projection.alias_or_name:
136
- columns.add(projection.alias_or_name.casefold())
137
- continue
138
- projected_sources: Iterable[exp.Table | Scope]
139
- if isinstance(projection, exp.Column):
140
- qualifier = _schema_reference_name(projection.parts[:-1])
141
- projected_sources = [resolve_source(source, qualifier)]
142
- else:
143
- projected_sources = sources_by_scope[scope_id].values()
144
- for projected in projected_sources:
145
- projected_columns = source_columns(projected, visiting)
146
- if projected_columns is None:
147
- output_cache[scope_id] = None
148
- return None
149
- columns.update(projected_columns)
150
- output_cache[scope_id] = columns
151
- return columns
152
-
153
- for scope in scopes:
154
- for column in scope.find_all(exp.Column):
155
- if column.table:
156
- qualifier = _schema_reference_name(column.parts[:-1])
157
- source = resolve_source(scope, qualifier)
158
- columns = source_columns(source)
159
- if not column.is_star and columns is not None and column.name.casefold() not in columns:
160
- raise LLMError("invalid_sql")
161
- elif all_columns and column.name.casefold() not in all_columns:
162
- raise LLMError("invalid_sql")
50
+ """Compatibility wrapper for the shared schema reference validator."""
51
+ try:
52
+ validate_schema_references(
53
+ statement,
54
+ SchemaContext.from_mapping(schema),
55
+ scope_traverser=traverse_scope,
56
+ )
57
+ except Exception:
58
+ raise LLMError("invalid_sql") from None
163
59
 
164
60
 
165
61
  def validate_llm_sql(