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
nl2sql/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ # nl2sql package
2
+
3
+ from .public_api import NL2SQL, QueryResult
4
+
5
+ # Also expose individual API modules for more granular access
6
+ from .api.query_api import QueryAPI
7
+ from .api.datasource_api import DatasourceAPI
8
+ from .api.llm_api import LLM_API
9
+ from .api.indexing_api import IndexingAPI
10
+ from .api.auth_api import AuthAPI
11
+ from .api.settings_api import SettingsAPI
12
+ from .api.result_api import ResultAPI
13
+ from .api.policy_api import PolicyAPI
14
+ from .api.benchmark_api import BenchmarkAPI
15
+
16
+ # Also expose core models and enums
17
+ from .common.errors import ErrorSeverity, ErrorCode, PipelineError
18
+ from .auth.models import UserContext
19
+ from .evaluation.types import BenchmarkConfig
20
+
21
+ __all__ = [
22
+ "NL2SQL",
23
+ "QueryResult",
24
+ "QueryAPI",
25
+ "DatasourceAPI",
26
+ "LLM_API",
27
+ "IndexingAPI",
28
+ "AuthAPI",
29
+ "SettingsAPI",
30
+ "ResultAPI",
31
+ "PolicyAPI",
32
+ "BenchmarkAPI",
33
+ "ErrorSeverity",
34
+ "ErrorCode",
35
+ "PipelineError",
36
+ "UserContext",
37
+ "BenchmarkConfig",
38
+ ]
File without changes
File without changes
@@ -0,0 +1,71 @@
1
+ from typing import Any, Dict
2
+
3
+ from nl2sql.adapters.sqlalchemy_base import (
4
+ CostEstimate,
5
+ DryRunResult,
6
+ QueryPlan,
7
+ BaseSQLAlchemyAdapter
8
+ )
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class DuckdbConnectionConfig(BaseModel):
14
+ """Strict configuration schema for DuckDB adapter."""
15
+ type: str
16
+ database: str = Field(..., description="Path to DuckDB database file, or ':memory:'")
17
+ options: Dict[str, Any] = Field(default_factory=dict)
18
+
19
+ model_config = {"extra": "ignore"}
20
+
21
+
22
+ class DuckdbAdapter(BaseSQLAlchemyAdapter):
23
+
24
+ def construct_uri(self, args: Dict[str, Any]) -> str:
25
+ """Constructs the DuckDB connection URI.
26
+
27
+ Args:
28
+ args: The raw connection arguments dictionary.
29
+
30
+ Returns:
31
+ str: The fully constructed SQLAlchemy connection URI.
32
+
33
+ Raises:
34
+ ValidationError: If the configuration is invalid.
35
+ """
36
+ config = DuckdbConnectionConfig(**args)
37
+ return f"duckdb:///{config.database}"
38
+
39
+ def dry_run(self, query: str) -> DryRunResult:
40
+ try:
41
+ self.execute_sql(f"EXPLAIN {query}")
42
+ return DryRunResult(is_valid=True, error_message=None)
43
+ except Exception as e:
44
+ return DryRunResult(is_valid=False, error_message=str(e))
45
+
46
+ def explain(self, query: str) -> QueryPlan:
47
+ try:
48
+ res = self.execute_sql(f"EXPLAIN {query}")
49
+ # EXPLAIN yields (explain_key, explain_value) rows; the plan is the value.
50
+ plan_text = "\n".join(str(row[-1]) for row in res.rows)
51
+ return QueryPlan(plan_text=plan_text)
52
+ except Exception:
53
+ return QueryPlan(plan_text="Could not retrieve plan")
54
+
55
+ def cost_estimate(self, query: str) -> CostEstimate:
56
+ # DuckDB's EXPLAIN prints an operator tree with no cost or cardinality
57
+ # figures, so there is nothing real to report here.
58
+ try:
59
+ self.execute_sql(f"EXPLAIN {query}")
60
+ return CostEstimate(estimated_cost=1.0, estimated_rows=10) # Stub
61
+ except Exception:
62
+ return CostEstimate(estimated_cost=-1.0, estimated_rows=0)
63
+
64
+ def get_dialect(self) -> str:
65
+ return "duckdb"
66
+
67
+ @property
68
+ def exclude_schemas(self) -> set[str]:
69
+ # DuckDB qualifies schema names with their catalog; only the attached
70
+ # database's own schemas carry user tables.
71
+ return {"system.main", "system.information_schema", "temp.main"}
File without changes
@@ -0,0 +1,122 @@
1
+ from typing import Any, List, Dict
2
+ from sqlalchemy import create_engine, text, inspect
3
+ from sqlalchemy.dialects import mssql
4
+ from nl2sql.adapters.sqlalchemy_base import (
5
+ CostEstimate,
6
+ DryRunResult,
7
+ QueryPlan,
8
+ )
9
+ from nl2sql.adapters.sqlalchemy_base import BaseSQLAlchemyAdapter
10
+
11
+ from pydantic import BaseModel, Field, SecretStr
12
+ from typing import Optional
13
+
14
+ class MssqlConnectionConfig(BaseModel):
15
+ """Strict configuration schema for MSSQL adapter."""
16
+ type: str = Field("mssql", description="Connection type")
17
+ host: str = Field(..., description="Server hostname")
18
+ user: Optional[str] = None
19
+ password: Optional[SecretStr] = None
20
+ port: int = 1433
21
+ database: str = Field(..., description="Database name")
22
+ driver: str = "ODBC Driver 17 for SQL Server"
23
+ trusted_connection: bool = False
24
+ options: Dict[str, Any] = Field(default_factory=dict)
25
+
26
+ model_config = {"extra": "ignore"}
27
+
28
+ class MssqlAdapter(BaseSQLAlchemyAdapter):
29
+
30
+ def construct_uri(self, args: Dict[str, Any]) -> str:
31
+ """Constructs the MSSQL connection URI.
32
+
33
+ Args:
34
+ args: The raw connection arguments dictionary.
35
+
36
+ Returns:
37
+ str: The fully constructed SQLAlchemy connection URI.
38
+
39
+ Raises:
40
+ ValidationError: If the configuration is invalid.
41
+ """
42
+ config = MssqlConnectionConfig(**args)
43
+
44
+ user = config.user or ""
45
+ password = config.password.get_secret_value() if config.password else ""
46
+ host = config.host
47
+ port = config.port
48
+ database = config.database
49
+ driver = config.driver
50
+
51
+ options = config.options.copy()
52
+ options["driver"] = driver
53
+
54
+ if config.trusted_connection:
55
+ options["Trusted_Connection"] = "yes"
56
+
57
+ creds = f"{user}:{password}@" if user or password else ""
58
+ netloc = f"{host}:{port}"
59
+
60
+ from urllib.parse import urlencode
61
+ query_str = "?" + urlencode(options)
62
+
63
+ return f"mssql+pyodbc://{creds}{netloc}/{database}{query_str}"
64
+
65
+ def dry_run(self, sql: str) -> DryRunResult:
66
+ try:
67
+ with self.engine.connect() as conn:
68
+ conn.execute(text("SET NOEXEC ON"))
69
+ try:
70
+ conn.execute(text(sql))
71
+ valid = True
72
+ msg = None
73
+ except Exception as e:
74
+ valid = False
75
+ msg = str(e)
76
+ finally:
77
+ conn.execute(text("SET NOEXEC OFF"))
78
+ return DryRunResult(is_valid=valid, error_message=msg)
79
+ except Exception as e:
80
+ return DryRunResult(is_valid=False, error_message=str(e))
81
+
82
+ def explain(self, sql: str) -> QueryPlan:
83
+ try:
84
+ with self.engine.connect() as conn:
85
+ conn.execute(text("SET SHOWPLAN_XML ON"))
86
+ except Exception as e:
87
+ return QueryPlan(plan_text=f"Error: {e}")
88
+
89
+ def get_dialect(self) -> str:
90
+ """MSSQL uses T-SQL dialect."""
91
+ return mssql.dialect.name
92
+
93
+ def cost_estimate(self, sql: str) -> CostEstimate:
94
+ import re
95
+ try:
96
+ with self.engine.connect() as conn:
97
+ conn.execute(text("SET SHOWPLAN_XML ON"))
98
+ try:
99
+ res = conn.execute(text(sql)).fetchone()
100
+ finally:
101
+ conn.execute(text("SET SHOWPLAN_XML OFF"))
102
+
103
+ if res and res[0]:
104
+ xml_str = res[0]
105
+ # Extract cost and rows using regex to avoid namespace complexity
106
+ # StatementSubTreeCost="0.00328" StatementEstRows="1"
107
+ cost_match = re.search(r'StatementSubTreeCost="([^"]+)"', xml_str)
108
+ rows_match = re.search(r'StatementEstRows="([^"]+)"', xml_str)
109
+
110
+ return CostEstimate(
111
+ estimated_cost=float(cost_match.group(1)) if cost_match else 0.0,
112
+ estimated_rows=float(rows_match.group(1)) if rows_match else 0
113
+ )
114
+ except Exception:
115
+ pass
116
+ return CostEstimate(estimated_cost=0.0, estimated_rows=0)
117
+
118
+
119
+ @property
120
+ def exclude_schemas(self) -> set[str]:
121
+ return {"sys", "INFORMATION_SCHEMA"}
122
+
File without changes
@@ -0,0 +1,123 @@
1
+ from typing import Any, List, Dict
2
+ from sqlalchemy import create_engine, text, inspect
3
+ from sqlalchemy.dialects import mysql
4
+ from nl2sql.adapters.sqlalchemy_base import (
5
+ CostEstimate,
6
+ DryRunResult,
7
+ QueryPlan,
8
+ BaseSQLAlchemyAdapter
9
+ )
10
+
11
+ from pydantic import BaseModel, Field, SecretStr
12
+ from typing import Optional
13
+
14
+ class MysqlConnectionConfig(BaseModel):
15
+ """Strict configuration schema for MySQL adapter."""
16
+ type: str
17
+ host: str = Field(..., description="MySQL server hostname")
18
+ user: str = Field(..., description="Username")
19
+ password: SecretStr = Field(..., description="Password")
20
+ port: int = 3306
21
+ database: str = Field(..., description="Database name")
22
+ options: Dict[str, Any] = Field(default_factory=dict)
23
+
24
+ model_config = {"extra": "ignore"}
25
+
26
+ class MysqlAdapter(BaseSQLAlchemyAdapter):
27
+
28
+ def construct_uri(self, args: Dict[str, Any]) -> str:
29
+ """Constructs the MySQL connection URI.
30
+
31
+ Args:
32
+ args: The raw connection arguments dictionary.
33
+
34
+ Returns:
35
+ str: The fully constructed SQLAlchemy connection URI.
36
+
37
+ Raises:
38
+ ValidationError: If the configuration is invalid.
39
+ """
40
+ config = MysqlConnectionConfig(**args)
41
+
42
+ user = config.user
43
+ password = config.password.get_secret_value()
44
+ host = config.host
45
+ port = config.port
46
+ database = config.database
47
+ options = config.options.copy()
48
+
49
+ creds = f"{user}:{password}@" if user or password else ""
50
+ netloc = f"{host}:{port}"
51
+
52
+ query_str = ""
53
+ if options:
54
+ from urllib.parse import urlencode
55
+ query_str = "?" + urlencode(options)
56
+
57
+ return f"mysql+pymysql://{creds}{netloc}/{database}{query_str}"
58
+
59
+ def connect(self) -> None:
60
+ """MySQL-specific connection with Native Server-Side Timeout."""
61
+ if not self.connection_string:
62
+ raise ValueError(f"Connection string is required for {self}")
63
+
64
+ connect_args = {}
65
+ if self.statement_timeout_ms:
66
+ # Native MySQL Timeout (server-side)
67
+ # SET MAX_EXECUTION_TIME={ms}
68
+ connect_args["init_command"] = f"SET MAX_EXECUTION_TIME={self.statement_timeout_ms}"
69
+
70
+ try:
71
+ self.engine = create_engine(
72
+ self.connection_string,
73
+ pool_pre_ping=True,
74
+ execution_options=self.execution_options,
75
+ connect_args=connect_args
76
+ )
77
+ except Exception as e:
78
+ import logging
79
+ logging.getLogger(__name__).error(f"Failed to connect to MySQL: {e}")
80
+ raise
81
+
82
+ def dry_run(self, sql: str) -> DryRunResult:
83
+ try:
84
+ with self.engine.connect() as conn:
85
+ trans = conn.begin()
86
+ conn.execute(text(sql))
87
+ trans.rollback()
88
+ return DryRunResult(is_valid=True)
89
+ except Exception as e:
90
+ return DryRunResult(is_valid=False, error_message=str(e))
91
+
92
+ def explain(self, sql: str) -> QueryPlan:
93
+ try:
94
+ with self.engine.connect() as conn:
95
+ res = conn.execute(text(f"EXPLAIN FORMAT=JSON {sql}")).scalar()
96
+ return QueryPlan(plan_text=str(res))
97
+ except Exception as e:
98
+ return QueryPlan(plan_text=f"Error: {e}")
99
+
100
+
101
+ def cost_estimate(self, sql: str) -> CostEstimate:
102
+ import json
103
+ try:
104
+ with self.engine.connect() as conn:
105
+ res = conn.execute(text(f"EXPLAIN FORMAT=JSON {sql}")).scalar()
106
+ if res:
107
+ data = json.loads(res)
108
+ cost_info = data.get('query_block', {}).get('cost_info', {})
109
+ return CostEstimate(
110
+ estimated_cost=float(cost_info.get('query_cost', 0.0)),
111
+ estimated_rows=0 # MySQL doesn't give a single total rows estimate easily
112
+ )
113
+ except Exception:
114
+ pass
115
+ return CostEstimate(estimated_cost=0.0, estimated_rows=0)
116
+
117
+ def get_dialect(self) -> str:
118
+ return mysql.dialect.name
119
+
120
+
121
+ @property
122
+ def exclude_schemas(self) -> set[str]:
123
+ return {"mysql", "INFORMATION_SCHEMA", "performance_schema", "sys"}
File without changes
@@ -0,0 +1,115 @@
1
+ from typing import Dict, Any
2
+ from sqlalchemy import create_engine, inspect, text
3
+ from sqlalchemy.dialects import postgresql
4
+ from nl2sql.adapters.sqlalchemy_base import (
5
+ DryRunResult,
6
+ QueryPlan,
7
+ CostEstimate,
8
+ BaseSQLAlchemyAdapter
9
+ )
10
+
11
+ from pydantic import BaseModel, Field, SecretStr
12
+ from typing import Optional
13
+
14
+ class PostgresConnectionConfig(BaseModel):
15
+ """Strict configuration schema for Postgres adapter."""
16
+ type: str = Field("postgresql", description="Connection type")
17
+ host: str = Field(..., description="Postgres server hostname")
18
+ user: str = Field(..., description="Username")
19
+ password: SecretStr = Field(..., description="Password")
20
+ port: int = 5432
21
+ database: str = Field(..., description="Database name")
22
+ options: Dict[str, Any] = Field(default_factory=dict)
23
+
24
+ model_config = {"extra": "ignore"}
25
+
26
+ class PostgresAdapter(BaseSQLAlchemyAdapter):
27
+
28
+ def construct_uri(self, args: Dict[str, Any]) -> str:
29
+ """Constructs the Postgres connection URI.
30
+
31
+ Args:
32
+ args: The raw connection arguments dictionary.
33
+
34
+ Returns:
35
+ str: The fully constructed SQLAlchemy connection URI.
36
+
37
+ Raises:
38
+ ValidationError: If the configuration is invalid.
39
+ """
40
+ config = PostgresConnectionConfig(**args)
41
+
42
+ user = config.user
43
+ password = config.password.get_secret_value()
44
+ host = config.host
45
+ port = config.port
46
+ database = config.database
47
+ options = config.options.copy()
48
+
49
+ creds = f"{user}:{password}@" if user or password else ""
50
+ netloc = f"{host}:{port}"
51
+
52
+ query_str = ""
53
+ if options:
54
+ from urllib.parse import urlencode
55
+ query_str = "?" + urlencode(options)
56
+
57
+ return f"postgresql://{creds}{netloc}/{database}{query_str}"
58
+
59
+ def connect(self) -> None:
60
+ """Postgres-specific connection with Native Server-Side Timeout."""
61
+ if not self.connection_string:
62
+ raise ValueError(f"Connection string is required for {self}")
63
+
64
+ connect_args = {}
65
+ if self.statement_timeout_ms:
66
+ connect_args["options"] = f"-c statement_timeout={self.statement_timeout_ms}"
67
+
68
+ try:
69
+ self.engine = create_engine(
70
+ self.connection_string,
71
+ pool_pre_ping=True,
72
+ execution_options=self.execution_options, # Pass standard options too
73
+ connect_args=connect_args
74
+ )
75
+ except Exception as e:
76
+ import logging
77
+ logging.getLogger(__name__).error(f"Failed to connect to Postgres: {e}")
78
+ raise
79
+
80
+ def dry_run(self, sql: str) -> DryRunResult:
81
+ try:
82
+ self.execute_sql(f"EXPLAIN {sql}")
83
+ return DryRunResult(is_valid=True)
84
+ except Exception as e:
85
+ return DryRunResult(is_valid=False, error_message=str(e))
86
+
87
+ def explain(self, sql: str) -> QueryPlan:
88
+ try:
89
+ res = self.execute_sql(f"EXPLAIN (FORMAT JSON) {sql}")
90
+ return QueryPlan(plan_text=str(res.rows))
91
+ except Exception: # Fallback
92
+ return QueryPlan(plan_text="Could not retrieve plan")
93
+
94
+ def cost_estimate(self, sql: str) -> CostEstimate:
95
+ try:
96
+ res = self.execute_sql(f"EXPLAIN (FORMAT JSON) {sql}")
97
+ if res.rows and res.rows[0]:
98
+ plan_data = res.rows[0][0] # The JSON object/list
99
+ if isinstance(plan_data, list) and len(plan_data) > 0:
100
+ root = plan_data[0].get('Plan', {})
101
+ return CostEstimate(
102
+ estimated_cost=float(root.get('Total Cost', 0.0)),
103
+ estimated_rows=int(root.get('Plan Rows', 0))
104
+ )
105
+ return CostEstimate(estimated_cost=0.0, estimated_rows=0)
106
+ except Exception:
107
+ return CostEstimate(estimated_cost=0.0, estimated_rows=0)
108
+
109
+ def get_dialect(self) -> str:
110
+ return postgresql.dialect.name
111
+
112
+
113
+ @property
114
+ def exclude_schemas(self) -> set[str]:
115
+ return {"pg_catalog", "information_schema"}
@@ -0,0 +1,17 @@
1
+ from .adapter import BaseSQLAlchemyAdapter
2
+ from .models import (
3
+ QueryResult,
4
+ CostEstimate,
5
+ DryRunResult,
6
+ QueryPlan,
7
+ AdapterError,
8
+ )
9
+
10
+ __all__ = [
11
+ "BaseSQLAlchemyAdapter",
12
+ "QueryResult",
13
+ "CostEstimate",
14
+ "DryRunResult",
15
+ "QueryPlan",
16
+ "AdapterError",
17
+ ]