tdsql-mcp 1.0.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.
- tdsql_mcp/__init__.py +0 -0
- tdsql_mcp/server.py +450 -0
- tdsql_mcp/syntax/aggregate-functions.md +94 -0
- tdsql_mcp/syntax/ai-text-analytics.md +630 -0
- tdsql_mcp/syntax/association-analysis.md +108 -0
- tdsql_mcp/syntax/authorization-objects.md +173 -0
- tdsql_mcp/syntax/byom-model-loading.md +337 -0
- tdsql_mcp/syntax/byom-scoring.md +581 -0
- tdsql_mcp/syntax/catalog-views.md +125 -0
- tdsql_mcp/syntax/conditional.md +85 -0
- tdsql_mcp/syntax/data-cleaning.md +514 -0
- tdsql_mcp/syntax/data-exploration.md +317 -0
- tdsql_mcp/syntax/data-prep.md +928 -0
- tdsql_mcp/syntax/data-types-casting.md +237 -0
- tdsql_mcp/syntax/date-time.md +107 -0
- tdsql_mcp/syntax/embeddings.md +279 -0
- tdsql_mcp/syntax/fit-transform-pattern.md +94 -0
- tdsql_mcp/syntax/geospatial.md +731 -0
- tdsql_mcp/syntax/guidelines.md +290 -0
- tdsql_mcp/syntax/hypothesis-testing.md +240 -0
- tdsql_mcp/syntax/index.md +102 -0
- tdsql_mcp/syntax/llm-providers.md +230 -0
- tdsql_mcp/syntax/ml-functions.md +757 -0
- tdsql_mcp/syntax/ml-patterns.md +296 -0
- tdsql_mcp/syntax/model-evaluation.md +252 -0
- tdsql_mcp/syntax/numeric-functions.md +94 -0
- tdsql_mcp/syntax/path-analysis.md +358 -0
- tdsql_mcp/syntax/query-tuning.md +104 -0
- tdsql_mcp/syntax/sql-basics.md +85 -0
- tdsql_mcp/syntax/string-functions.md +74 -0
- tdsql_mcp/syntax/text-analytics.md +524 -0
- tdsql_mcp/syntax/uaf-concepts.md +309 -0
- tdsql_mcp/syntax/uaf-data-prep.md +291 -0
- tdsql_mcp/syntax/uaf-diagnostics.md +654 -0
- tdsql_mcp/syntax/uaf-dsp.md +676 -0
- tdsql_mcp/syntax/uaf-estimation.md +1055 -0
- tdsql_mcp/syntax/uaf-forecasting.md +384 -0
- tdsql_mcp/syntax/uaf-formula-rules.md +123 -0
- tdsql_mcp/syntax/uaf-utility.md +421 -0
- tdsql_mcp/syntax/utility-functions.md +152 -0
- tdsql_mcp/syntax/vector-search.md +396 -0
- tdsql_mcp/syntax/window-functions.md +97 -0
- tdsql_mcp-1.0.0.dist-info/METADATA +525 -0
- tdsql_mcp-1.0.0.dist-info/RECORD +46 -0
- tdsql_mcp-1.0.0.dist-info/WHEEL +4 -0
- tdsql_mcp-1.0.0.dist-info/entry_points.txt +2 -0
tdsql_mcp/__init__.py
ADDED
|
File without changes
|
tdsql_mcp/server.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""tdsql-mcp: MCP server for Teradata SQL operations."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from importlib import resources
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import urlparse, parse_qs, unquote
|
|
11
|
+
|
|
12
|
+
import teradatasql
|
|
13
|
+
from dotenv import load_dotenv
|
|
14
|
+
from mcp.server.fastmcp import FastMCP
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Server
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
mcp = FastMCP(
|
|
21
|
+
"tdsql-mcp",
|
|
22
|
+
instructions=(
|
|
23
|
+
"You are working with a Teradata Vantage database. "
|
|
24
|
+
"IMPORTANT: Always prefer native Teradata table operators over hand-written SQL equivalents. "
|
|
25
|
+
"Teradata Vantage has built-in distributed functions for analytics, ML, data preparation, "
|
|
26
|
+
"text processing, and vector search. These run across all AMPs in parallel and outperform "
|
|
27
|
+
"equivalent hand-written SQL. Do NOT write manual SQL for operations like scaling, encoding, "
|
|
28
|
+
"binning, statistics, clustering, classification, or similarity search when a native function exists. "
|
|
29
|
+
"Before writing any analytics, transformation, or ML SQL: "
|
|
30
|
+
"(1) call get_syntax_help(topic='guidelines') to see the canonical mapping of common operations "
|
|
31
|
+
"to native Teradata functions, "
|
|
32
|
+
"(2) call get_syntax_help(topic='index') to discover all available topics, "
|
|
33
|
+
"(3) load the relevant topic(s) for exact syntax. "
|
|
34
|
+
"Use explain_query to validate syntax before executing. "
|
|
35
|
+
"Use describe_table and list_tables to explore the schema. "
|
|
36
|
+
"Results are returned as JSON."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Connection management
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
_connection: "teradatasql.TeradataConnection | None" = None
|
|
45
|
+
_conn_params: dict[str, Any] = {}
|
|
46
|
+
_conn_lock = threading.RLock() # RLock so tools can call helpers without deadlock
|
|
47
|
+
_read_only: bool = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _reconnect_if_needed() -> "teradatasql.TeradataConnection":
|
|
51
|
+
"""Return a live connection. Must be called with _conn_lock held."""
|
|
52
|
+
global _connection
|
|
53
|
+
if _connection is not None:
|
|
54
|
+
try:
|
|
55
|
+
cur = _connection.cursor()
|
|
56
|
+
cur.execute("SELECT 1")
|
|
57
|
+
cur.close()
|
|
58
|
+
return _connection
|
|
59
|
+
except Exception:
|
|
60
|
+
try:
|
|
61
|
+
_connection.close()
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
_connection = None
|
|
65
|
+
_connection = teradatasql.connect(**_conn_params)
|
|
66
|
+
return _connection
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_connection() -> "teradatasql.TeradataConnection":
|
|
70
|
+
"""Return a live connection, reconnecting if necessary."""
|
|
71
|
+
with _conn_lock:
|
|
72
|
+
return _reconnect_if_needed()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Internal helpers
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def _require_write() -> None:
|
|
80
|
+
if _read_only:
|
|
81
|
+
raise PermissionError("Server is running in read-only mode; write operations are disabled.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _execute_query_internal(sql: str, params: list | None = None) -> list[dict]:
|
|
85
|
+
with _conn_lock:
|
|
86
|
+
conn = _reconnect_if_needed()
|
|
87
|
+
cur = conn.cursor()
|
|
88
|
+
try:
|
|
89
|
+
cur.execute(sql, params or [])
|
|
90
|
+
if cur.description is None:
|
|
91
|
+
return []
|
|
92
|
+
columns = [desc[0] for desc in cur.description]
|
|
93
|
+
return [dict(zip(columns, row)) for row in cur.fetchall()]
|
|
94
|
+
finally:
|
|
95
|
+
cur.close()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Tools
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
@mcp.tool()
|
|
103
|
+
def execute_query(sql: str, max_rows: int = 100) -> str:
|
|
104
|
+
"""Execute a SQL SELECT query and return results as JSON.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
sql: The SQL query to execute.
|
|
108
|
+
max_rows: Maximum number of rows to return (default 100, capped at 10000).
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
JSON object with keys: rows (array), row_count (int), truncated (bool).
|
|
112
|
+
"""
|
|
113
|
+
max_rows = min(max(1, max_rows), 10_000)
|
|
114
|
+
with _conn_lock:
|
|
115
|
+
conn = _reconnect_if_needed()
|
|
116
|
+
cur = conn.cursor()
|
|
117
|
+
try:
|
|
118
|
+
cur.execute(sql)
|
|
119
|
+
if cur.description is None:
|
|
120
|
+
return json.dumps({"rows": [], "row_count": 0, "truncated": False})
|
|
121
|
+
columns = [desc[0] for desc in cur.description]
|
|
122
|
+
rows = cur.fetchmany(max_rows)
|
|
123
|
+
result = [dict(zip(columns, row)) for row in rows]
|
|
124
|
+
# Peek to detect truncation without fetching everything
|
|
125
|
+
truncated = cur.fetchone() is not None
|
|
126
|
+
return json.dumps(
|
|
127
|
+
{"rows": result, "row_count": len(result), "truncated": truncated},
|
|
128
|
+
default=str,
|
|
129
|
+
)
|
|
130
|
+
finally:
|
|
131
|
+
cur.close()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@mcp.tool()
|
|
135
|
+
def execute_statement(sql: str) -> str:
|
|
136
|
+
"""Execute a DDL or DML statement (INSERT, UPDATE, DELETE, CREATE, DROP, etc.).
|
|
137
|
+
|
|
138
|
+
Not available when the server is running in read-only mode.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
sql: The SQL statement to execute.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
JSON object with keys: status (str), rowcount (int).
|
|
145
|
+
"""
|
|
146
|
+
_require_write()
|
|
147
|
+
with _conn_lock:
|
|
148
|
+
conn = _reconnect_if_needed()
|
|
149
|
+
cur = conn.cursor()
|
|
150
|
+
try:
|
|
151
|
+
cur.execute(sql)
|
|
152
|
+
return json.dumps({"status": "success", "rowcount": cur.rowcount}, default=str)
|
|
153
|
+
finally:
|
|
154
|
+
cur.close()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@mcp.tool()
|
|
158
|
+
def list_databases() -> str:
|
|
159
|
+
"""List all accessible databases/schemas in the Teradata system.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
JSON array of objects with keys: DatabaseName, CommentString.
|
|
163
|
+
"""
|
|
164
|
+
rows = _execute_query_internal(
|
|
165
|
+
"SELECT DatabaseName, CommentString FROM DBC.DatabasesV ORDER BY DatabaseName"
|
|
166
|
+
)
|
|
167
|
+
return json.dumps(rows, default=str)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@mcp.tool()
|
|
171
|
+
def list_tables(database: str) -> str:
|
|
172
|
+
"""List tables and views in a given database/schema.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
database: The database or schema name to inspect.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
JSON array of objects with keys: TableName, TableKind, CommentString.
|
|
179
|
+
TableKind: T=table, V=view, O=NoPI table, Q=queue table, etc.
|
|
180
|
+
"""
|
|
181
|
+
rows = _execute_query_internal(
|
|
182
|
+
"SELECT TableName, TableKind, CommentString "
|
|
183
|
+
"FROM DBC.TablesV "
|
|
184
|
+
"WHERE DatabaseName = ? "
|
|
185
|
+
"ORDER BY TableKind, TableName",
|
|
186
|
+
[database],
|
|
187
|
+
)
|
|
188
|
+
return json.dumps(rows, default=str)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@mcp.tool()
|
|
192
|
+
def describe_table(table_name: str, database: str = "") -> str:
|
|
193
|
+
"""Describe the columns of a table or view.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
table_name: The table or view name.
|
|
197
|
+
database: The database/schema name. Uses the server default if omitted.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
JSON array of column definitions with keys: ColumnName, ColumnType,
|
|
201
|
+
Nullable, ColumnLength, DecimalTotalDigits, DecimalFractionalDigits,
|
|
202
|
+
ColumnFormat, CommentString.
|
|
203
|
+
"""
|
|
204
|
+
db = database or _conn_params.get("database", "")
|
|
205
|
+
if not db:
|
|
206
|
+
return json.dumps(
|
|
207
|
+
{"error": "'database' parameter is required when no default database is configured."}
|
|
208
|
+
)
|
|
209
|
+
rows = _execute_query_internal(
|
|
210
|
+
"SELECT ColumnName, ColumnType, Nullable, ColumnLength, "
|
|
211
|
+
"DecimalTotalDigits, DecimalFractionalDigits, ColumnFormat, CommentString "
|
|
212
|
+
"FROM DBC.ColumnsV "
|
|
213
|
+
"WHERE DatabaseName = ? AND TableName = ? "
|
|
214
|
+
"ORDER BY ColumnId",
|
|
215
|
+
[db, table_name],
|
|
216
|
+
)
|
|
217
|
+
return json.dumps(rows, default=str)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@mcp.tool()
|
|
221
|
+
def explain_query(sql: str) -> str:
|
|
222
|
+
"""Run EXPLAIN on a SQL query to validate syntax and preview the execution plan.
|
|
223
|
+
|
|
224
|
+
Use this to check whether a query is syntactically correct before executing it.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
sql: The SQL query to explain (do not include the EXPLAIN keyword).
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
JSON object with keys: valid (bool), plan (list of step strings) on success,
|
|
231
|
+
or error (str) on failure.
|
|
232
|
+
"""
|
|
233
|
+
with _conn_lock:
|
|
234
|
+
conn = _reconnect_if_needed()
|
|
235
|
+
cur = conn.cursor()
|
|
236
|
+
try:
|
|
237
|
+
cur.execute(f"EXPLAIN {sql}")
|
|
238
|
+
steps = [row[0] for row in cur.fetchall()]
|
|
239
|
+
return json.dumps({"valid": True, "plan": steps})
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
return json.dumps({"valid": False, "error": str(exc)})
|
|
242
|
+
finally:
|
|
243
|
+
cur.close()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# Syntax help — file-backed, auto-discovers src/tdsql_mcp/syntax/*.md
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
def _syntax_dir():
|
|
251
|
+
"""Return a Traversable pointing at the syntax/ package directory."""
|
|
252
|
+
return resources.files("tdsql_mcp") / "syntax"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _list_topics() -> list[str]:
|
|
256
|
+
"""Return sorted list of available topic names (filename without .md)."""
|
|
257
|
+
return sorted(
|
|
258
|
+
f.name[:-3]
|
|
259
|
+
for f in _syntax_dir().iterdir()
|
|
260
|
+
if f.name.endswith(".md")
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _read_topic(topic: str) -> str | None:
|
|
265
|
+
path = _syntax_dir() / f"{topic}.md"
|
|
266
|
+
try:
|
|
267
|
+
return path.read_text(encoding="utf-8")
|
|
268
|
+
except (FileNotFoundError, TypeError):
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@mcp.tool()
|
|
273
|
+
def get_syntax_help(topic: str = "index") -> str:
|
|
274
|
+
"""Return Teradata SQL syntax reference for a given topic.
|
|
275
|
+
|
|
276
|
+
IMPORTANT: Call this tool BEFORE writing any analytics, transformation, ML, or data
|
|
277
|
+
preparation SQL. Teradata Vantage has native distributed table operators for most
|
|
278
|
+
operations — scaling, encoding, binning, statistics, clustering, classification, text
|
|
279
|
+
analytics, vector search, and more. These outperform hand-written SQL and should always
|
|
280
|
+
be preferred. Do not write manual SQL for an operation if a native function exists.
|
|
281
|
+
|
|
282
|
+
Recommended call order:
|
|
283
|
+
1. get_syntax_help(topic='guidelines') — see the canonical mapping of common SQL
|
|
284
|
+
patterns to native Teradata functions (start here if unsure what exists)
|
|
285
|
+
2. get_syntax_help(topic='index') — browse all available topics and the Workflows
|
|
286
|
+
section that maps use cases to topic sequences
|
|
287
|
+
3. get_syntax_help(topic='<specific-topic>') — load exact syntax for a topic
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
topic: The topic name (e.g. 'data-prep', 'ml-functions', 'vector-search').
|
|
291
|
+
Use 'index' to list all available topics.
|
|
292
|
+
Use 'guidelines' for the native-functions-first reference.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Markdown reference text for the requested topic, or a list of valid topics
|
|
296
|
+
if the requested topic is not found.
|
|
297
|
+
"""
|
|
298
|
+
content = _read_topic(topic)
|
|
299
|
+
if content is not None:
|
|
300
|
+
return content
|
|
301
|
+
available = _list_topics()
|
|
302
|
+
return (
|
|
303
|
+
f"Topic '{topic}' not found.\n\n"
|
|
304
|
+
f"Available topics:\n"
|
|
305
|
+
+ "\n".join(f" - {t}" for t in available)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ---------------------------------------------------------------------------
|
|
310
|
+
# Resources — file-backed, same source as get_syntax_help tool
|
|
311
|
+
# ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
@mcp.resource("teradata://syntax/{topic}")
|
|
314
|
+
def get_syntax_resource(topic: str) -> str:
|
|
315
|
+
"""Teradata SQL syntax reference for a given topic. Use 'index' to list all topics."""
|
|
316
|
+
content = _read_topic(topic)
|
|
317
|
+
if content is not None:
|
|
318
|
+
return content
|
|
319
|
+
available = _list_topics()
|
|
320
|
+
return (
|
|
321
|
+
f"Topic '{topic}' not found.\n\n"
|
|
322
|
+
f"Available topics:\n"
|
|
323
|
+
+ "\n".join(f" - {t}" for t in available)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# URI parsing
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
def _parse_uri(uri: str) -> dict[str, Any]:
|
|
332
|
+
"""Parse a Teradata connection URI into a teradatasql.connect() kwargs dict.
|
|
333
|
+
|
|
334
|
+
URI format:
|
|
335
|
+
teradata://user:password@host[:port][/database][?param=value&...]
|
|
336
|
+
|
|
337
|
+
URI components map to teradatasql parameters:
|
|
338
|
+
user → user
|
|
339
|
+
password → password
|
|
340
|
+
host → host
|
|
341
|
+
port → dbs_port (Teradata default: 1025)
|
|
342
|
+
/path → database
|
|
343
|
+
?query → passed through as-is to teradatasql.connect()
|
|
344
|
+
|
|
345
|
+
Any additional teradatasql connection parameter (logmech, encryptdata,
|
|
346
|
+
sslmode, logon_timeout, etc.) can be appended as query-string key=value pairs.
|
|
347
|
+
"""
|
|
348
|
+
parsed = urlparse(uri)
|
|
349
|
+
|
|
350
|
+
if parsed.scheme.lower() != "teradata":
|
|
351
|
+
raise ValueError(
|
|
352
|
+
f"Invalid URI scheme '{parsed.scheme}'. Must be 'teradata'. "
|
|
353
|
+
f"Expected format: teradata://user:password@host/database?param=value"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
params: dict[str, Any] = {}
|
|
357
|
+
|
|
358
|
+
if not parsed.hostname:
|
|
359
|
+
raise ValueError("URI is missing a hostname. Expected: teradata://user:password@host/...")
|
|
360
|
+
|
|
361
|
+
params["host"] = parsed.hostname
|
|
362
|
+
|
|
363
|
+
if parsed.username:
|
|
364
|
+
params["user"] = unquote(parsed.username)
|
|
365
|
+
|
|
366
|
+
if parsed.password:
|
|
367
|
+
params["password"] = unquote(parsed.password)
|
|
368
|
+
|
|
369
|
+
if parsed.port:
|
|
370
|
+
params["dbs_port"] = str(parsed.port)
|
|
371
|
+
|
|
372
|
+
# Path becomes the default database (strip leading slash)
|
|
373
|
+
if parsed.path and parsed.path.lstrip("/"):
|
|
374
|
+
params["database"] = parsed.path.lstrip("/")
|
|
375
|
+
|
|
376
|
+
# All query-string parameters are passed through to teradatasql as-is.
|
|
377
|
+
# Values are always strings, which is correct — teradatasql expects quoted
|
|
378
|
+
# integers and booleans as strings (e.g. logon_timeout="30", encryptdata="true").
|
|
379
|
+
for key, values in parse_qs(parsed.query, keep_blank_values=True).items():
|
|
380
|
+
params[key] = values[0]
|
|
381
|
+
|
|
382
|
+
if not params.get("user"):
|
|
383
|
+
raise ValueError("URI is missing a username. Expected: teradata://user:password@host/...")
|
|
384
|
+
if not params.get("password"):
|
|
385
|
+
raise ValueError("URI is missing a password. Expected: teradata://user:password@host/...")
|
|
386
|
+
|
|
387
|
+
return params
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ---------------------------------------------------------------------------
|
|
391
|
+
# Entry point
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
def main() -> None:
|
|
395
|
+
global _conn_params, _read_only
|
|
396
|
+
|
|
397
|
+
# Load .env file if present (no-op if missing)
|
|
398
|
+
load_dotenv()
|
|
399
|
+
|
|
400
|
+
parser = argparse.ArgumentParser(
|
|
401
|
+
description="tdsql-mcp: Teradata SQL MCP server",
|
|
402
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
403
|
+
epilog=(
|
|
404
|
+
"Connection URI format:\n"
|
|
405
|
+
" teradata://user:password@host[:port][/database][?param=value&...]\n\n"
|
|
406
|
+
"Examples:\n"
|
|
407
|
+
" teradata://alice:s3cr3t@myhost/mydb\n"
|
|
408
|
+
" teradata://alice:s3cr3t@myhost:1025/mydb?logmech=LDAP&encryptdata=true\n"
|
|
409
|
+
" teradata://alice:s3cr3t@myhost/mydb?logon_timeout=30&sslmode=VERIFY-FULL\n\n"
|
|
410
|
+
"Any teradatasql connection parameter can be added as a query-string argument.\n"
|
|
411
|
+
"See: https://github.com/Teradata/python-driver#connection-parameters"
|
|
412
|
+
),
|
|
413
|
+
)
|
|
414
|
+
parser.add_argument("--read-only", action="store_true", help="Disable all write operations")
|
|
415
|
+
parser.add_argument(
|
|
416
|
+
"--uri",
|
|
417
|
+
metavar="URI",
|
|
418
|
+
help="Teradata connection URI (overrides DATABASE_URI env var)",
|
|
419
|
+
)
|
|
420
|
+
args = parser.parse_args()
|
|
421
|
+
|
|
422
|
+
_read_only = args.read_only or os.getenv("TD_READ_ONLY", "").lower() in ("1", "true", "yes")
|
|
423
|
+
|
|
424
|
+
raw_uri = args.uri or os.getenv("DATABASE_URI", "")
|
|
425
|
+
if not raw_uri:
|
|
426
|
+
parser.error(
|
|
427
|
+
"A connection URI is required.\n"
|
|
428
|
+
"Set DATABASE_URI env var or pass --uri.\n"
|
|
429
|
+
"Format: teradata://user:password@host/database"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
_conn_params = _parse_uri(raw_uri)
|
|
434
|
+
except ValueError as exc:
|
|
435
|
+
parser.error(str(exc))
|
|
436
|
+
|
|
437
|
+
# Eagerly connect so startup errors surface immediately
|
|
438
|
+
try:
|
|
439
|
+
get_connection()
|
|
440
|
+
except Exception as exc:
|
|
441
|
+
raise SystemExit(f"Failed to connect to Teradata at {_conn_params.get('host')!r}: {exc}") from exc
|
|
442
|
+
|
|
443
|
+
mode = "read-only" if _read_only else "read-write"
|
|
444
|
+
print(f"tdsql-mcp started ({mode}) — connected to {_conn_params['host']}", file=sys.stderr, flush=True)
|
|
445
|
+
|
|
446
|
+
mcp.run()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
if __name__ == "__main__":
|
|
450
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Teradata Aggregate Functions
|
|
2
|
+
|
|
3
|
+
## Standard Aggregates
|
|
4
|
+
```sql
|
|
5
|
+
COUNT(*) -- count all rows
|
|
6
|
+
COUNT(col) -- count non-NULL values
|
|
7
|
+
COUNT(DISTINCT col) -- count distinct non-NULL values
|
|
8
|
+
SUM(col)
|
|
9
|
+
AVG(col)
|
|
10
|
+
MIN(col)
|
|
11
|
+
MAX(col)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Statistical Aggregates
|
|
15
|
+
```sql
|
|
16
|
+
STDDEV_POP(col) -- population standard deviation
|
|
17
|
+
STDDEV_SAMP(col) -- sample standard deviation
|
|
18
|
+
VAR_POP(col) -- population variance
|
|
19
|
+
VAR_SAMP(col) -- sample variance
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Percentile / Quantile
|
|
23
|
+
```sql
|
|
24
|
+
-- Exact (sorts all data — can be slow on large sets)
|
|
25
|
+
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY col) -- median
|
|
26
|
+
PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY col) -- 25th percentile (discrete)
|
|
27
|
+
|
|
28
|
+
-- Approximate (fast, uses HLL sketch — Vantage)
|
|
29
|
+
APPROX_PERCENTILE(col, 0.5)
|
|
30
|
+
APPROX_PERCENTILE(col, 0.95)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Approximate Count Distinct
|
|
34
|
+
```sql
|
|
35
|
+
-- HyperLogLog-based — much faster than COUNT(DISTINCT) on large data
|
|
36
|
+
APPROX_COUNT_DISTINCT(col)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## String Aggregation
|
|
40
|
+
```sql
|
|
41
|
+
-- Concatenate values into one string (XML-based, common pattern)
|
|
42
|
+
XMLAGG(XMLELEMENT(NAME x, col || ',') ORDER BY col)
|
|
43
|
+
|
|
44
|
+
-- Cleaner alternative using TD_SYSFNLIB.XMLAGG or custom UDF
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## GROUP BY Variants
|
|
48
|
+
```sql
|
|
49
|
+
-- Standard
|
|
50
|
+
SELECT dept, SUM(salary) FROM db.t GROUP BY dept;
|
|
51
|
+
|
|
52
|
+
-- Multiple grouping sets in one pass
|
|
53
|
+
GROUP BY GROUPING SETS ((dept), (region), (dept, region), ())
|
|
54
|
+
|
|
55
|
+
-- Equivalent shorthand
|
|
56
|
+
GROUP BY ROLLUP(dept, region) -- all prefixes + grand total
|
|
57
|
+
GROUP BY CUBE(dept, region) -- all combinations
|
|
58
|
+
|
|
59
|
+
-- Identify which grouping a row belongs to
|
|
60
|
+
GROUPING(dept) -- 1 if dept is aggregated away, 0 if not
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## HAVING
|
|
64
|
+
```sql
|
|
65
|
+
SELECT dept, COUNT(*) AS cnt
|
|
66
|
+
FROM db.employees
|
|
67
|
+
GROUP BY dept
|
|
68
|
+
HAVING COUNT(*) > 10;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Conditional Aggregation
|
|
72
|
+
```sql
|
|
73
|
+
-- Count rows meeting a condition
|
|
74
|
+
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active_count
|
|
75
|
+
|
|
76
|
+
-- Average only non-zero values
|
|
77
|
+
AVG(NULLIFZERO(amount))
|
|
78
|
+
|
|
79
|
+
-- Max of a filtered subset
|
|
80
|
+
MAX(CASE WHEN category = 'A' THEN value END)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Common Patterns
|
|
84
|
+
```sql
|
|
85
|
+
-- Frequency distribution
|
|
86
|
+
SELECT val, COUNT(*) AS freq,
|
|
87
|
+
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER () AS pct
|
|
88
|
+
FROM db.t
|
|
89
|
+
GROUP BY val
|
|
90
|
+
ORDER BY freq DESC;
|
|
91
|
+
|
|
92
|
+
-- Running total (use window function instead of aggregate)
|
|
93
|
+
SUM(amount) OVER (ORDER BY event_date ROWS UNBOUNDED PRECEDING)
|
|
94
|
+
```
|