nl2sql-engine 0.1.0__py3-none-any.whl

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 (192) hide show
  1. nl2sql/__init__.py +38 -0
  2. nl2sql/adapters/__init__.py +0 -0
  3. nl2sql/adapters/duckdb/__init__.py +0 -0
  4. nl2sql/adapters/duckdb/adapter.py +71 -0
  5. nl2sql/adapters/mssql/__init__.py +0 -0
  6. nl2sql/adapters/mssql/adapter.py +122 -0
  7. nl2sql/adapters/mysql/__init__.py +0 -0
  8. nl2sql/adapters/mysql/adapter.py +123 -0
  9. nl2sql/adapters/postgres/__init__.py +0 -0
  10. nl2sql/adapters/postgres/adapter.py +115 -0
  11. nl2sql/adapters/sqlalchemy_base/__init__.py +17 -0
  12. nl2sql/adapters/sqlalchemy_base/adapter.py +476 -0
  13. nl2sql/adapters/sqlalchemy_base/models.py +36 -0
  14. nl2sql/adapters/sqlite/__init__.py +0 -0
  15. nl2sql/adapters/sqlite/adapter.py +88 -0
  16. nl2sql/aggregation/__init__.py +3 -0
  17. nl2sql/aggregation/aggregator.py +98 -0
  18. nl2sql/aggregation/engines/__init__.py +3 -0
  19. nl2sql/aggregation/engines/polars_duckdb.py +125 -0
  20. nl2sql/api/__init__.py +0 -0
  21. nl2sql/api/auth_api.py +60 -0
  22. nl2sql/api/benchmark_api.py +114 -0
  23. nl2sql/api/datasource_api.py +132 -0
  24. nl2sql/api/indexing_api.py +59 -0
  25. nl2sql/api/llm_api.py +82 -0
  26. nl2sql/api/policy_api.py +135 -0
  27. nl2sql/api/query_api.py +138 -0
  28. nl2sql/api/result_api.py +24 -0
  29. nl2sql/api/settings_api.py +65 -0
  30. nl2sql/auth/__init__.py +8 -0
  31. nl2sql/auth/models.py +36 -0
  32. nl2sql/auth/rbac.py +25 -0
  33. nl2sql/cli/__init__.py +0 -0
  34. nl2sql/cli/checks.py +53 -0
  35. nl2sql/cli/commands/__init__.py +0 -0
  36. nl2sql/cli/commands/benchmark.py +34 -0
  37. nl2sql/cli/commands/doctor.py +49 -0
  38. nl2sql/cli/commands/indexing.py +126 -0
  39. nl2sql/cli/commands/info.py +25 -0
  40. nl2sql/cli/commands/install.py +27 -0
  41. nl2sql/cli/commands/policy.py +57 -0
  42. nl2sql/cli/commands/run.py +166 -0
  43. nl2sql/cli/commands/setup.py +415 -0
  44. nl2sql/cli/commands/visualize.py +34 -0
  45. nl2sql/cli/common/decorators.py +34 -0
  46. nl2sql/cli/config.py +24 -0
  47. nl2sql/cli/console.py +52 -0
  48. nl2sql/cli/demo/__init__.py +1 -0
  49. nl2sql/cli/demo/data.py +87 -0
  50. nl2sql/cli/demo/defaults.py +122 -0
  51. nl2sql/cli/demo/factory.py +289 -0
  52. nl2sql/cli/demo/manager.py +230 -0
  53. nl2sql/cli/demo/schemas.py +336 -0
  54. nl2sql/cli/demo/writers/__init__.py +0 -0
  55. nl2sql/cli/demo/writers/docker.py +182 -0
  56. nl2sql/cli/demo/writers/sqlite.py +88 -0
  57. nl2sql/cli/generators/datasources/__init__.py +3 -0
  58. nl2sql/cli/generators/datasources/generator.py +24 -0
  59. nl2sql/cli/generators/datasources/templates.py +7 -0
  60. nl2sql/cli/generators/env/__init__.py +3 -0
  61. nl2sql/cli/generators/env/generator.py +46 -0
  62. nl2sql/cli/generators/env/templates.py +25 -0
  63. nl2sql/cli/generators/llm/__init__.py +3 -0
  64. nl2sql/cli/generators/llm/generator.py +24 -0
  65. nl2sql/cli/generators/llm/templates.py +4 -0
  66. nl2sql/cli/generators/policies/__init__.py +3 -0
  67. nl2sql/cli/generators/policies/generator.py +20 -0
  68. nl2sql/cli/generators/policies/templates.py +2 -0
  69. nl2sql/cli/main.py +195 -0
  70. nl2sql/cli/reporting.py +878 -0
  71. nl2sql/cli/types.py +13 -0
  72. nl2sql/common/__init__.py +1 -0
  73. nl2sql/common/cancellation.py +25 -0
  74. nl2sql/common/context.py +5 -0
  75. nl2sql/common/errors.py +109 -0
  76. nl2sql/common/event_logger.py +88 -0
  77. nl2sql/common/exceptions.py +3 -0
  78. nl2sql/common/logger.py +119 -0
  79. nl2sql/common/metrics.py +50 -0
  80. nl2sql/common/resilience.py +59 -0
  81. nl2sql/common/settings.py +195 -0
  82. nl2sql/configs/__init__.py +6 -0
  83. nl2sql/configs/datasources.py +10 -0
  84. nl2sql/configs/llm.py +36 -0
  85. nl2sql/configs/manager.py +176 -0
  86. nl2sql/configs/policies.py +14 -0
  87. nl2sql/configs/sample_questions.py +11 -0
  88. nl2sql/configs/secrets.py +11 -0
  89. nl2sql/context.py +106 -0
  90. nl2sql/datasources/__init__.py +21 -0
  91. nl2sql/datasources/discovery.py +28 -0
  92. nl2sql/datasources/models.py +21 -0
  93. nl2sql/datasources/protocols.py +3 -0
  94. nl2sql/datasources/registry.py +172 -0
  95. nl2sql/evaluation/__init__.py +6 -0
  96. nl2sql/evaluation/benchmark_runner.py +320 -0
  97. nl2sql/evaluation/evaluator.py +134 -0
  98. nl2sql/evaluation/types.py +22 -0
  99. nl2sql/execution/__init__.py +4 -0
  100. nl2sql/execution/artifacts/__init__.py +3 -0
  101. nl2sql/execution/artifacts/parquet.py +41 -0
  102. nl2sql/execution/artifacts/store.py +165 -0
  103. nl2sql/execution/contracts.py +57 -0
  104. nl2sql/execution/execution_store.py +25 -0
  105. nl2sql/execution/executor/__init__.py +3 -0
  106. nl2sql/execution/executor/sql_executor.py +116 -0
  107. nl2sql/indexing/__init__.py +7 -0
  108. nl2sql/indexing/chunk_builder.py +227 -0
  109. nl2sql/indexing/embeddings.py +180 -0
  110. nl2sql/indexing/enrichment_service.py +316 -0
  111. nl2sql/indexing/models.py +209 -0
  112. nl2sql/indexing/orchestrator.py +90 -0
  113. nl2sql/indexing/vector_store.py +422 -0
  114. nl2sql/llm/__init__.py +8 -0
  115. nl2sql/llm/models.py +10 -0
  116. nl2sql/llm/registry.py +214 -0
  117. nl2sql/pipeline/__init__.py +1 -0
  118. nl2sql/pipeline/graph.py +73 -0
  119. nl2sql/pipeline/graph_utils.py +141 -0
  120. nl2sql/pipeline/nodes/__init__.py +25 -0
  121. nl2sql/pipeline/nodes/aggregator/__init__.py +4 -0
  122. nl2sql/pipeline/nodes/aggregator/node.py +55 -0
  123. nl2sql/pipeline/nodes/aggregator/prompts.py +20 -0
  124. nl2sql/pipeline/nodes/aggregator/schemas.py +28 -0
  125. nl2sql/pipeline/nodes/answer_synthesizer/__init__.py +4 -0
  126. nl2sql/pipeline/nodes/answer_synthesizer/node.py +98 -0
  127. nl2sql/pipeline/nodes/answer_synthesizer/prompts.py +19 -0
  128. nl2sql/pipeline/nodes/answer_synthesizer/schemas.py +24 -0
  129. nl2sql/pipeline/nodes/ast_planner/__init__.py +4 -0
  130. nl2sql/pipeline/nodes/ast_planner/node.py +104 -0
  131. nl2sql/pipeline/nodes/ast_planner/prompts.py +138 -0
  132. nl2sql/pipeline/nodes/ast_planner/schemas.py +236 -0
  133. nl2sql/pipeline/nodes/datasource_resolver/__init__.py +4 -0
  134. nl2sql/pipeline/nodes/datasource_resolver/node.py +253 -0
  135. nl2sql/pipeline/nodes/datasource_resolver/schemas.py +21 -0
  136. nl2sql/pipeline/nodes/decomposer/__init__.py +3 -0
  137. nl2sql/pipeline/nodes/decomposer/node.py +219 -0
  138. nl2sql/pipeline/nodes/decomposer/prompts.py +96 -0
  139. nl2sql/pipeline/nodes/decomposer/schemas.py +143 -0
  140. nl2sql/pipeline/nodes/executor/__init__.py +3 -0
  141. nl2sql/pipeline/nodes/executor/node.py +107 -0
  142. nl2sql/pipeline/nodes/generator/__init__.py +4 -0
  143. nl2sql/pipeline/nodes/generator/node.py +267 -0
  144. nl2sql/pipeline/nodes/generator/schemas.py +13 -0
  145. nl2sql/pipeline/nodes/global_planner/__init__.py +4 -0
  146. nl2sql/pipeline/nodes/global_planner/node.py +186 -0
  147. nl2sql/pipeline/nodes/global_planner/schemas.py +101 -0
  148. nl2sql/pipeline/nodes/refiner/__init__.py +4 -0
  149. nl2sql/pipeline/nodes/refiner/node.py +132 -0
  150. nl2sql/pipeline/nodes/refiner/prompts.py +28 -0
  151. nl2sql/pipeline/nodes/refiner/schemas.py +13 -0
  152. nl2sql/pipeline/nodes/schema_retriever/__init__.py +3 -0
  153. nl2sql/pipeline/nodes/schema_retriever/node.py +252 -0
  154. nl2sql/pipeline/nodes/schema_retriever/schema.py +27 -0
  155. nl2sql/pipeline/nodes/validator/__init__.py +7 -0
  156. nl2sql/pipeline/nodes/validator/node.py +839 -0
  157. nl2sql/pipeline/nodes/validator/schemas.py +12 -0
  158. nl2sql/pipeline/pipeline_runner.py +72 -0
  159. nl2sql/pipeline/routes.py +72 -0
  160. nl2sql/pipeline/runtime.py +153 -0
  161. nl2sql/pipeline/state.py +92 -0
  162. nl2sql/pipeline/subgraphs/__init__.py +5 -0
  163. nl2sql/pipeline/subgraphs/schemas.py +23 -0
  164. nl2sql/pipeline/subgraphs/sql_agent.py +167 -0
  165. nl2sql/public_api.py +199 -0
  166. nl2sql/schema/__init__.py +37 -0
  167. nl2sql/schema/in_memory_store.py +173 -0
  168. nl2sql/schema/protocol.py +88 -0
  169. nl2sql/schema/sqlite_store.py +233 -0
  170. nl2sql/schema/store.py +29 -0
  171. nl2sql/secrets/__init__.py +14 -0
  172. nl2sql/secrets/factory.py +85 -0
  173. nl2sql/secrets/interfaces.py +16 -0
  174. nl2sql/secrets/manager.py +139 -0
  175. nl2sql/secrets/models.py +56 -0
  176. nl2sql/secrets/providers/aws.py +30 -0
  177. nl2sql/secrets/providers/azure.py +49 -0
  178. nl2sql/secrets/providers/env.py +8 -0
  179. nl2sql/secrets/providers/hashi.py +46 -0
  180. nl2sql/services/__init__.py +0 -0
  181. nl2sql/services/callbacks/__init__.py +0 -0
  182. nl2sql/services/callbacks/monitor.py +84 -0
  183. nl2sql/services/callbacks/node_context.py +7 -0
  184. nl2sql/services/callbacks/node_handlers.py +187 -0
  185. nl2sql/services/callbacks/node_metrics.py +14 -0
  186. nl2sql/services/callbacks/presenter.py +12 -0
  187. nl2sql/services/callbacks/token_handler.py +56 -0
  188. nl2sql_engine-0.1.0.dist-info/METADATA +295 -0
  189. nl2sql_engine-0.1.0.dist-info/RECORD +192 -0
  190. nl2sql_engine-0.1.0.dist-info/WHEEL +5 -0
  191. nl2sql_engine-0.1.0.dist-info/entry_points.txt +9 -0
  192. nl2sql_engine-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,138 @@
