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.
Files changed (46) hide show
  1. tdsql_mcp/__init__.py +0 -0
  2. tdsql_mcp/server.py +450 -0
  3. tdsql_mcp/syntax/aggregate-functions.md +94 -0
  4. tdsql_mcp/syntax/ai-text-analytics.md +630 -0
  5. tdsql_mcp/syntax/association-analysis.md +108 -0
  6. tdsql_mcp/syntax/authorization-objects.md +173 -0
  7. tdsql_mcp/syntax/byom-model-loading.md +337 -0
  8. tdsql_mcp/syntax/byom-scoring.md +581 -0
  9. tdsql_mcp/syntax/catalog-views.md +125 -0
  10. tdsql_mcp/syntax/conditional.md +85 -0
  11. tdsql_mcp/syntax/data-cleaning.md +514 -0
  12. tdsql_mcp/syntax/data-exploration.md +317 -0
  13. tdsql_mcp/syntax/data-prep.md +928 -0
  14. tdsql_mcp/syntax/data-types-casting.md +237 -0
  15. tdsql_mcp/syntax/date-time.md +107 -0
  16. tdsql_mcp/syntax/embeddings.md +279 -0
  17. tdsql_mcp/syntax/fit-transform-pattern.md +94 -0
  18. tdsql_mcp/syntax/geospatial.md +731 -0
  19. tdsql_mcp/syntax/guidelines.md +290 -0
  20. tdsql_mcp/syntax/hypothesis-testing.md +240 -0
  21. tdsql_mcp/syntax/index.md +102 -0
  22. tdsql_mcp/syntax/llm-providers.md +230 -0
  23. tdsql_mcp/syntax/ml-functions.md +757 -0
  24. tdsql_mcp/syntax/ml-patterns.md +296 -0
  25. tdsql_mcp/syntax/model-evaluation.md +252 -0
  26. tdsql_mcp/syntax/numeric-functions.md +94 -0
  27. tdsql_mcp/syntax/path-analysis.md +358 -0
  28. tdsql_mcp/syntax/query-tuning.md +104 -0
  29. tdsql_mcp/syntax/sql-basics.md +85 -0
  30. tdsql_mcp/syntax/string-functions.md +74 -0
  31. tdsql_mcp/syntax/text-analytics.md +524 -0
  32. tdsql_mcp/syntax/uaf-concepts.md +309 -0
  33. tdsql_mcp/syntax/uaf-data-prep.md +291 -0
  34. tdsql_mcp/syntax/uaf-diagnostics.md +654 -0
  35. tdsql_mcp/syntax/uaf-dsp.md +676 -0
  36. tdsql_mcp/syntax/uaf-estimation.md +1055 -0
  37. tdsql_mcp/syntax/uaf-forecasting.md +384 -0
  38. tdsql_mcp/syntax/uaf-formula-rules.md +123 -0
  39. tdsql_mcp/syntax/uaf-utility.md +421 -0
  40. tdsql_mcp/syntax/utility-functions.md +152 -0
  41. tdsql_mcp/syntax/vector-search.md +396 -0
  42. tdsql_mcp/syntax/window-functions.md +97 -0
  43. tdsql_mcp-1.0.0.dist-info/METADATA +525 -0
  44. tdsql_mcp-1.0.0.dist-info/RECORD +46 -0
  45. tdsql_mcp-1.0.0.dist-info/WHEEL +4 -0
  46. 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
+ ```