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.
- nl2sql/__init__.py +38 -0
- nl2sql/adapters/__init__.py +0 -0
- nl2sql/adapters/duckdb/__init__.py +0 -0
- nl2sql/adapters/duckdb/adapter.py +71 -0
- nl2sql/adapters/mssql/__init__.py +0 -0
- nl2sql/adapters/mssql/adapter.py +122 -0
- nl2sql/adapters/mysql/__init__.py +0 -0
- nl2sql/adapters/mysql/adapter.py +123 -0
- nl2sql/adapters/postgres/__init__.py +0 -0
- nl2sql/adapters/postgres/adapter.py +115 -0
- nl2sql/adapters/sqlalchemy_base/__init__.py +17 -0
- nl2sql/adapters/sqlalchemy_base/adapter.py +476 -0
- nl2sql/adapters/sqlalchemy_base/models.py +36 -0
- nl2sql/adapters/sqlite/__init__.py +0 -0
- nl2sql/adapters/sqlite/adapter.py +88 -0
- nl2sql/aggregation/__init__.py +3 -0
- nl2sql/aggregation/aggregator.py +98 -0
- nl2sql/aggregation/engines/__init__.py +3 -0
- nl2sql/aggregation/engines/polars_duckdb.py +125 -0
- nl2sql/api/__init__.py +0 -0
- nl2sql/api/auth_api.py +60 -0
- nl2sql/api/benchmark_api.py +114 -0
- nl2sql/api/datasource_api.py +132 -0
- nl2sql/api/indexing_api.py +59 -0
- nl2sql/api/llm_api.py +82 -0
- nl2sql/api/policy_api.py +135 -0
- nl2sql/api/query_api.py +138 -0
- nl2sql/api/result_api.py +24 -0
- nl2sql/api/settings_api.py +65 -0
- nl2sql/auth/__init__.py +8 -0
- nl2sql/auth/models.py +36 -0
- nl2sql/auth/rbac.py +25 -0
- nl2sql/cli/__init__.py +0 -0
- nl2sql/cli/checks.py +53 -0
- nl2sql/cli/commands/__init__.py +0 -0
- nl2sql/cli/commands/benchmark.py +34 -0
- nl2sql/cli/commands/doctor.py +49 -0
- nl2sql/cli/commands/indexing.py +126 -0
- nl2sql/cli/commands/info.py +25 -0
- nl2sql/cli/commands/install.py +27 -0
- nl2sql/cli/commands/policy.py +57 -0
- nl2sql/cli/commands/run.py +166 -0
- nl2sql/cli/commands/setup.py +415 -0
- nl2sql/cli/commands/visualize.py +34 -0
- nl2sql/cli/common/decorators.py +34 -0
- nl2sql/cli/config.py +24 -0
- nl2sql/cli/console.py +52 -0
- nl2sql/cli/demo/__init__.py +1 -0
- nl2sql/cli/demo/data.py +87 -0
- nl2sql/cli/demo/defaults.py +122 -0
- nl2sql/cli/demo/factory.py +289 -0
- nl2sql/cli/demo/manager.py +230 -0
- nl2sql/cli/demo/schemas.py +336 -0
- nl2sql/cli/demo/writers/__init__.py +0 -0
- nl2sql/cli/demo/writers/docker.py +182 -0
- nl2sql/cli/demo/writers/sqlite.py +88 -0
- nl2sql/cli/generators/datasources/__init__.py +3 -0
- nl2sql/cli/generators/datasources/generator.py +24 -0
- nl2sql/cli/generators/datasources/templates.py +7 -0
- nl2sql/cli/generators/env/__init__.py +3 -0
- nl2sql/cli/generators/env/generator.py +46 -0
- nl2sql/cli/generators/env/templates.py +25 -0
- nl2sql/cli/generators/llm/__init__.py +3 -0
- nl2sql/cli/generators/llm/generator.py +24 -0
- nl2sql/cli/generators/llm/templates.py +4 -0
- nl2sql/cli/generators/policies/__init__.py +3 -0
- nl2sql/cli/generators/policies/generator.py +20 -0
- nl2sql/cli/generators/policies/templates.py +2 -0
- nl2sql/cli/main.py +195 -0
- nl2sql/cli/reporting.py +878 -0
- nl2sql/cli/types.py +13 -0
- nl2sql/common/__init__.py +1 -0
- nl2sql/common/cancellation.py +25 -0
- nl2sql/common/context.py +5 -0
- nl2sql/common/errors.py +109 -0
- nl2sql/common/event_logger.py +88 -0
- nl2sql/common/exceptions.py +3 -0
- nl2sql/common/logger.py +119 -0
- nl2sql/common/metrics.py +50 -0
- nl2sql/common/resilience.py +59 -0
- nl2sql/common/settings.py +195 -0
- nl2sql/configs/__init__.py +6 -0
- nl2sql/configs/datasources.py +10 -0
- nl2sql/configs/llm.py +36 -0
- nl2sql/configs/manager.py +176 -0
- nl2sql/configs/policies.py +14 -0
- nl2sql/configs/sample_questions.py +11 -0
- nl2sql/configs/secrets.py +11 -0
- nl2sql/context.py +106 -0
- nl2sql/datasources/__init__.py +21 -0
- nl2sql/datasources/discovery.py +28 -0
- nl2sql/datasources/models.py +21 -0
- nl2sql/datasources/protocols.py +3 -0
- nl2sql/datasources/registry.py +172 -0
- nl2sql/evaluation/__init__.py +6 -0
- nl2sql/evaluation/benchmark_runner.py +320 -0
- nl2sql/evaluation/evaluator.py +134 -0
- nl2sql/evaluation/types.py +22 -0
- nl2sql/execution/__init__.py +4 -0
- nl2sql/execution/artifacts/__init__.py +3 -0
- nl2sql/execution/artifacts/parquet.py +41 -0
- nl2sql/execution/artifacts/store.py +165 -0
- nl2sql/execution/contracts.py +57 -0
- nl2sql/execution/execution_store.py +25 -0
- nl2sql/execution/executor/__init__.py +3 -0
- nl2sql/execution/executor/sql_executor.py +116 -0
- nl2sql/indexing/__init__.py +7 -0
- nl2sql/indexing/chunk_builder.py +227 -0
- nl2sql/indexing/embeddings.py +180 -0
- nl2sql/indexing/enrichment_service.py +316 -0
- nl2sql/indexing/models.py +209 -0
- nl2sql/indexing/orchestrator.py +90 -0
- nl2sql/indexing/vector_store.py +422 -0
- nl2sql/llm/__init__.py +8 -0
- nl2sql/llm/models.py +10 -0
- nl2sql/llm/registry.py +214 -0
- nl2sql/pipeline/__init__.py +1 -0
- nl2sql/pipeline/graph.py +73 -0
- nl2sql/pipeline/graph_utils.py +141 -0
- nl2sql/pipeline/nodes/__init__.py +25 -0
- nl2sql/pipeline/nodes/aggregator/__init__.py +4 -0
- nl2sql/pipeline/nodes/aggregator/node.py +55 -0
- nl2sql/pipeline/nodes/aggregator/prompts.py +20 -0
- nl2sql/pipeline/nodes/aggregator/schemas.py +28 -0
- nl2sql/pipeline/nodes/answer_synthesizer/__init__.py +4 -0
- nl2sql/pipeline/nodes/answer_synthesizer/node.py +98 -0
- nl2sql/pipeline/nodes/answer_synthesizer/prompts.py +19 -0
- nl2sql/pipeline/nodes/answer_synthesizer/schemas.py +24 -0
- nl2sql/pipeline/nodes/ast_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/ast_planner/node.py +104 -0
- nl2sql/pipeline/nodes/ast_planner/prompts.py +138 -0
- nl2sql/pipeline/nodes/ast_planner/schemas.py +236 -0
- nl2sql/pipeline/nodes/datasource_resolver/__init__.py +4 -0
- nl2sql/pipeline/nodes/datasource_resolver/node.py +253 -0
- nl2sql/pipeline/nodes/datasource_resolver/schemas.py +21 -0
- nl2sql/pipeline/nodes/decomposer/__init__.py +3 -0
- nl2sql/pipeline/nodes/decomposer/node.py +219 -0
- nl2sql/pipeline/nodes/decomposer/prompts.py +96 -0
- nl2sql/pipeline/nodes/decomposer/schemas.py +143 -0
- nl2sql/pipeline/nodes/executor/__init__.py +3 -0
- nl2sql/pipeline/nodes/executor/node.py +107 -0
- nl2sql/pipeline/nodes/generator/__init__.py +4 -0
- nl2sql/pipeline/nodes/generator/node.py +267 -0
- nl2sql/pipeline/nodes/generator/schemas.py +13 -0
- nl2sql/pipeline/nodes/global_planner/__init__.py +4 -0
- nl2sql/pipeline/nodes/global_planner/node.py +186 -0
- nl2sql/pipeline/nodes/global_planner/schemas.py +101 -0
- nl2sql/pipeline/nodes/refiner/__init__.py +4 -0
- nl2sql/pipeline/nodes/refiner/node.py +132 -0
- nl2sql/pipeline/nodes/refiner/prompts.py +28 -0
- nl2sql/pipeline/nodes/refiner/schemas.py +13 -0
- nl2sql/pipeline/nodes/schema_retriever/__init__.py +3 -0
- nl2sql/pipeline/nodes/schema_retriever/node.py +252 -0
- nl2sql/pipeline/nodes/schema_retriever/schema.py +27 -0
- nl2sql/pipeline/nodes/validator/__init__.py +7 -0
- nl2sql/pipeline/nodes/validator/node.py +839 -0
- nl2sql/pipeline/nodes/validator/schemas.py +12 -0
- nl2sql/pipeline/pipeline_runner.py +72 -0
- nl2sql/pipeline/routes.py +72 -0
- nl2sql/pipeline/runtime.py +153 -0
- nl2sql/pipeline/state.py +92 -0
- nl2sql/pipeline/subgraphs/__init__.py +5 -0
- nl2sql/pipeline/subgraphs/schemas.py +23 -0
- nl2sql/pipeline/subgraphs/sql_agent.py +167 -0
- nl2sql/public_api.py +199 -0
- nl2sql/schema/__init__.py +37 -0
- nl2sql/schema/in_memory_store.py +173 -0
- nl2sql/schema/protocol.py +88 -0
- nl2sql/schema/sqlite_store.py +233 -0
- nl2sql/schema/store.py +29 -0
- nl2sql/secrets/__init__.py +14 -0
- nl2sql/secrets/factory.py +85 -0
- nl2sql/secrets/interfaces.py +16 -0
- nl2sql/secrets/manager.py +139 -0
- nl2sql/secrets/models.py +56 -0
- nl2sql/secrets/providers/aws.py +30 -0
- nl2sql/secrets/providers/azure.py +49 -0
- nl2sql/secrets/providers/env.py +8 -0
- nl2sql/secrets/providers/hashi.py +46 -0
- nl2sql/services/__init__.py +0 -0
- nl2sql/services/callbacks/__init__.py +0 -0
- nl2sql/services/callbacks/monitor.py +84 -0
- nl2sql/services/callbacks/node_context.py +7 -0
- nl2sql/services/callbacks/node_handlers.py +187 -0
- nl2sql/services/callbacks/node_metrics.py +14 -0
- nl2sql/services/callbacks/presenter.py +12 -0
- nl2sql/services/callbacks/token_handler.py +56 -0
- nl2sql_engine-0.1.0.dist-info/METADATA +295 -0
- nl2sql_engine-0.1.0.dist-info/RECORD +192 -0
- nl2sql_engine-0.1.0.dist-info/WHEEL +5 -0
- nl2sql_engine-0.1.0.dist-info/entry_points.txt +9 -0
- 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,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
|
+
|