1
+ """Prompts and examples for the SQL Planner node."""
2
+
3
+ PLANNER_EXAMPLES = """
4
+ Examples:
5
+
6
+ User Query: "Show me the names of users who placed orders in 2023"
7
+ Semantic Context:
8
+ {
9
+ "canonical_query": "List names of users with orders in 2023",
10
+ "keywords": ["users", "orders"]
11
+ }
12
+
13
+ Plan:
14
+ {
15
+ "reasoning": "Filter orders by year 2023. Join users. Select user name.",
16
+ "tables": [
17
+ {"name": "users", "alias": "t1", "ordinal": 0},
18
+ {"name": "orders", "alias": "t2", "ordinal": 1}
19
+ ],
20
+ "joins": [
21
+ {
22
+ "left_alias": "t1",
23
+ "right_alias": "t2",
24
+ "join_type": "inner",
25
+ "ordinal": 0,
26
+ "condition": {
27
+ "kind": "binary",
28
+ "op": "=",
29
+ "left": {"kind": "column", "alias": "t1", "column_name": "id"},
30
+ "right": {"kind": "column", "alias": "t2", "column_name": "user_id"}
31
+ }
32
+ }
33
+ ],
34
+ "where": {
35
+ "kind": "binary",
36
+ "op": "AND",
37
+ "left": {
38
+ "kind": "binary",
39
+ "op": ">=",
40
+ "left": {"kind": "column", "alias": "t2", "column_name": "order_date"},
41
+ "right": {"kind": "literal", "value": "2023-01-01"}
42
+ },
43
+ "right": {
44
+ "kind": "binary",
45
+ "op": "<=",
46
+ "left": {"kind": "column", "alias": "t2", "column_name": "order_date"},
47
+ "right": {"kind": "literal", "value": "2023-12-31"}
48
+ }
49
+ },
50
+ "select_items": [
51
+ {
52
+ "ordinal": 0,
53
+ "expr": {"kind": "column", "alias": "t1", "column_name": "name"},
54
+ "alias": "user_name"
55
+ }
56
+ ]
57
+ }
58
+
59
+ User Query: "Total revenue by region last quarter"
60
+ Expected Schema:
61
+ [
62
+ {"name": "region", "dtype": "string"},
63
+ {"name": "total_revenue", "dtype": "float"}
64
+ ]
65
+
66
+ Plan:
67
+ {
68
+ "reasoning": "Group by region and sum revenue.",
69
+ "tables": [
70
+ {"name": "orders", "alias": "t1", "ordinal": 0}
71
+ ],
72
+ "joins": [],
73
+ "where": {
74
+ "kind": "binary",
75
+ "op": "=",
76
+ "left": {"kind": "column", "alias": "t1", "column_name": "quarter"},
77
+ "right": {"kind": "literal", "value": "last_quarter"}
78
+ },
79
+ "select_items": [
80
+ {
81
+ "ordinal": 0,
82
+ "expr": {"kind": "column", "alias": "t1", "column_name": "region"},
83
+ "alias": "region"
84
+ },
85
+ {
86
+ "ordinal": 1,
87
+ "expr": {"kind": "func", "func_name": "SUM", "args": [{"kind": "column", "alias": "t1", "column_name": "revenue"}], "is_aggregate": true},
88
+ "alias": "total_revenue"
89
+ }
90
+ ],
91
+ "group_by": [
92
+ {
93
+ "ordinal": 0,
94
+ "expr": {"kind": "column", "alias": "t1", "column_name": "region"}
95
+ }
96
+ ]
97
+ }
98
+ """
99
+
100
+ PLANNER_PROMPT = (
101
+ "[ROLE]\n"
102
+ "You are a SQL Planner. Your job is to create a structured, executable SQL plan"
103
+ " in the form of a deterministic Abstract Syntax Tree (AST).\n\n"
104
+
105
+ "[INSTRUCTIONS]\n"
106
+ "1. Analyze [USER_QUERY] and [SEMANTIC_CONTEXT].\n"
107
+ "2. Select ONLY tables from [RELEVANT_TABLES]. Assign strict 'ordinal' positions 0..N.\n"
108
+ "3. When joining, use relationships listed in [RELEVANT_TABLES]. If no relationship exists, do not join.\n"
109
+ "4. Define joins using ONLY table aliases (left_alias/right_alias).\n"
110
+ "5. Build Expr trees using:\n"
111
+ " literal | column | func | binary | unary | case\n"
112
+ "6. Every list MUST contain `ordinal` fields in ascending order starting at 0.\n"
113
+ "7. Order lists to match ordinals (0..N) exactly.\n"
114
+ "8. For literal values on '=' or 'IN', choose values from column stats if available.\n"
115
+ "9. If no exact match is available, fall back to LIKE but keep the pattern derived from stats/synonyms.\n\n"
116
+
117
+ "[OUTPUT CONTRACT]\n"
118
+ "- If [EXPECTED_SCHEMA] is provided and non-empty:\n"
119
+ " - select_items length MUST equal expected_schema length.\n"
120
+ " - select_items aliases MUST match expected_schema names in the same order.\n"
121
+ "- All table/column references MUST come from [RELEVANT_TABLES].\n"
122
+ "- Joins MUST use relationships provided in [RELEVANT_TABLES].\n"
123
+ "- The [EXAMPLES] are illustrative; always follow [EXPECTED_SCHEMA] when provided.\n\n"
124
+
125
+ "[CONSTRAINTS]\n"
126
+ "- STRICTLY follow PlanModel schema.\n"
127
+ "- Do NOT hallucinate tables or columns.\n"
128
+ "- Do NOT output text, ONLY the JSON object.\n"
129
+ "- Use ISO 8601 dates.\n"
130
+ "- No extra keys beyond the schema.\n\n"
131
+
132
+ "[RELEVANT_TABLES]\n{relevant_tables}\n\n"
133
+ "[EXPECTED_SCHEMA]\n{expected_schema}\n\n"
134
+ "[SEMANTIC_CONTEXT]\n{semantic_context}\n\n"
135
+ "[EXAMPLES]\n{examples}\n\n"
136
+ "[FEEDBACK]\n{feedback}\n\n"
137
+ "[USER_QUERY]\n{user_query}"
138
+ )
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+ from typing import List, Optional, Literal, Union
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+
6
+ class CaseWhen(BaseModel):
7
+ """Represents a single WHEN ... THEN ... clause in a CASE expression.
8
+
9
+ Attributes:
10
+ condition (Expr): The condition expression to evaluate.
11
+ result (Expr): The result expression if the condition is true.
12
+ ordinal (int): The position of this clause in the CASE statement.
13
+ """
14
+ model_config = ConfigDict(extra="forbid")
15
+
16
+ condition: "Expr"
17
+ result: "Expr"
18
+ ordinal: int
19
+
20
+
21
+ class Expr(BaseModel):
22
+ """Unified AST for deterministic SQL expressions.
23
+
24
+ The 'kind' attribute determines which fields must or may be populated.
25
+ Strict validation rules are enforced in `model_post_init`.
26
+
27
+ Attributes:
28
+ kind (Literal): The type of expression (literal, column, func, binary, unary, case).
29
+ value (Optional[Union[str, int, float, bool]]): Value for literal expressions.
30
+ is_null (bool): Whether the literal is a NULL value.
31
+ alias (Optional[str]): Table alias for column expressions.
32
+ column_name (Optional[str]): Name of the column.
33
+ func_name (Optional[str]): Name of the function.
34
+ args (List[Expr]): Arguments for function expressions.
35
+ is_aggregate (bool): Whether the function is an aggregate function.
36
+ op (Optional[str]): Operator for binary or unary expressions.
37
+ left (Optional[Expr]): Left operand for binary expressions.
38
+ right (Optional[Expr]): Right operand for binary expressions.
39
+ expr (Optional[Expr]): Operand for unary expressions.
40
+ whens (List[CaseWhen]): List of WHEN clauses for CASE expressions.
41
+ else_expr (Optional[Expr]): The ELSE expression for CASE expressions.
42
+ """
43
+ model_config = ConfigDict(extra="forbid")
44
+
45
+ kind: Literal["literal", "column", "func", "binary", "unary", "case"]
46
+
47
+ # LITERAL
48
+ value: Optional[Union[str, int, float, bool]] = None
49
+ is_null: bool = False
50
+
51
+ # COLUMN
52
+ alias: Optional[str] = None
53
+ column_name: Optional[str] = None
54
+
55
+ # FUNCTION
56
+ func_name: Optional[str] = None
57
+ args: List["Expr"] = Field(default_factory=list)
58
+ is_aggregate: bool = False
59
+
60
+ # OPERATORS
61
+ op: Optional[
62
+ Literal[
63
+ "=", "!=", ">", "<", ">=", "<=",
64
+ "+", "-", "*", "/", "%",
65
+ "AND", "OR", "LIKE", "IN",
66
+ "NOT", "IS", "IS NOT"
67
+ ]
68
+ ] = None
69
+
70
+ left: Optional["Expr"] = None
71
+ right: Optional["Expr"] = None
72
+
73
+ # UNARY
74
+ expr: Optional["Expr"] = None
75
+
76
+ # CASE
77
+ whens: List[CaseWhen] = Field(default_factory=list)
78
+ else_expr: Optional["Expr"] = None
79
+
80
+ def model_post_init(self, *_):
81
+ """Validates the expression based on its `kind`."""
82
+ k = self.kind
83
+
84
+ if k == "literal":
85
+ if self.value is None and not self.is_null:
86
+ raise ValueError("Literal must have value or is_null=True")
87
+
88
+ if k == "column" and not self.column_name:
89
+ raise ValueError("column_name is required for column expression")
90
+
91
+ if k == "func" and not self.func_name:
92
+ raise ValueError("func_name is required for func expression")
93
+
94
+ if k == "binary":
95
+ if not (self.left and self.right):
96
+ raise ValueError("Binary expression requires left and right")
97
+ if not self.op:
98
+ raise ValueError("Binary expression requires operator")
99
+
100
+ if k == "unary":
101
+ if not self.expr:
102
+ raise ValueError("Unary expression requires expr")
103
+ if self.op not in ("NOT", "+", "-"):
104
+ raise ValueError("Unary op must be NOT, +, or -")
105
+
106
+ if k == "case" and not self.whens:
107
+ raise ValueError("CASE expression must have at least one WHEN")
108
+
109
+
110
+ class TableRef(BaseModel):
111
+ """Represents a reference to a database table.
112
+
113
+ Attributes:
114
+ name (str): The name of the table.
115
+ schema_name (Optional[str]): The schema of the table.
116
+ database (Optional[str]): The database name.
117
+ alias (str): The alias used for the table in the query.
118
+ ordinal (int): The strict ordinal position of the table.
119
+ """
120
+ model_config = ConfigDict(extra="forbid")
121
+
122
+ name: str
123
+ schema_name: Optional[str] = None
124
+ database: Optional[str] = None
125
+ alias: str
126
+ ordinal: int
127
+
128
+
129
+ class JoinSpec(BaseModel):
130
+ """Specification for a table join.
131
+
132
+ Attributes:
133
+ left_alias (str): Alias of the left table.
134
+ right_alias (str): Alias of the right table.
135
+ join_type (Literal): Type of join (inner, left, right, full).
136
+ condition (Expr): The join condition expression.
137
+ ordinal (int): The strict ordinal position of the join.
138
+ """
139
+ model_config = ConfigDict(extra="forbid")
140
+
141
+ left_alias: str
142
+ right_alias: str
143
+ join_type: Literal["inner", "left", "right", "full"] = "inner"
144
+ condition: Expr
145
+ ordinal: int
146
+
147
+
148
+ class SelectItem(BaseModel):
149
+ """Represents an item in the SELECT clause.
150
+
151
+ Attributes:
152
+ expr (Expr): The expression to select.
153
+ alias (Optional[str]): The alias for the selected expression.
154
+ ordinal (int): The strict ordinal position of the select item.
155
+ """
156
+ model_config = ConfigDict(extra="forbid")
157
+
158
+ expr: Expr
159
+ alias: Optional[str] = None
160
+ ordinal: int
161
+
162
+
163
+ class OrderItem(BaseModel):
164
+ """Represents an item in the ORDER BY clause.
165
+
166
+ Attributes:
167
+ expr (Expr): The expression to order by.
168
+ direction (Literal): The sort direction (asc, desc).
169
+ ordinal (int): The strict ordinal position of the order item.
170
+ """
171
+ model_config = ConfigDict(extra="forbid")
172
+
173
+ expr: Expr
174
+ direction: Literal["asc", "desc"] = "asc"
175
+ ordinal: int
176
+
177
+
178
+ class GroupByItem(BaseModel):
179
+ """Represents an item in the GROUP BY clause.
180
+
181
+ Attributes:
182
+ expr (Expr): The expression to group by.
183
+ ordinal (int): The strict ordinal position of the group by item.
184
+ """
185
+ model_config = ConfigDict(extra="forbid")
186
+
187
+ expr: Expr
188
+ ordinal: int
189
+
190
+
191
+ class PlanModel(BaseModel):
192
+ """Standardized representation of a SQL execution plan.
193
+
194
+ Attributes:
195
+ query_type (Literal): The type of query (default: READ).
196
+ distinct (bool): Whether to select distinct rows.
197
+ tables (List[TableRef]): List of tables involved in the query.
198
+ joins (List[JoinSpec]): List of join specifications.
199
+ select_items (List[SelectItem]): List of items to select.
200
+ where (Optional[Expr]): The WHERE clause expression.
201
+ group_by (List[GroupByItem]): List of GROUP BY items.
202
+ having (Optional[Expr]): The HAVING clause expression.
203
+ order_by (List[OrderItem]): List of ORDER BY items.
204
+ limit (Optional[int]): The LIMIT count.
205
+ offset (Optional[int]): The OFFSET count.
206
+ reasoning (Optional[str]): Explanatory text for the plan generation.
207
+ """
208
+ model_config = ConfigDict(extra="forbid")
209
+
210
+ query_type: Literal["READ"] = "READ"
211
+ distinct: bool = False
212
+
213
+ tables: List[TableRef] = Field(default_factory=list)
214
+ joins: List[JoinSpec] = Field(default_factory=list)
215
+
216
+ select_items: List[SelectItem] = Field(default_factory=list)
217
+
218
+ where: Optional[Expr] = None
219
+ group_by: List[GroupByItem] = Field(default_factory=list)
220
+ having: Optional[Expr] = None
221
+ order_by: List[OrderItem] = Field(default_factory=list)
222
+
223
+ limit: Optional[int] = None
224
+ offset: Optional[int] = None
225
+
226
+ reasoning: Optional[str] = None
227
+
228
+
229
+ class ASTPlannerResponse(BaseModel):
230
+ plan: Optional[PlanModel] = None
231
+
232
+
233
+ # Fix forward references
234
+ Expr.model_rebuild()
235
+ CaseWhen.model_rebuild()
236
+ PlanModel.model_rebuild()
@@ -0,0 +1,4 @@
1
+ from .node import DatasourceResolverNode
2
+ from .schemas import DatasourceResolverResponse, ResolvedDatasource
3
+
4
+ __all__ = ["DatasourceResolverNode", "DatasourceResolverResponse", "ResolvedDatasource"]
@@ -0,0 +1,253 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Any, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from nl2sql.pipeline.state import GraphState
7
+
8
+ from nl2sql.common.errors import PipelineError, ErrorSeverity, ErrorCode
9
+ from langchain_core.documents import Document
10
+ from nl2sql.auth import UserContext
11
+ from nl2sql.common.logger import get_logger
12
+ from nl2sql.context import NL2SQLContext
13
+ from nl2sql.common.settings import settings
14
+ from .schemas import DatasourceResolverResponse, ResolvedDatasource
15
+
16
+
17
+ logger = get_logger("datasource_resolver")
18
+
19
+
20
+ class DatasourceResolverNode:
21
+ """Resolves candidate datasources using vector search over datasource chunks."""
22
+
23
+ def __init__(self, ctx: NL2SQLContext):
24
+ self.node_name = self.__class__.__name__.lower().replace("node", "")
25
+ self.vector_store = ctx.vector_store
26
+ self.rbac = ctx.rbac
27
+ self.ds_registry = ctx.ds_registry
28
+ self.schema_store = ctx.schema_store
29
+
30
+ def _get_unsupported_datasources(self, datasource_ids: list[str]) -> list[str]:
31
+ available_ds_ids = self.ds_registry.list_ids()
32
+ unsupported = [ds_id for ds_id in datasource_ids if ds_id not in available_ds_ids]
33
+ return sorted(unsupported)
34
+
35
+ def _error_response(
36
+ self,
37
+ resolved_datasources: list[ResolvedDatasource],
38
+ allowed_ids: list[str],
39
+ unsupported_ids: list[str],
40
+ message: str,
41
+ severity: ErrorSeverity,
42
+ error_code: ErrorCode,
43
+ ) -> Dict[str, Any]:
44
+ return {
45
+ "datasource_resolver_response": DatasourceResolverResponse(
46
+ resolved_datasources=resolved_datasources,
47
+ allowed_datasource_ids=allowed_ids,
48
+ unsupported_datasource_ids=unsupported_ids,
49
+ ),
50
+ "errors": [
51
+ PipelineError(
52
+ node=self.node_name,
53
+ message=message,
54
+ severity=severity,
55
+ error_code=error_code,
56
+ )
57
+ ],
58
+ }
59
+
60
+ def _get_candidate_datasources(
61
+ self,
62
+ candidate_docs: list[Document],
63
+ ) -> Dict[str, ResolvedDatasource]:
64
+ candidate_datasources: Dict[str, ResolvedDatasource] = {}
65
+ schema_versions: Dict[str, str | None] = {}
66
+ for doc in candidate_docs:
67
+ ds_id = doc.metadata.get("datasource_id")
68
+ if not ds_id or ds_id in candidate_datasources:
69
+ continue
70
+ if ds_id not in schema_versions:
71
+ schema_versions[ds_id] = self.schema_store.get_latest_version(ds_id)
72
+ chunk_schema_version = doc.metadata.get("schema_version")
73
+ schema_version = schema_versions.get(ds_id)
74
+ candidate_datasources[ds_id] = ResolvedDatasource(
75
+ datasource_id=ds_id,
76
+ metadata=dict(doc.metadata),
77
+ schema_version=schema_version,
78
+ chunk_schema_version=chunk_schema_version,
79
+ schema_version_mismatch=bool(
80
+ chunk_schema_version
81
+ and schema_version
82
+ and chunk_schema_version != schema_version
83
+ ),
84
+ )
85
+ return candidate_datasources
86
+
87
+ def _get_allowed_datasource_ids(
88
+ self,
89
+ user_context: UserContext,
90
+ candidate_ids: list[str],
91
+ ) -> list[str]:
92
+ allowed_ids = self.rbac.get_allowed_datasources(user_context)
93
+ if not allowed_ids:
94
+ return []
95
+ if "*" in allowed_ids:
96
+ return candidate_ids
97
+ return [ds_id for ds_id in candidate_ids if ds_id in allowed_ids]
98
+
99
+
100
+ def _apply_schema_version_mismatch_policy(self, resolved_datasources: list[ResolvedDatasource], allowed_ids: list[str], unsupported_ids: list[str]):
101
+ mismatches = [
102
+ ds.datasource_id
103
+ for ds in resolved_datasources
104
+ if ds.schema_version_mismatch
105
+ ]
106
+ if mismatches:
107
+ policy = (settings.schema_version_mismatch_policy or "warn").lower()
108
+ message = (
109
+ "Schema version mismatch for datasources: "
110
+ + ", ".join(sorted(mismatches))
111
+ )
112
+ if policy == "fail":
113
+ return self._error_response(
114
+ resolved_datasources=[],
115
+ allowed_ids=[],
116
+ unsupported_ids=[],
117
+ message=message,
118
+ severity=ErrorSeverity.ERROR,
119
+ error_code=ErrorCode.INVALID_STATE,
120
+ )
121
+ elif policy == "warn":
122
+ return {
123
+ "datasource_resolver_response": DatasourceResolverResponse(
124
+ resolved_datasources=resolved_datasources,
125
+ allowed_datasource_ids=allowed_ids,
126
+ unsupported_datasource_ids=unsupported_ids,
127
+ ),
128
+ "reasoning": [
129
+ {
130
+ "node": self.node_name,
131
+ "content": message,
132
+ "type": "warning",
133
+ }
134
+ ],
135
+ "warnings": [
136
+ {
137
+ "node": self.node_name,
138
+ "content": message,
139
+ }
140
+ ],
141
+ }
142
+
143
+
144
+ def __call__(self, state: GraphState) -> Dict[str, Any]:
145
+ try:
146
+ if state.datasource_id:
147
+ unsupported_ids = self._get_unsupported_datasources([state.datasource_id])
148
+ if unsupported_ids:
149
+ return self._error_response(
150
+ resolved_datasources=[],
151
+ allowed_ids=[],
152
+ unsupported_ids=unsupported_ids,
153
+ message=f"Datasource not found: {state.datasource_id}.",
154
+ severity=ErrorSeverity.ERROR,
155
+ error_code=ErrorCode.INVALID_STATE,
156
+ )
157
+
158
+ resolved = ResolvedDatasource(
159
+ datasource_id=state.datasource_id,
160
+ metadata={},
161
+ schema_version=self.schema_store.get_latest_version(state.datasource_id),
162
+ )
163
+ allowed_ids = self._get_allowed_datasource_ids(
164
+ state.user_context,
165
+ [state.datasource_id],
166
+ )
167
+ if not allowed_ids:
168
+ return self._error_response(
169
+ resolved_datasources=[resolved],
170
+ allowed_ids=[],
171
+ unsupported_ids=[],
172
+ message="Datasource not allowed.",
173
+ severity=ErrorSeverity.CRITICAL,
174
+ error_code=ErrorCode.SECURITY_VIOLATION,
175
+ )
176
+
177
+ return {
178
+ "datasource_resolver_response": DatasourceResolverResponse(
179
+ resolved_datasources=[resolved],
180
+ allowed_datasource_ids=allowed_ids,
181
+ unsupported_datasource_ids=[],
182
+ ),
183
+ "reasoning": [
184
+ {
185
+ "node": self.node_name,
186
+ "content": "Using explicit datasource override.",
187
+ }
188
+ ],
189
+ }
190
+
191
+ if not self.vector_store:
192
+ return {
193
+ "datasource_resolver_response": DatasourceResolverResponse(),
194
+ "reasoning": [{"node": self.node_name, "content": "Vector store unavailable."}],
195
+ }
196
+
197
+ query = state.user_query
198
+ candidate_docs = self.vector_store.retrieve_datasource_candidates(query, k=5)
199
+ candidate_datasources = self._get_candidate_datasources(candidate_docs)
200
+ candidate_ids = list(candidate_datasources.keys())
201
+ if not candidate_ids:
202
+ return self._error_response(
203
+ resolved_datasources=[],
204
+ allowed_ids=[],
205
+ unsupported_ids=[],
206
+ message="No datasource candidates resolved.",
207
+ severity=ErrorSeverity.ERROR,
208
+ error_code=ErrorCode.SCHEMA_RETRIEVAL_FAILED,
209
+ )
210
+ unsupported_ids = self._get_unsupported_datasources(candidate_ids)
211
+ allowed_ids = self._get_allowed_datasource_ids(state.user_context, candidate_ids)
212
+ if not allowed_ids:
213
+ return self._error_response(
214
+ resolved_datasources=list(candidate_datasources.values()),
215
+ allowed_ids=[],
216
+ unsupported_ids=unsupported_ids,
217
+ message="No allowed datasources.",
218
+ severity=ErrorSeverity.CRITICAL,
219
+ error_code=ErrorCode.SECURITY_VIOLATION,
220
+ )
221
+
222
+
223
+ schema_version_mismatch_response = self._apply_schema_version_mismatch_policy(
224
+ list(candidate_datasources.values()),
225
+ allowed_ids,
226
+ unsupported_ids,
227
+ )
228
+ if schema_version_mismatch_response:
229
+ return schema_version_mismatch_response
230
+
231
+
232
+ result = DatasourceResolverResponse(
233
+ resolved_datasources=list(candidate_datasources.values()),
234
+ allowed_datasource_ids=allowed_ids,
235
+ unsupported_datasource_ids=unsupported_ids,
236
+ )
237
+ return {
238
+ "datasource_resolver_response": result,
239
+ "reasoning": [{"node": self.node_name, "content": "Ranked by vector similarity."}],
240
+ }
241
+ except Exception as exc:
242
+ logger.error(f"Datasource resolver failed: {exc}")
243
+ return {
244
+ "datasource_resolver_response": DatasourceResolverResponse(),
245
+ "errors": [
246
+ PipelineError(
247
+ node=self.node_name,
248
+ message=f"Datasource resolution failed: {exc}",
249
+ severity=ErrorSeverity.ERROR,
250
+ error_code=ErrorCode.SCHEMA_RETRIEVAL_FAILED,
251
+ )
252
+ ],
253
+ }
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ResolvedDatasource(BaseModel):
9
+ datasource_id: str
10
+ metadata: Dict[str, Any] = Field(default_factory=dict)
11
+ schema_version: Optional[str] = None
12
+ chunk_schema_version: Optional[str] = None
13
+ schema_version_mismatch: bool = False
14
+
15
+
16
+ class DatasourceResolverResponse(BaseModel):
17
+ resolved_datasources: List[ResolvedDatasource] = Field(default_factory=list)
18
+ allowed_datasource_ids: List[str] = Field(default_factory=list)
19
+ unsupported_datasource_ids: List[str] = Field(default_factory=list)
20
+
21
+
@@ -0,0 +1,3 @@
1
+ from .node import DecomposerNode
2
+
3
+ __all__ = ["DecomposerNode"]