feat(integrations): add ClickHouse block and expand Dagster + Tinybird tools (#4883)

* feat(integrations): add ClickHouse block and expand Dagster + Tinybird tools

* fix(tinybird): fail loudly on invalid query_pipe parameters JSON

parsePipeParameters previously returned {} on any JSON parse error, so a
mistyped 'parameters' input produced a successful pipe call with the dynamic
filters silently dropped. Throw a clear error for non-empty, non-object input
instead; an omitted/empty value still means 'no parameters'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dagster): guard NaN numeric coercions and bound list_assets pagination

Address PR review:
- Route all block numeric coercions (list_runs limit/createdAfter/createdBefore,
  get_run_logs logsLimit, list_assets assetsLimit) through a toFiniteNumber()
  guard so invalid/wand-generated text becomes undefined instead of NaN.
- list_assets now applies a default page size (100) when no limit is given, so
  paging stays bounded and hasMore is meaningful even when limit is omitted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dagster): make list_assets hasMore exact via fetch N+1

Address PR review (hasMore true on exact page): request one extra row
(pageSize + 1), use its presence as the authoritative hasMore, slice it off,
and derive the returned cursor from the last RETURNED asset's key path
(JSON-serialized; Dagster normalizes JS/Python whitespace on the way in).
This removes the false-positive hasMore when the final page is exactly full.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(clickhouse): enforce read-only query operation and harden WHERE-clause guard

* fix(dagster): make list_runs hasMore exact via fetch N+1

Address PR review (list runs false hasMore): request one extra row
(pageSize + 1), use its presence as the authoritative hasMore, and slice it
off before mapping. Removes the false-positive hasMore (and misleading cursor)
when the final page is exactly `limit` runs long. Mirrors the list_assets fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(clickhouse): restrict DROP PARTITION to literal values to prevent SQL injection

* fix(clickhouse): reject chained statements in read-only query operation

* fix(clickhouse): force JSON output on query path and ignore comments when detecting chained statements

* fix(tinybird): encode datasource/pipe names in URL paths to prevent traversal

A user-or-llm datasource/pipe name interpolated raw into the URL path (e.g.
'real_ds/../../other') is normalized by the WHATWG URL parser and can target a
different endpoint. Wrap the path segment with encodeURIComponent in the
truncate, delete, and query_pipe URLs. Events/append pass the name via
URLSearchParams, which already encodes, so they were unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(clickhouse): block WITH-led writes/DDL in read-only query operation

* fix(clickhouse): validate column types structurally and normalize FORMAT around SETTINGS

* fix(clickhouse): balance-check ORDER BY/PARTITION BY and skip leading comments in read-only guard

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-06-04 14:49:51 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 2ccd9a3ac5
commit 16954e46fe
101 changed files with 9002 additions and 157 deletions
+29 -6
View File
@@ -2743,6 +2743,18 @@ export function ClerkIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ClickHouseIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 150 150' xmlns='http://www.w3.org/2000/svg'>
<path d='m0 0h150v150h-150z' fill='#f9ff69' />
<path
d='m30 28.3c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v93.3c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1zm21.2 0c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v93.3c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1zm21.2 0c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v93.3c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1zm21.3 0c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v93.3c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1zm21.2 37.2c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v19c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1z'
fill='#161616'
/>
</svg>
)
}
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 23 23' {...props}>
@@ -3365,7 +3377,18 @@ export const OllamaIcon = (props: SVGProps<SVGSVGElement>) => (
xmlns='http://www.w3.org/2000/svg'
>
<title>Ollama</title>
<path d='M7.91 1.09c.216.09.411.23.588.41.3.306.54.744.73 1.26.191.52.315 1.1.36 1.68a5.05 5.05 0 12.05-.636l.051-.004c.87-.07 1.73.09 2.48.47.101.05.2.11.3.17.05-.569.17-1.13.36-1.64.19-.52.44-.957.73-1.26a1.67 1.67 0 1.59-.41c.257-.1.53-.118.8-.42.4.114.75.368 1.02.737.25.337.43.769.56 1.29.23.93.27 2.16.115 3.65l.053.4.03.019c.757.58 1.28 1.4 1.56 2.35.44 1.49.216 3.16-.534 4.09l-.18.02.2c.417.76.67 1.57.724 2.4l.002.03c.064 1.07-.2 2.14-.814 3.19l-.007.01.1.02c.472 1.16.62 2.32.438 3.49l-.6.04a.651.65 0 01-.747.54.648.65 0 01-.54-.742c.167-1.03.01-2.07-.48-3.12a.643.64 0 01.04-.617l.004-.006c.604-.924.85-1.83.8-2.72-.046-.779-.325-1.54-.8-2.27a.644.64 0 01.18-.886l.009-.006c.243-.159.47-.565.58-1.12a4.23 4.23 0 00-.095-1.97c-.205-.7-.58-1.28-1.1-1.68-.595-.454-1.38-.673-2.38-.61a.653.65 0 01-.632-.371c-.314-.665-.772-1.14-1.34-1.44a3.29 3.29 0 00-1.77-.332c-1.25.099-2.34.801-2.67 1.69a.652.65 0 01-.61.43c-1.07.002-1.89.252-2.5.703-.522.39-.878.94-1.07 1.59a4.07 4.07 0 00-.068 1.89c.112.56.331 1.2.58 1.27l.8.01c.212.21.257.53.11.785-.36.62-.629 1.55-.673 2.44-.05 1.02.186 1.9.719 2.54l.16.02a.643.64 0 1.1.69c-.576 1.24-.753 2.25-.562 3.05a.652.65 0 01-1.27.298c-.243-1.02-.078-2.18.473-3.5l.014-.035-.008-.012a4.34 4.34 0 01-.598-1.31l-.005-.019a5.76 5.76 0 01-.177-1.78c.044-.91.28-1.84.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.54l-.005-.024a5.35 5.35 0 1.09-2.49c.262-.915.78-1.7 1.54-2.27.06-.45.12-.9.19-.132-.159-1.49-.119-2.73.11-3.67.13-.518.31-.95.56-1.29.27-.368.61-.622 1.01-.737.27-.076.54-.59.8.042zm4.12 9.09c.936 0 1.8.31 2.45.855.63.53 1 1.24 1 1.94 0 .888-.406 1.58-1.13 2.02-.62.38-1.45.557-2.4.557-1.01 0-1.87-.259-2.49-.734-.617-.47-.963-1.13-.963-1.84 0-.707.4-1.42 1.06-1.95.668-.537 1.55-.849 2.49-.849zm0 .896a3.07 3.07 0 00-1.92.65c-.461.37-.722.84-.722 1.25 0 .428.21.83.61 1.13.455.35 1.12.548 1.94.548.8 0 1.47-.147 1.93-.426.46-.28.7-.686.7-1.26 0-.423-.246-.89-.683-1.26-.484-.405-1.14-.643-1.86-.643zm.662 1.21l.4c.12.15.095.37-.056.49l-.292.23v.446a.375.38 0 01-.376.37.375.38 0 01-.376-.373v-.46l-.271-.218a.347.35 0 01-.052-.49.35.353 0 1.49-.051l.215.17.22-.174a.353.35 0 01.49.05zm-5.04-1.92c.478 0 .867.39.87.871a.87.87 0 01-.868.87.87.87 0 01-.867-.87.87.87 0 1.87-.872zm8.71 0c.48 0 .868.39.87.871a.87.87 0 01-.868.87.87.87 0 01-.867-.87.87.87 0 1.87-.872zM7.44 2.3l-.3a.659.66 0 00-.285.24l-.5.01c-.138.19-.258.47-.348.83-.17.69-.216 1.63-.124 2.78.43-.128.9-.208 1.4-.237l.01-.1.02-.034c.046-.82.1-.161.15-.239.12-.771.02-1.69-.253-2.44-.134-.364-.297-.65-.453-.813a.628.63 0 00-.107-.09L7.44 2.3zm9.17.04l-.2a.628.63 0 00-.107.09c-.156.16-.32.45-.453.81-.29.79-.387 1.78-.23 2.57l.58.1.8.01h.03a5.18 5.18 0 11.47.212c.086-1.12.038-2.04-.128-2.72-.09-.365-.21-.643-.349-.832l-.004-.006a.659.66 0 00-.285-.239h-.004z' />
<path d='M7.905 1.09c.216.085.411.225.588.41.295.306.544.744.734 1.263.191.522.315 1.1.362 1.68a5.054 5.054 0 012.049-.636l.051-.004c.87-.07 1.73.087 2.48.474.101.053.2.11.297.17.05-.569.172-1.134.36-1.644.19-.52.439-.957.733-1.264a1.67 1.67 0 01.589-.41c.257-.1.53-.118.796-.042.401.114.745.368 1.016.737.248.337.434.769.561 1.287.23.934.27 2.163.115 3.645l.053.04.026.019c.757.576 1.284 1.397 1.563 2.35.435 1.487.216 3.155-.534 4.088l-.018.021.002.003c.417.762.67 1.567.724 2.4l.002.03c.064 1.065-.2 2.137-.814 3.19l-.007.01.01.024c.472 1.157.62 2.322.438 3.486l-.006.039a.651.651 0 01-.747.536.648.648 0 01-.54-.742c.167-1.033.01-2.069-.48-3.123a.643.643 0 01.04-.617l.004-.006c.604-.924.854-1.83.8-2.72-.046-.779-.325-1.544-.8-2.273a.644.644 0 01.18-.886l.009-.006c.243-.159.467-.565.58-1.12a4.229 4.229 0 00-.095-1.974c-.205-.7-.58-1.284-1.105-1.683-.595-.454-1.383-.673-2.38-.61a.653.653 0 01-.632-.371c-.314-.665-.772-1.141-1.343-1.436a3.288 3.288 0 00-1.772-.332c-1.245.099-2.343.801-2.67 1.686a.652.652 0 01-.61.425c-1.067.002-1.893.252-2.497.703-.522.39-.878.935-1.066 1.588a4.07 4.07 0 00-.068 1.886c.112.558.331 1.02.582 1.269l.008.007c.212.207.257.53.109.785-.36.622-.629 1.549-.673 2.44-.05 1.018.186 1.902.719 2.536l.016.019a.643.643 0 01.095.69c-.576 1.236-.753 2.252-.562 3.052a.652.652 0 01-1.269.298c-.243-1.018-.078-2.184.473-3.498l.014-.035-.008-.012a4.339 4.339 0 01-.598-1.309l-.005-.019a5.764 5.764 0 01-.177-1.785c.044-.91.278-1.842.622-2.59l.012-.026-.002-.002c-.293-.418-.51-.953-.63-1.545l-.005-.024a5.352 5.352 0 01.093-2.49c.262-.915.777-1.701 1.536-2.269.06-.045.123-.09.186-.132-.159-1.493-.119-2.73.112-3.67.127-.518.314-.95.562-1.287.27-.368.614-.622 1.015-.737.266-.076.54-.059.797.042zm4.116 9.09c.936 0 1.8.313 2.446.855.63.527 1.005 1.235 1.005 1.94 0 .888-.406 1.58-1.133 2.022-.62.375-1.451.557-2.403.557-1.009 0-1.871-.259-2.493-.734-.617-.47-.963-1.13-.963-1.845 0-.707.398-1.417 1.056-1.946.668-.537 1.55-.849 2.485-.849zm0 .896a3.07 3.07 0 00-1.916.65c-.461.37-.722.835-.722 1.25 0 .428.21.829.61 1.134.455.347 1.124.548 1.943.548.799 0 1.473-.147 1.932-.426.463-.28.7-.686.7-1.257 0-.423-.246-.89-.683-1.256-.484-.405-1.14-.643-1.864-.643zm.662 1.21l.004.004c.12.151.095.37-.056.49l-.292.23v.446a.375.375 0 01-.376.373.375.375 0 01-.376-.373v-.46l-.271-.218a.347.347 0 01-.052-.49.353.353 0 01.494-.051l.215.172.22-.174a.353.353 0 01.49.051zm-5.04-1.919c.478 0 .867.39.867.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zm8.706 0c.48 0 .868.39.868.871a.87.87 0 01-.868.871.87.87 0 01-.867-.87.87.87 0 01.867-.872zM7.44 2.3l-.003.002a.659.659 0 00-.285.238l-.005.006c-.138.189-.258.467-.348.832-.17.692-.216 1.631-.124 2.782.43-.128.899-.208 1.404-.237l.01-.001.019-.034c.046-.082.095-.161.148-.239.123-.771.022-1.692-.253-2.444-.134-.364-.297-.65-.453-.813a.628.628 0 00-.107-.09L7.44 2.3zm9.174.04l-.002.001a.628.628 0 00-.107.09c-.156.163-.32.45-.453.814-.29.794-.387 1.776-.23 2.572l.058.097.008.014h.03a5.184 5.184 0 011.466.212c.086-1.124.038-2.043-.128-2.722-.09-.365-.21-.643-.349-.832l-.004-.006a.659.659 0 00-.285-.239h-.004z' />
</svg>
)
export const FalIcon = (props: SVGProps<SVGSVGElement>) => (
<svg {...props} height='1em' viewBox='0 0 24 24' width='1em' xmlns='http://www.w3.org/2000/svg'>
<title>Fal</title>
<path
clipRule='evenodd'
d='M15.477 0c.415 0 .749.338.788.752a7.775 7.775 0 006.985 6.984c.413.04.752.373.752.788v6.952c0 .415-.338.748-.752.788a7.775 7.775 0 00-6.985 6.984c-.04.414-.373.752-.788.752H8.525c-.416 0-.749-.338-.789-.752a7.775 7.775 0 00-6.984-6.984c-.414-.04-.752-.373-.752-.788V8.524c0-.415.338-.748.752-.788A7.775 7.775 0 007.736.752C7.776.338 8.11 0 8.526 0h6.95zM4.819 11.98a7.226 7.226 0 007.223 7.23 7.226 7.226 0 007.223-7.23c0-3.994-3.234-7.23-7.223-7.23a7.227 7.227 0 00-7.223 7.23z'
fill='#EC0648'
fillRule='evenodd'
/>
</svg>
)
export function ShieldCheckIcon(props: SVGProps<SVGSVGElement>) {
@@ -3982,16 +4005,16 @@ export function FireworksIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 0 512 512'
height='1em'
width='1em'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
fillRule='evenodd'
clipRule='evenodd'
strokeLinejoin='round'
strokeMiterlimit={2}
>
<path
d='M314.33 110.17L255.98 251.73l-58.42-141.56h-37.46l64 154.75c5.23 12.85 17.77 21.31 31.65 21.31s26.42-8.44 31.65-21.27l64.4-154.79h-37.46zm24.92 215.67L446 216.58l-14.56-34.77-116.58 119.56c-9.71 9.96-12.54 24.83-7.15 37.65 5.29 12.73 17.79 21.08 31.58 21.08l.42.06L506 359.75l-14.56-34.77-152.15.853h-.042zM66 216.5l14.56-34.77 116.58 119.56a34.59 34.59 0 17.15 37.65C199 351.67 186.5 360.02 172.71 360.02l-166.67-.375-.42.04 14.56-34.77 152.15.875L66 216.5z'
fill='#5019c5'
d='M14.8 5l-2.801 6.795L9.195 5H7.397l3.072 7.428a1.64 1.64 0 003.038.002L16.598 5H14.8zm1.196 10.352l5.124-5.244-.699-1.669-5.596 5.739a1.664 1.664 0 00-.343 1.807 1.642 1.642 0 001.516 1.012L16 17l8-.02-.699-1.669-7.303.041h-.002zM2.88 10.104l.699-1.669 5.596 5.739c.468.479.603 1.189.343 1.807a1.643 1.643 0 01-1.516 1.012l-8-.018-.002.002.699-1.669 7.303.042-5.122-5.246z'
fill='#5019C5'
/>
</svg>
)
+2
View File
@@ -31,6 +31,7 @@ import {
CirclebackIcon,
ClayIcon,
ClerkIcon,
ClickHouseIcon,
CloudFormationIcon,
CloudflareIcon,
CloudWatchIcon,
@@ -243,6 +244,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
circleback: CirclebackIcon,
clay: ClayIcon,
clerk: ClerkIcon,
clickhouse: ClickHouseIcon,
cloudflare: CloudflareIcon,
cloudformation: CloudFormationIcon,
cloudwatch: CloudWatchIcon,
@@ -0,0 +1,559 @@
---
title: ClickHouse
description: Connect to a ClickHouse database
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="clickhouse"
color="#f9ff69"
/>
{/* MANUAL-CONTENT-START:intro */}
[ClickHouse](https://clickhouse.com) is an open-source, column-oriented database management system for online analytical processing (OLAP). It is built for speed at scale — running aggregations and analytical queries over billions of rows in real time.
The ClickHouse block connects to any ClickHouse deployment (ClickHouse Cloud or self-hosted) over the [HTTP interface](https://clickhouse.com/docs/interfaces/http). Use it to run analytical queries, stream rows into tables, manage schemas, inspect system state, and execute arbitrary SQL — all from within a workflow.
**Connection details**
- **Host** — your ClickHouse hostname (e.g. `your-instance.clickhouse.cloud` or your server address).
- **Port** — the HTTP interface port. Use `8443` for HTTPS (ClickHouse Cloud) or `8123` for plain HTTP (self-hosted).
- **Database** / **Username** — default to `default` if not specified.
- **Password** — optional for unauthenticated local instances.
- **Use HTTPS** — keep enabled for any remote or Cloud instance.
**Things to know**
- `UPDATE` and `DELETE` are implemented as ClickHouse [mutations](https://clickhouse.com/docs/sql-reference/statements/alter/update) (`ALTER TABLE ... UPDATE/DELETE`). Mutations run **asynchronously** in the background, so the affected row count is not returned immediately.
- ClickHouse is optimized for bulk inserts. Prefer batching many rows per insert over many single-row inserts.
- The connection host is validated to block private/internal addresses, so the block cannot reach `localhost` or internal-only hosts.
{/* MANUAL-CONTENT-END */}
## Usage Instructions
Integrate ClickHouse into the workflow. Query and insert data, manage databases and tables, inspect schemas, monitor mutations and running queries, manage partitions, and execute raw SQL over the ClickHouse HTTP interface.
## Tools
### `clickhouse_query`
Execute a SELECT query on a ClickHouse database
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) |
| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) |
| `database` | string | Yes | Database name to connect to |
| `username` | string | Yes | ClickHouse username |
| `password` | string | No | ClickHouse password |
| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) |
| `query` | string | Yes | SQL SELECT query to execute |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Array of rows returned from the query |
| `rowCount` | number | Number of rows returned |
### `clickhouse_execute`
Execute raw SQL (DDL, mutations, or queries) on a ClickHouse database
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) |
| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) |
| `database` | string | Yes | Database name to connect to |
| `username` | string | Yes | ClickHouse username |
| `password` | string | No | ClickHouse password |
| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) |
| `query` | string | Yes | Raw SQL statement to execute |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Array of rows returned from the statement |
| `rowCount` | number | Number of rows returned or affected |
### `clickhouse_insert`
Insert a row into a ClickHouse table
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) |
| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) |
| `database` | string | Yes | Database name to connect to |
| `username` | string | Yes | ClickHouse username |
| `password` | string | No | ClickHouse password |
| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) |
| `table` | string | Yes | Table name to insert data into |
| `data` | object | Yes | Data object to insert \(key-value pairs mapping column names to values\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Inserted rows \(empty for ClickHouse inserts\) |
| `rowCount` | number | Number of rows inserted |
### `clickhouse_insert_rows`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_update`
Update rows in a ClickHouse table via an ALTER TABLE ... UPDATE mutation
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) |
| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) |
| `database` | string | Yes | Database name to connect to |
| `username` | string | Yes | ClickHouse username |
| `password` | string | No | ClickHouse password |
| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) |
| `table` | string | Yes | Table name to update data in |
| `data` | object | Yes | Data object with fields to update \(key-value pairs\) |
| `where` | string | Yes | WHERE clause condition \(without the WHERE keyword\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Updated rows \(empty for ClickHouse mutations\) |
| `rowCount` | number | Number of rows written by the mutation |
### `clickhouse_delete`
Delete rows from a ClickHouse table via an ALTER TABLE ... DELETE mutation
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) |
| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) |
| `database` | string | Yes | Database name to connect to |
| `username` | string | Yes | ClickHouse username |
| `password` | string | No | ClickHouse password |
| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) |
| `table` | string | Yes | Table name to delete data from |
| `where` | string | Yes | WHERE clause condition \(without the WHERE keyword\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `rows` | array | Deleted rows \(empty for ClickHouse mutations\) |
| `rowCount` | number | Number of rows affected by the mutation |
### `clickhouse_list_databases`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_list_tables`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_describe_table`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_show_create_table`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_count_rows`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_introspect`
Introspect a ClickHouse database to retrieve table structures, columns, and engines
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | ClickHouse server hostname \(e.g., your-instance.clickhouse.cloud\) |
| `port` | number | Yes | ClickHouse HTTP interface port \(8443 for HTTPS, 8123 for HTTP\) |
| `database` | string | Yes | Database name to introspect |
| `username` | string | Yes | ClickHouse username |
| `password` | string | No | ClickHouse password |
| `secure` | boolean | No | Use a secure HTTPS connection \(default: true\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Operation status message |
| `tables` | array | Array of table schemas with columns and engines |
| ↳ `name` | string | Table name |
| ↳ `database` | string | Database the table belongs to |
| ↳ `engine` | string | Table engine \(e.g., MergeTree, Log\) |
| ↳ `totalRows` | number | Approximate total number of rows in the table |
| ↳ `columns` | array | Table columns |
| ↳ `name` | string | Column name |
| ↳ `type` | string | ClickHouse data type \(e.g., UInt32, String, DateTime\) |
| ↳ `defaultKind` | string | Kind of default expression \(DEFAULT, MATERIALIZED, ALIAS\) |
| ↳ `defaultExpression` | string | Default value expression for the column |
| ↳ `isInPrimaryKey` | boolean | Whether the column is part of the primary key |
| ↳ `isInSortingKey` | boolean | Whether the column is part of the sorting key |
### `clickhouse_create_database`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_drop_database`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_create_table`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_drop_table`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_truncate_table`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_rename_table`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_optimize_table`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_list_partitions`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_drop_partition`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_list_mutations`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_list_running_queries`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_kill_query`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_table_stats`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
### `clickhouse_list_clusters`
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message describing the operation outcome |
| `rows` | array | Array of rows returned from the operation |
| `rowCount` | number | Number of rows returned or affected by the operation |
| `count` | number | Row count \(count rows operation\) |
| `ddl` | string | CREATE TABLE statement \(show create table operation\) |
| `tables` | array | Array of table schemas with columns and engines \(introspect operation\) |
+130 -2
View File
@@ -73,8 +73,15 @@ Get the status and details of a Dagster run by its ID.
| `runId` | string | Run ID |
| `jobName` | string | Name of the job this run belongs to |
| `status` | string | Run status \(QUEUED, NOT_STARTED, STARTING, MANAGED, STARTED, SUCCESS, FAILURE, CANCELING, CANCELED\) |
| `mode` | string | Execution mode of the run |
| `startTime` | number | Run start time as Unix timestamp |
| `endTime` | number | Run end time as Unix timestamp |
| `creationTime` | number | Time the run was created as Unix timestamp |
| `updateTime` | number | Time the run was last updated as Unix timestamp |
| `parentRunId` | string | ID of the immediate parent run \(for re-executions\) |
| `rootRunId` | string | ID of the root run in the re-execution group |
| `canTerminate` | boolean | Whether the run can currently be terminated |
| `assetSelection` | json | Asset keys targeted by the run, as slash-joined strings |
| `runConfigYaml` | string | Run configuration as YAML |
| `tags` | json | Run tags as array of \{key, value\} objects |
@@ -108,7 +115,7 @@ Fetch execution event logs for a Dagster run.
### `dagster_list_runs`
List recent Dagster runs, optionally filtered by job name.
List Dagster runs with optional filters by job name, status, and creation-time range, plus cursor pagination.
#### Input
@@ -118,6 +125,9 @@ List recent Dagster runs, optionally filtered by job name.
| `apiKey` | string | No | Dagster+ API token \(leave blank for OSS / self-hosted\) |
| `jobName` | string | No | Filter runs by job name \(optional\) |
| `statuses` | string | No | Comma-separated run statuses to filter by, e.g. "SUCCESS,FAILURE" \(optional\) |
| `createdAfter` | number | No | Only return runs created at or after this Unix timestamp in seconds \(optional\) |
| `createdBefore` | number | No | Only return runs created at or before this Unix timestamp in seconds \(optional\) |
| `cursor` | string | No | Run ID to page after, from a previous response cursor \(optional\) |
| `limit` | number | No | Maximum number of runs to return \(default 20\) |
#### Output
@@ -131,6 +141,8 @@ List recent Dagster runs, optionally filtered by job name.
| ↳ `tags` | json | Run tags as array of \{key, value\} objects |
| ↳ `startTime` | number | Start time as Unix timestamp |
| ↳ `endTime` | number | End time as Unix timestamp |
| `cursor` | string | Run ID of the last returned run — pass as cursor to fetch the next page |
| `hasMore` | boolean | Whether more runs are likely available beyond this page |
### `dagster_list_jobs`
@@ -295,7 +307,7 @@ List all sensors in a Dagster repository, optionally filtered by status.
| --------- | ---- | ----------- |
| `sensors` | json | Array of sensors \(name, sensorType, status, id, description\) |
| ↳ `name` | string | Sensor name |
| ↳ `sensorType` | string | Sensor type \(ASSET, AUTO_MATERIALIZE, FRESHNESS_POLICY, MULTI_ASSET, RUN_STATUS, STANDARD\) |
| ↳ `sensorType` | string | Sensor type \(ASSET, AUTO_MATERIALIZE, FRESHNESS_POLICY, MULTI_ASSET, RUN_STATUS, STANDARD, UNKNOWN\) |
| ↳ `status` | string | Sensor status: RUNNING or STOPPED |
| ↳ `id` | string | Instigator state ID — use this to start or stop the sensor |
| ↳ `description` | string | Human-readable sensor description |
@@ -340,4 +352,120 @@ Disable (stop) a running sensor in Dagster.
| `id` | string | Instigator state ID of the sensor |
| `status` | string | Updated sensor status \(RUNNING or STOPPED\) |
### `dagster_list_assets`
List assets tracked by a Dagster instance, optionally filtered by key prefix.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | Dagster host URL \(e.g., https://myorg.dagster.cloud/prod or http://localhost:3001\) |
| `apiKey` | string | No | Dagster+ API token \(leave blank for OSS / self-hosted\) |
| `prefix` | string | No | Slash-delimited asset key prefix to filter by, e.g. "raw" or "raw/events" \(optional\) |
| `cursor` | string | No | Asset key cursor from a previous response, for pagination \(optional\) |
| `limit` | number | No | Maximum number of assets to return \(optional\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `assets` | json | Array of assets \(assetKey, path\) |
| ↳ `assetKey` | string | Slash-joined asset key |
| ↳ `path` | json | Asset key path segments |
| `cursor` | string | Cursor to pass on the next call to fetch more assets |
| `hasMore` | boolean | Whether more assets are likely available beyond this page |
### `dagster_get_asset`
Get an asset definition and its latest materialization by asset key.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | Dagster host URL \(e.g., https://myorg.dagster.cloud/prod or http://localhost:3001\) |
| `apiKey` | string | No | Dagster+ API token \(leave blank for OSS / self-hosted\) |
| `assetKey` | string | Yes | Slash-delimited asset key, e.g. "my_asset" or "raw/events" |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `assetKey` | string | Slash-joined asset key |
| `path` | json | Asset key path segments |
| `groupName` | string | Asset group the definition belongs to |
| `description` | string | Asset description |
| `jobNames` | json | Names of jobs that can materialize this asset |
| `computeKind` | string | Compute kind tag \(e.g., python, dbt, spark\) |
| `isPartitioned` | boolean | Whether the asset is partitioned |
| `latestMaterialization` | json | Most recent materialization \(runId, timestamp, partition, stepKey\) |
| ↳ `runId` | string | Run that produced the materialization |
| ↳ `timestamp` | string | Materialization timestamp \(epoch ms string\) |
| ↳ `partition` | string | Partition key, if partitioned |
| ↳ `stepKey` | string | Step key that emitted it |
### `dagster_materialize_assets`
Materialize selected assets by launching their asset job with an asset selection.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | Dagster host URL \(e.g., https://myorg.dagster.cloud/prod or http://localhost:3001\) |
| `apiKey` | string | No | Dagster+ API token \(leave blank for OSS / self-hosted\) |
| `repositoryLocationName` | string | Yes | Repository location \(code location\) name |
| `repositoryName` | string | Yes | Repository name within the code location |
| `jobName` | string | Yes | Asset job that contains the assets, e.g. "__ASSET_JOB" or a named asset job |
| `assetSelection` | string | Yes | Comma- or newline-separated asset keys to materialize, each slash-delimited \(e.g. "raw/events, summary"\) |
| `tags` | string | No | Tags as a JSON array of \{key, value\} objects \(optional\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `runId` | string | The globally unique ID of the launched materialization run |
### `dagster_report_asset_materialization`
Report an external (runless) materialization or observation for an asset.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | Dagster host URL \(e.g., https://myorg.dagster.cloud/prod or http://localhost:3001\) |
| `apiKey` | string | No | Dagster+ API token \(leave blank for OSS / self-hosted\) |
| `assetKey` | string | Yes | Slash-delimited asset key to report against, e.g. "my_asset" or "raw/events" |
| `eventType` | string | No | Event type to report: ASSET_MATERIALIZATION \(default\) or ASSET_OBSERVATION |
| `partitionKeys` | string | No | Comma-separated partition keys to report against \(optional\) |
| `description` | string | No | Human-readable description for the reported event \(optional\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the event was reported successfully |
| `assetKey` | string | Slash-joined asset key the event was reported against |
### `dagster_wipe_asset`
DESTRUCTIVE: permanently wipes ALL materialization history (every partition) for an asset. This cannot be undone.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `host` | string | Yes | Dagster host URL \(e.g., https://myorg.dagster.cloud/prod or http://localhost:3001\) |
| `apiKey` | string | No | Dagster+ API token \(leave blank for OSS / self-hosted\) |
| `assetKey` | string | Yes | Slash-delimited asset key to wipe, e.g. "my_asset" or "raw/events" |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the asset was wiped successfully |
| `assetKey` | string | Slash-joined asset key that was wiped |
@@ -27,6 +27,7 @@
"circleback",
"clay",
"clerk",
"clickhouse",
"cloudflare",
"cloudformation",
"cloudwatch",
+105 -2
View File
@@ -1,6 +1,6 @@
---
title: Tinybird
description: Send events and query data with Tinybird
description: Send events, query data, and manage Data Sources with Tinybird
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
@@ -30,7 +30,7 @@ Connect Tinybird to your workflows today to accelerate data-driven features, aut
## Usage Instructions
Interact with Tinybird using the Events API to stream JSON or NDJSON events, or use the Query API to execute SQL queries against Pipes and Data Sources.
Interact with Tinybird: stream JSON or NDJSON events with the Events API, run SQL with the Query API, call published Pipe API Endpoints by name with dynamic parameters, and manage Data Sources by appending from a URL, truncating, or deleting rows by condition.
@@ -77,7 +77,110 @@ Execute SQL queries against Tinybird Pipes and Data Sources using the Query API.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Query result data. For FORMAT JSON: array of objects. For other formats \(CSV, TSV, etc.\): raw text string. |
| `meta` | array | Column metadata for the result set \(only available with FORMAT JSON\) |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Column data type |
| `rows` | number | Number of rows returned \(only available with FORMAT JSON\) |
| `rows_before_limit_at_least` | number | Minimum number of rows there would be without a LIMIT clause \(only available with FORMAT JSON\) |
| `statistics` | json | Query execution statistics - elapsed time, rows read, bytes read \(only available with FORMAT JSON\) |
### `tinybird_query_pipe`
Call a published Tinybird Pipe API Endpoint by name, passing dynamic parameters and receiving structured JSON results.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co\) |
| `pipe` | string | Yes | Name of the published Pipe API Endpoint to call. Example: "top_pages" |
| `parameters` | json | No | Dynamic Pipe parameters as a JSON object, sent as query-string arguments. Example: \{"start_date": "2024-01-01", "limit": 10\} |
| `q` | string | No | Optional SQL to run on top of the Pipe result. Use "_" to reference the Pipe. Example: "SELECT count\(\) FROM _" |
| `token` | string | Yes | Tinybird API Token with PIPE:READ scope |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `data` | json | Pipe result data as an array of row objects |
| `meta` | array | Column metadata for the result set |
| ↳ `name` | string | Column name |
| ↳ `type` | string | Column data type |
| `rows` | number | Number of rows returned |
| `rows_before_limit_at_least` | number | Minimum number of rows there would be without a LIMIT clause |
| `statistics` | json | Query execution statistics - elapsed time, rows read, bytes read |
| ↳ `elapsed` | number | Query execution time in seconds |
| ↳ `rows_read` | number | Number of rows processed |
| ↳ `bytes_read` | number | Number of bytes processed |
### `tinybird_append_datasource`
Append data to a Tinybird Data Source from a remote file URL (CSV, NDJSON, Parquet).
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co\) |
| `datasource` | string | Yes | Name of the existing Data Source to append to. Example: "events_raw" |
| `url` | string | Yes | Publicly accessible URL of the file to append. Example: "https://example.com/data.csv" |
| `format` | string | No | Format of the source file: "csv" \(default\), "ndjson", or "parquet" |
| `token` | string | Yes | Tinybird API Token with DATASOURCES:CREATE scope |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Identifier of the append operation |
| `import_id` | string | Import identifier for the append job |
| `job_id` | string | Job identifier used to poll import status |
| `job_url` | string | URL to query the import job status |
| `status` | string | Initial job status \(e.g., "waiting"\) |
| `job` | json | Full import job details \(kind, id, status, created_at, datasource, ...\) |
| `datasource` | json | Target Data Source metadata \(id, name, ...\) |
### `tinybird_truncate_datasource`
Delete all rows from a Tinybird Data Source.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co\) |
| `datasource` | string | Yes | Name of the Data Source to truncate. Example: "events_raw" |
| `token` | string | Yes | Tinybird API Token with DATASOURCES:CREATE scope |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `truncated` | boolean | Whether the Data Source was truncated successfully |
| `result` | json | Raw response body from the truncate endpoint, if any |
### `tinybird_delete_datasource_rows`
Delete rows from a Tinybird Data Source matching a SQL condition.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `base_url` | string | Yes | Tinybird API base URL \(e.g., https://api.tinybird.co\) |
| `datasource` | string | Yes | Name of the Data Source to delete rows from. Example: "events_raw" |
| `delete_condition` | string | Yes | SQL WHERE-clause condition selecting the rows to delete. Example: "country = \'ES\'" or "event_date &lt; \'2024-01-01\'" |
| `dry_run` | boolean | No | When true, returns how many rows would be deleted without deleting them. Defaults to false. |
| `token` | string | Yes | Tinybird API Token with DATASOURCES:CREATE scope |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Identifier of the delete operation |
| `job_id` | string | Job identifier used to poll delete status |
| `delete_id` | string | Deletion identifier |
| `job_url` | string | URL to query the delete job status |
| `status` | string | Current job status \(e.g., "waiting", "done"\) |
| `job` | json | Full delete job details \(kind, id, status, delete_condition, rows_affected, ...\) |
@@ -31,6 +31,7 @@ import {
CirclebackIcon,
ClayIcon,
ClerkIcon,
ClickHouseIcon,
CloudFormationIcon,
CloudflareIcon,
CloudWatchIcon,
@@ -242,6 +243,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
circleback: CirclebackIcon,
clay: ClayIcon,
clerk: ClerkIcon,
clickhouse: ClickHouseIcon,
cloudflare: CloudflareIcon,
cloudformation: CloudFormationIcon,
cloudwatch: CloudWatchIcon,
@@ -2492,6 +2492,129 @@
"integrationTypes": ["security", "developer-tools"],
"tags": ["identity", "automation"]
},
{
"type": "clickhouse",
"slug": "clickhouse",
"name": "ClickHouse",
"description": "Connect to a ClickHouse database",
"longDescription": "Integrate ClickHouse into the workflow. Query and insert data, manage databases and tables, inspect schemas, monitor mutations and running queries, manage partitions, and execute raw SQL over the ClickHouse HTTP interface.",
"bgColor": "#f9ff69",
"iconName": "ClickHouseIcon",
"docsUrl": "https://docs.sim.ai/tools/clickhouse",
"operations": [
{
"name": "Query (SELECT)",
"description": "Execute a SELECT query on a ClickHouse database"
},
{
"name": "Execute Raw SQL",
"description": "Execute raw SQL (DDL, mutations, or queries) on a ClickHouse database"
},
{
"name": "Insert Row",
"description": "Insert a row into a ClickHouse table"
},
{
"name": "Insert Rows (Bulk)",
"description": "Insert multiple rows into a ClickHouse table"
},
{
"name": "Update Data",
"description": "Update rows in a ClickHouse table via an ALTER TABLE ... UPDATE mutation"
},
{
"name": "Delete Data",
"description": "Delete rows from a ClickHouse table via an ALTER TABLE ... DELETE mutation"
},
{
"name": "List Databases",
"description": "List all databases on a ClickHouse server"
},
{
"name": "List Tables",
"description": "List tables in the connected ClickHouse database"
},
{
"name": "Describe Table",
"description": "Describe the columns of a ClickHouse table"
},
{
"name": "Show Create Table",
"description": "Get the CREATE TABLE statement (DDL) for a ClickHouse table"
},
{
"name": "Count Rows",
"description": "Count rows in a ClickHouse table, optionally filtered"
},
{
"name": "Introspect Schema",
"description": "Introspect a ClickHouse database to retrieve table structures, columns, and engines"
},
{
"name": "Create Database",
"description": "Create a new database on a ClickHouse server"
},
{
"name": "Drop Database",
"description": "Drop a database from a ClickHouse server"
},
{
"name": "Create Table",
"description": "Create a new MergeTree-family table in ClickHouse"
},
{
"name": "Drop Table",
"description": "Drop a table from a ClickHouse database"
},
{
"name": "Truncate Table",
"description": "Remove all rows from a ClickHouse table"
},
{
"name": "Rename Table",
"description": "Rename a ClickHouse table"
},
{
"name": "Optimize Table",
"description": "Trigger a merge of table parts via OPTIMIZE TABLE"
},
{
"name": "List Partitions",
"description": "List active partitions for a ClickHouse table"
},
{
"name": "Drop Partition",
"description": "Drop a partition from a ClickHouse table"
},
{
"name": "List Mutations",
"description": "List mutations (async ALTER UPDATE/DELETE) for the connected database"
},
{
"name": "List Running Queries",
"description": "List currently running queries on a ClickHouse server"
},
{
"name": "Kill Query",
"description": "Kill a running query by its query ID"
},
{
"name": "Table Stats",
"description": "Get row counts and on-disk size for tables in the connected database"
},
{
"name": "List Clusters",
"description": "List configured clusters, shards, and replicas"
}
],
"operationCount": 26,
"triggers": [],
"triggerCount": 0,
"authType": "none",
"category": "tools",
"integrationTypes": ["databases", "analytics"],
"tags": ["data-warehouse", "data-analytics"]
},
{
"type": "cloudflare",
"slug": "cloudflare",
@@ -3110,7 +3233,7 @@
},
{
"name": "List Runs",
"description": "List recent Dagster runs, optionally filtered by job name."
"description": "List Dagster runs with optional filters by job name, status, and creation-time range, plus cursor pagination."
},
{
"name": "List Jobs",
@@ -3151,9 +3274,29 @@
{
"name": "Stop Sensor",
"description": "Disable (stop) a running sensor in Dagster."
},
{
"name": "List Assets",
"description": "List assets tracked by a Dagster instance, optionally filtered by key prefix."
},
{
"name": "Get Asset",
"description": "Get an asset definition and its latest materialization by asset key."
},
{
"name": "Materialize Assets",
"description": "Materialize selected assets by launching their asset job with an asset selection."
},
{
"name": "Report Asset Materialization",
"description": "Report an external (runless) materialization or observation for an asset."
},
{
"name": "Wipe Asset",
"description": "DESTRUCTIVE: permanently wipes ALL materialization history (every partition) for an asset. This cannot be undone."
}
],
"operationCount": 14,
"operationCount": 19,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
@@ -14219,11 +14362,11 @@
"type": "tinybird",
"slug": "tinybird",
"name": "Tinybird",
"description": "Send events and query data with Tinybird",
"longDescription": "Interact with Tinybird using the Events API to stream JSON or NDJSON events, or use the Query API to execute SQL queries against Pipes and Data Sources.",
"description": "Send events, query data, and manage Data Sources with Tinybird",
"longDescription": "Interact with Tinybird: stream JSON or NDJSON events with the Events API, run SQL with the Query API, call published Pipe API Endpoints by name with dynamic parameters, and manage Data Sources by appending from a URL, truncating, or deleting rows by condition.",
"bgColor": "#2EF598",
"iconName": "TinybirdIcon",
"docsUrl": "https://www.tinybird.co/docs/api-reference",
"docsUrl": "https://docs.sim.ai/tools/tinybird",
"operations": [
{
"name": "Send Events",
@@ -14232,9 +14375,25 @@
{
"name": "Query",
"description": "Execute SQL queries against Tinybird Pipes and Data Sources using the Query API."
},
{
"name": "Query Pipe Endpoint",
"description": "Call a published Tinybird Pipe API Endpoint by name, passing dynamic parameters and receiving structured JSON results."
},
{
"name": "Append Data Source (from URL)",
"description": "Append data to a Tinybird Data Source from a remote file URL (CSV, NDJSON, Parquet)."
},
{
"name": "Truncate Data Source",
"description": "Delete all rows from a Tinybird Data Source."
},
{
"name": "Delete Data Source Rows",
"description": "Delete rows from a Tinybird Data Source matching a SQL condition."
}
],
"operationCount": 2,
"operationCount": 6,
"triggers": [],
"triggerCount": 0,
"authType": "none",
@@ -0,0 +1,42 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseCountRowsContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseCountRows } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseCountRowsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse count rows attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseCountRowsContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const count = await executeClickHouseCountRows(params, params.table, params.where)
return NextResponse.json({
message: `Table contains ${count} row(s).`,
count,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse count rows failed:`, error)
return NextResponse.json(
{ error: `ClickHouse count rows failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseCreateDatabaseContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseCreateDatabase } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseCreateDatabaseAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse create database attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseCreateDatabaseContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseCreateDatabase(params, params.name)
return NextResponse.json({
message: `Database '${params.name}' created.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse create database failed:`, error)
return NextResponse.json(
{ error: `ClickHouse create database failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,50 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseCreateTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseCreateTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseCreateTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse create table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseCreateTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseCreateTable(
params,
params.table,
params.columns,
params.engine,
params.orderBy,
params.partitionBy
)
return NextResponse.json({
message: `Table '${params.table}' created.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse create table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse create table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,49 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDeleteContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDelete } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDeleteAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse delete attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDeleteContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Deleting data from ${params.table} on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseDelete(params, params.table, params.where)
logger.info(`[${requestId}] Delete mutation submitted, ${result.rowCount} row(s) affected`)
return NextResponse.json({
message: `Delete mutation submitted. ClickHouse mutations run asynchronously. ${result.rowCount} row(s) affected.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse delete failed:`, error)
return NextResponse.json(
{ error: `ClickHouse delete failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDescribeTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDescribeTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDescribeTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse describe table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDescribeTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseDescribeTable(params, params.table)
return NextResponse.json({
message: `Described table with ${result.rowCount} column(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse describe table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse describe table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDropDatabaseContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDropDatabase } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDropDatabaseAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse drop database attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDropDatabaseContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseDropDatabase(params, params.name)
return NextResponse.json({
message: `Database '${params.name}' dropped.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse drop database failed:`, error)
return NextResponse.json(
{ error: `ClickHouse drop database failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDropPartitionContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDropPartition } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDropPartitionAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse drop partition attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDropPartitionContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseDropPartition(params, params.table, params.partition)
return NextResponse.json({
message: `Dropped partition from table '${params.table}'.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse drop partition failed:`, error)
return NextResponse.json(
{ error: `ClickHouse drop partition failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDropTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDropTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDropTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse drop table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDropTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseDropTable(params, params.table)
return NextResponse.json({
message: `Table '${params.table}' dropped.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse drop table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse drop table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,49 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseExecuteContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseQuery } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseExecuteAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse execute attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseExecuteContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Executing ClickHouse statement on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseQuery(params, params.query)
logger.info(`[${requestId}] Statement executed successfully, ${result.rowCount} row(s)`)
return NextResponse.json({
message: `Statement executed successfully. ${result.rowCount} row(s) returned or affected.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse execute failed:`, error)
return NextResponse.json(
{ error: `ClickHouse execute failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseInsertRowsContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseInsertRows } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseInsertRowsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse insert rows attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseInsertRowsContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseInsertRows(params, params.table, params.rows)
return NextResponse.json({
message: `Inserted ${result.rowCount} row(s) into '${params.table}'.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse insert rows failed:`, error)
return NextResponse.json(
{ error: `ClickHouse insert rows failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,49 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseInsertContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseInsert } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseInsertAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse insert attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseInsertContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Inserting data into ${params.table} on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseInsert(params, params.table, params.data)
logger.info(`[${requestId}] Insert executed successfully, ${result.rowCount} row(s) inserted`)
return NextResponse.json({
message: `Data inserted successfully. ${result.rowCount} row(s) affected.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse insert failed:`, error)
return NextResponse.json(
{ error: `ClickHouse insert failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,50 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseIntrospectContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseIntrospect } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseIntrospectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse introspect attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseIntrospectContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Introspecting ClickHouse schema on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseIntrospect(params)
logger.info(
`[${requestId}] Introspection completed successfully, found ${result.tables.length} tables`
)
return NextResponse.json({
message: `Schema introspection completed. Found ${result.tables.length} table(s) in database '${params.database}'.`,
tables: result.tables,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse introspection failed:`, error)
return NextResponse.json(
{ error: `ClickHouse introspection failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseKillQueryContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseKillQuery } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseKillQueryAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse kill query attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseKillQueryContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseKillQuery(params, params.queryId)
return NextResponse.json({
message: `Kill command executed for query '${params.queryId}'.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse kill query failed:`, error)
return NextResponse.json(
{ error: `ClickHouse kill query failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListClustersContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListClusters } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListClustersAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list clusters attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListClustersContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListClusters(params)
return NextResponse.json({
message: `Found ${result.rowCount} cluster node(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list clusters failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list clusters failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListDatabasesContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListDatabases } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListDatabasesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list databases attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListDatabasesContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListDatabases(params)
return NextResponse.json({
message: `Found ${result.rowCount} database(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list databases failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list databases failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListMutationsContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListMutations } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListMutationsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list mutations attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListMutationsContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListMutations(params, params.table, params.onlyRunning)
return NextResponse.json({
message: `Found ${result.rowCount} mutation(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list mutations failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list mutations failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListPartitionsContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListPartitions } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListPartitionsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list partitions attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListPartitionsContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListPartitions(params, params.table)
return NextResponse.json({
message: `Found ${result.rowCount} partition(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list partitions failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list partitions failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListRunningQueriesContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListRunningQueries } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListRunningQueriesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list running queries attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListRunningQueriesContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListRunningQueries(params)
return NextResponse.json({
message: `Found ${result.rowCount} running query(ies).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list running queries failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list running queries failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListTablesContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListTables } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListTablesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list tables attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListTablesContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListTables(params)
return NextResponse.json({
message: `Found ${result.rowCount} table(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list tables failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list tables failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseOptimizeTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseOptimizeTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseOptimizeTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse optimize table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseOptimizeTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseOptimizeTable(params, params.table, params.final)
return NextResponse.json({
message: `Optimize submitted for table '${params.table}'.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse optimize table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse optimize table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,46 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseQueryContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseQuery } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseQueryAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse query attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseQueryContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Executing ClickHouse query on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseQuery(params, params.query, { enforceReadOnly: true })
logger.info(`[${requestId}] Query executed successfully, returned ${result.rowCount} rows`)
return NextResponse.json({
message: `Query executed successfully. ${result.rowCount} row(s) returned.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse query failed:`, error)
return NextResponse.json({ error: `ClickHouse query failed: ${errorMessage}` }, { status: 500 })
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseRenameTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseRenameTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseRenameTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse rename table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseRenameTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseRenameTable(params, params.table, params.newTable)
return NextResponse.json({
message: `Renamed table '${params.table}' to '${params.newTable}'.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse rename table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse rename table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,42 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseShowCreateTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseShowCreateTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseShowCreateTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse show create table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseShowCreateTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const ddl = await executeClickHouseShowCreateTable(params, params.table)
return NextResponse.json({
message: 'Retrieved CREATE statement.',
ddl,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse show create table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse show create table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseTableStatsContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseTableStats } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseTableStatsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse table stats attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseTableStatsContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseTableStats(params, params.table)
return NextResponse.json({
message: `Retrieved stats for ${result.rowCount} table(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse table stats failed:`, error)
return NextResponse.json(
{ error: `ClickHouse table stats failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseTruncateTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseTruncateTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseTruncateTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse truncate table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseTruncateTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseTruncateTable(params, params.table)
return NextResponse.json({
message: `Table '${params.table}' truncated.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse truncate table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse truncate table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,49 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseUpdateContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseUpdate } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseUpdateAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse update attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseUpdateContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Updating data in ${params.table} on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseUpdate(params, params.table, params.data, params.where)
logger.info(`[${requestId}] Update mutation submitted, ${result.rowCount} row(s) written`)
return NextResponse.json({
message: `Update mutation submitted. ClickHouse mutations run asynchronously. ${result.rowCount} row(s) written.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse update failed:`, error)
return NextResponse.json(
{ error: `ClickHouse update failed: ${errorMessage}` },
{ status: 500 }
)
}
})
+863
View File
@@ -0,0 +1,863 @@
import { validateDatabaseHost } from '@/lib/core/security/input-validation.server'
import type { ClickHouseConnectionConfig } from '@/tools/clickhouse/types'
const REQUEST_TIMEOUT_MS = 30_000
interface ClickHouseSummary {
read_rows?: string
written_rows?: string
result_rows?: string
}
interface ClickHouseHttpResult {
text: string
summary: ClickHouseSummary | null
}
export interface ClickHouseRowsResult {
rows: unknown[]
rowCount: number
}
interface ClickHouseColumnRow {
table: string
name: string
type: string
default_kind?: string
default_expression?: string
is_in_primary_key?: number | string
is_in_sorting_key?: number | string
position?: number | string
}
interface ClickHouseTableRow {
name: string
engine?: string
total_rows?: number | string | null
}
export interface ClickHouseIntrospectionResult {
tables: Array<{
name: string
database: string
engine: string
totalRows?: number
columns: Array<{
name: string
type: string
defaultKind?: string
defaultExpression?: string
isInPrimaryKey: boolean
isInSortingKey: boolean
}>
}>
}
/**
* Sends a single statement to the ClickHouse HTTP interface and returns the raw
* response body alongside the parsed `X-ClickHouse-Summary` header.
* @see https://clickhouse.com/docs/interfaces/http
*/
async function clickhouseRequest(
config: ClickHouseConnectionConfig,
statement: string
): Promise<ClickHouseHttpResult> {
const hostValidation = await validateDatabaseHost(config.host, 'host')
if (!hostValidation.isValid) {
throw new Error(hostValidation.error)
}
const protocol = config.secure ? 'https' : 'http'
const url = new URL(`${protocol}://${config.host}:${config.port}/`)
url.searchParams.set('database', config.database)
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
let response: Response
try {
response = await fetch(url.toString(), {
method: 'POST',
headers: {
'X-ClickHouse-User': config.username,
'X-ClickHouse-Key': config.password,
'Content-Type': 'text/plain; charset=utf-8',
},
body: statement,
signal: controller.signal,
})
} finally {
clearTimeout(timeout)
}
const text = await response.text()
if (!response.ok) {
throw new Error(text.trim() || `ClickHouse request failed with status ${response.status}`)
}
return { text, summary: parseSummary(response.headers.get('x-clickhouse-summary')) }
}
function parseSummary(header: string | null): ClickHouseSummary | null {
if (!header) return null
try {
return JSON.parse(header) as ClickHouseSummary
} catch {
return null
}
}
/**
* Parses a ClickHouse `FORMAT JSON` response body into rows, falling back to the
* summary header's row counts for statements that do not return a result set.
*/
function parseRowsResult(result: ClickHouseHttpResult): ClickHouseRowsResult {
const trimmed = result.text.trim()
if (trimmed) {
try {
const parsed = JSON.parse(trimmed) as { data?: unknown[]; rows?: number }
if (parsed && Array.isArray(parsed.data)) {
const rowCount = typeof parsed.rows === 'number' ? parsed.rows : parsed.data.length
return { rows: parsed.data, rowCount }
}
} catch {
// Body was not JSON (e.g. a non-SELECT statement); fall through to summary.
}
}
const written = Number(result.summary?.written_rows ?? 0)
const read = Number(result.summary?.read_rows ?? 0)
return { rows: [], rowCount: written || read || 0 }
}
/** Read-only statement leaders that return a result set and never mutate data. */
const READ_ONLY_STATEMENT = /^(select|with|show|describe|desc|explain|exists)\b/i
/**
* Normalizes the output format of a read statement to JSON so the HTTP response
* can always be parsed into rows. Strips every `FORMAT <name>` clause — wherever
* it sits relative to a trailing `SETTINGS` clause — and appends a single canonical
* `FORMAT JSON`. The `format()` function and `FORMAT`/format names appearing inside
* strings or comments are ignored (the scan runs on comment/string-masked SQL).
* Non-read statements are returned untouched (their own FORMAT, e.g. JSONEachRow
* for inserts, is preserved).
*/
function ensureJsonFormat(query: string): string {
const trimmed = query.trim().replace(/;+\s*$/, '')
if (!READ_ONLY_STATEMENT.test(trimmed)) {
return trimmed
}
const masked = maskSqlNoise(trimmed)
const formatClause = /\bformat\s+[a-z0-9_]+\b/gi
const spans: Array<[number, number]> = []
for (let match = formatClause.exec(masked); match !== null; match = formatClause.exec(masked)) {
spans.push([match.index, match.index + match[0].length])
}
let result = trimmed
for (let i = spans.length - 1; i >= 0; i--) {
result = result.slice(0, spans[i][0]) + result.slice(spans[i][1])
}
return `${result.replace(/\s+$/, '')}\nFORMAT JSON`
}
/**
* Replaces string literals ('...'), quoted identifiers ("..." / `...`), and SQL
* comments (`-- …` and `/* … *​/`) with spaces so that structural scans (e.g. for
* statement-chaining semicolons) only see actual SQL code, not data or comments.
*/
function maskSqlNoise(sql: string): string {
let out = ''
let i = 0
while (i < sql.length) {
const ch = sql[i]
if (ch === "'" || ch === '"' || ch === '`') {
out += ' '
i++
while (i < sql.length && sql[i] !== ch) {
if (ch !== '`' && sql[i] === '\\') {
out += ' '
i += 2
continue
}
out += ' '
i++
}
if (i < sql.length) {
out += ' '
i++
}
continue
}
if (ch === '-' && sql[i + 1] === '-') {
const newline = sql.indexOf('\n', i + 2)
const end = newline === -1 ? sql.length : newline
out += ' '.repeat(end - i)
i = end
continue
}
if (ch === '/' && sql[i + 1] === '*') {
const close = sql.indexOf('*/', i + 2)
const end = close === -1 ? sql.length : close + 2
out += ' '.repeat(end - i)
i = end
continue
}
out += ch
i++
}
return out
}
/**
* Detects whether a statement chains a second statement after a `;`, ignoring
* semicolons inside string literals, quoted identifiers, and comments. A trailing
* semicolon (with only whitespace/comments after it) is allowed.
*/
function hasChainedStatement(sql: string): boolean {
return /;\s*\S/.test(maskSqlNoise(sql))
}
/**
* Write/DDL statement shapes that must never run under the read-only query
* operation, even when wrapped by a leading `WITH` CTE (e.g. `WITH … INSERT INTO …`).
* Patterns require the keyword's statement context (e.g. `insert into`, `alter table`)
* so SQL functions/columns like `truncate(x)` or `created_at` are not false-positives.
*/
const MUTATING_STATEMENT = [
/\binsert\s+into\b/i,
/\bdelete\s+from\b/i,
/\bupdate\s+[\w.`"]+\s+set\b/i,
/\balter\s+table\b/i,
/\b(?:create|attach)\s+(?:or\s+replace\s+)?(?:temporary\s+)?(?:table|database|dictionary|view|materialized\s+view|live\s+view|function|user|role)\b/i,
/\bdrop\s+(?:table|database|dictionary|view|column|partition|index|function|user|role)\b/i,
/\btruncate\s+table\b/i,
/\brename\s+(?:table|database|dictionary)\b/i,
/\bdetach\s+(?:table|database|dictionary|view|permanently)\b/i,
/\b(?:grant|revoke)\b/i,
/\boptimize\s+table\b/i,
]
/** Whether a statement performs a write/DDL anywhere (comments and strings masked out). */
function isMutatingStatement(sql: string): boolean {
const masked = maskSqlNoise(sql)
return MUTATING_STATEMENT.some((pattern) => pattern.test(masked))
}
/**
* Strips leading whitespace, `--`/`/* … *​/` comments, and opening parens from a
* statement so the read-only leader keyword can be detected even when a query
* starts with a comment (e.g. `-- note\nSELECT …`) or wrapping parens.
*/
function stripLeadingNoise(sql: string): string {
let s = sql.trim()
for (;;) {
if (s.startsWith('--')) {
const newline = s.indexOf('\n')
s = (newline === -1 ? '' : s.slice(newline + 1)).trim()
} else if (s.startsWith('/*')) {
const close = s.indexOf('*/')
s = (close === -1 ? '' : s.slice(close + 2)).trim()
} else if (s.startsWith('(')) {
s = s.slice(1).trim()
} else {
return s
}
}
}
export async function executeClickHouseQuery(
config: ClickHouseConnectionConfig,
query: string,
options: { enforceReadOnly?: boolean } = {}
): Promise<ClickHouseRowsResult> {
if (options.enforceReadOnly) {
// Strip leading comments/parens so wrapped or commented selects still validate.
const leader = stripLeadingNoise(query)
if (!READ_ONLY_STATEMENT.test(leader)) {
throw new Error(
'The query operation only allows read-only statements (SELECT, WITH, SHOW, DESCRIBE, EXPLAIN, EXISTS). Use the Execute Raw SQL operation to run writes or DDL.'
)
}
if (hasChainedStatement(query)) {
throw new Error(
'The query operation only allows a single statement; chained statements separated by ";" are not allowed. Use the Execute Raw SQL operation to run multiple statements.'
)
}
if (isMutatingStatement(query)) {
throw new Error(
'The query operation only allows read-only statements; a write or DDL statement (e.g. INSERT/ALTER/DROP, including after a WITH clause) was detected. Use the Execute Raw SQL operation instead.'
)
}
}
const result = await clickhouseRequest(config, ensureJsonFormat(query))
return parseRowsResult(result)
}
export async function executeClickHouseInsert(
config: ClickHouseConnectionConfig,
table: string,
data: Record<string, unknown>
): Promise<ClickHouseRowsResult> {
const sanitizedTable = sanitizeIdentifier(table)
const statement = `INSERT INTO ${sanitizedTable} FORMAT JSONEachRow\n${JSON.stringify(data)}`
const result = await clickhouseRequest(config, statement)
const written = Number(result.summary?.written_rows ?? 0)
return { rows: [], rowCount: written || 1 }
}
export async function executeClickHouseUpdate(
config: ClickHouseConnectionConfig,
table: string,
data: Record<string, unknown>,
where: string
): Promise<ClickHouseRowsResult> {
validateWhereClause(where)
const sanitizedTable = sanitizeIdentifier(table)
const assignments = Object.entries(data)
.map(([column, value]) => `${sanitizeIdentifier(column)} = ${formatValue(value)}`)
.join(', ')
if (!assignments) {
throw new Error('Update data object cannot be empty')
}
const statement = `ALTER TABLE ${sanitizedTable} UPDATE ${assignments} WHERE ${where}`
const result = await clickhouseRequest(config, statement)
return { rows: [], rowCount: Number(result.summary?.written_rows ?? 0) }
}
export async function executeClickHouseDelete(
config: ClickHouseConnectionConfig,
table: string,
where: string
): Promise<ClickHouseRowsResult> {
validateWhereClause(where)
const sanitizedTable = sanitizeIdentifier(table)
const statement = `ALTER TABLE ${sanitizedTable} DELETE WHERE ${where}`
const result = await clickhouseRequest(config, statement)
return { rows: [], rowCount: Number(result.summary?.written_rows ?? 0) }
}
export async function executeClickHouseIntrospect(
config: ClickHouseConnectionConfig
): Promise<ClickHouseIntrospectionResult> {
const database = quoteString(config.database)
const tablesResult = await clickhouseRequest(
config,
`SELECT name, engine, total_rows FROM system.tables WHERE database = ${database} ORDER BY name FORMAT JSON`
)
const tableRows = parseDataArray<ClickHouseTableRow>(tablesResult.text)
const columnsResult = await clickhouseRequest(
config,
`SELECT table, name, type, default_kind, default_expression, is_in_primary_key, is_in_sorting_key, position FROM system.columns WHERE database = ${database} ORDER BY table, position FORMAT JSON`
)
const columnRows = parseDataArray<ClickHouseColumnRow>(columnsResult.text)
const columnsByTable = new Map<
string,
ClickHouseIntrospectionResult['tables'][number]['columns']
>()
for (const column of columnRows) {
const columns = columnsByTable.get(column.table) ?? []
columns.push({
name: column.name,
type: column.type,
defaultKind: column.default_kind || undefined,
defaultExpression: column.default_expression || undefined,
isInPrimaryKey: toBoolean(column.is_in_primary_key),
isInSortingKey: toBoolean(column.is_in_sorting_key),
})
columnsByTable.set(column.table, columns)
}
const tables = tableRows.map((table) => ({
name: table.name,
database: config.database,
engine: table.engine ?? '',
totalRows: table.total_rows != null ? Number(table.total_rows) : undefined,
columns: columnsByTable.get(table.name) ?? [],
}))
return { tables }
}
function parseDataArray<T>(text: string): T[] {
const trimmed = text.trim()
if (!trimmed) return []
try {
const parsed = JSON.parse(trimmed) as { data?: T[] }
return Array.isArray(parsed.data) ? parsed.data : []
} catch {
return []
}
}
function toBoolean(value: number | string | undefined): boolean {
return value === 1 || value === '1'
}
/**
* Quotes and escapes a value for inline use in a ClickHouse statement.
* Strings use ClickHouse's backslash escaping for single quotes and backslashes.
*/
function formatValue(value: unknown): string {
if (value === null || value === undefined) {
return 'NULL'
}
if (typeof value === 'number') {
return Number.isFinite(value) ? String(value) : 'NULL'
}
if (typeof value === 'boolean') {
return value ? '1' : '0'
}
if (typeof value === 'object') {
return quoteString(JSON.stringify(value))
}
return quoteString(String(value))
}
function quoteString(value: string): string {
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
}
/**
* Validates and backtick-quotes a ClickHouse identifier, supporting
* `database.table` qualified names.
*/
export function sanitizeIdentifier(identifier: string): string {
if (identifier.includes('.')) {
return identifier
.split('.')
.map((part) => sanitizeSingleIdentifier(part))
.join('.')
}
return sanitizeSingleIdentifier(identifier)
}
function sanitizeSingleIdentifier(identifier: string): string {
const cleaned = identifier.replace(/`/g, '')
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(cleaned)) {
throw new Error(
`Invalid identifier: ${identifier}. Identifiers must start with a letter or underscore and contain only letters, numbers, and underscores.`
)
}
return `\`${cleaned}\``
}
/**
* Rejects WHERE clauses containing patterns commonly used in SQL injection so
* that user-supplied conditions cannot escape the intended mutation.
*/
function validateWhereClause(where: string): void {
const dangerousPatterns = [
/;\s*(drop|delete|insert|alter|create|truncate|rename|grant|revoke)/i,
/union\s+(all\s+)?select/i,
/into\s+outfile/i,
/--/,
/\/\*/,
/\*\//,
/\bor\s+(['"]?)(\w+)\1\s*=\s*\1\2\1/i,
/\bor\s+true\b/i,
/\bor\s+false\b/i,
/\band\s+(['"]?)(\w+)\1\s*=\s*\1\2\1/i,
/\band\s+true\b/i,
/\band\s+false\b/i,
/\bsleep\s*\(/i,
/;\s*\w+/,
// Constant / tautological conditions that don't reference columns and would
// broaden a mutation to all rows (e.g. "1=1", "1 < 2", "'a'='a'", bare "1"/"true").
/\b\d+\s*(?:=|==|<>|!=|<=|>=|<|>)\s*\d+\b/,
/(['"])([^'"]*)\1\s*(?:=|==|<>|!=)\s*\1\2\1/,
/\b(\w+)\s*=\s*\1\b/i,
/^\s*(?:\d+|true|false)\s*$/i,
]
for (const pattern of dangerousPatterns) {
if (pattern.test(where)) {
throw new Error('WHERE clause contains potentially dangerous operation')
}
}
}
/**
* Runs a SELECT statement (which must already include `FORMAT JSON`) and returns
* the parsed rows and row count.
*/
async function runSelect(
config: ClickHouseConnectionConfig,
statement: string
): Promise<ClickHouseRowsResult> {
const result = await clickhouseRequest(config, statement)
return parseRowsResult(result)
}
/**
* Runs a statement that does not return a result set (DDL or mutation) and
* returns the number of written rows reported by the summary header.
*/
async function runStatement(
config: ClickHouseConnectionConfig,
statement: string
): Promise<number> {
const result = await clickhouseRequest(config, statement)
return Number(result.summary?.written_rows ?? 0)
}
/**
* Validates a free-form SQL expression (ORDER BY, PARTITION BY, engine args)
* rejecting statement terminators and comment sequences.
*/
function validateExpression(expression: string, label: string): void {
if (/;|--|\/\*|\*\//.test(expression)) {
throw new Error(`${label} contains a disallowed character`)
}
}
/**
* Validates an ORDER BY / PARTITION BY expression that is spliced inside wrapping
* parentheses in the generated DDL. In addition to rejecting terminators/comments,
* it requires balanced parentheses (quote-aware) so the expression cannot close
* the wrapping `(...)` early and append extra clauses (e.g. `id) SETTINGS …`).
*/
function validateClauseExpression(expression: string, label: string): void {
const trimmed = expression.trim()
if (!trimmed) {
throw new Error(`${label} is required`)
}
if (/;|--|\/\*|\*\//.test(trimmed)) {
throw new Error(`${label} contains a disallowed sequence`)
}
let depth = 0
let inString = false
for (let i = 0; i < trimmed.length; i++) {
const ch = trimmed[i]
if (inString) {
if (ch === '\\') i++
else if (ch === "'") inString = false
continue
}
if (ch === "'") inString = true
else if (ch === '(') depth++
else if (ch === ')') {
depth--
if (depth < 0) {
throw new Error(`${label} has unbalanced parentheses`)
}
}
}
if (inString || depth !== 0) {
throw new Error(`${label} has unbalanced parentheses or quotes`)
}
}
/**
* Validates a partition value for `DROP PARTITION`. ClickHouse partition values
* are literals (signed numbers or single-quoted strings) or a parenthesised tuple
* of such literals, so anything else is rejected — barewords like `ALL`, function
* calls, operators, and extra tokens that could broaden the statement beyond
* dropping a single partition.
*/
function validatePartitionExpression(partition: string): void {
const partitionPattern =
/^\(?\s*(?:'(?:[^'\\]|\\.)*'|-?\d+(?:\.\d+)?)(?:\s*,\s*(?:'(?:[^'\\]|\\.)*'|-?\d+(?:\.\d+)?))*\s*\)?$/
if (!partitionPattern.test(partition.trim())) {
throw new Error(
"Partition must be a literal value or a tuple of literals (number or single-quoted string), e.g. 202401, '2024-01', or (2024, 'EU')"
)
}
}
export function executeClickHouseListDatabases(
config: ClickHouseConnectionConfig
): Promise<ClickHouseRowsResult> {
return runSelect(
config,
'SELECT name, engine, comment FROM system.databases ORDER BY name FORMAT JSON'
)
}
export function executeClickHouseListTables(
config: ClickHouseConnectionConfig
): Promise<ClickHouseRowsResult> {
return runSelect(
config,
`SELECT name, engine, total_rows AS totalRows, total_bytes AS totalBytes, comment FROM system.tables WHERE database = ${quoteString(config.database)} ORDER BY name FORMAT JSON`
)
}
export function executeClickHouseDescribeTable(
config: ClickHouseConnectionConfig,
table: string
): Promise<ClickHouseRowsResult> {
const tableName = stripDatabasePrefix(table)
return runSelect(
config,
`SELECT name, type, default_kind AS defaultKind, default_expression AS defaultExpression, comment, is_in_primary_key AS isInPrimaryKey, is_in_sorting_key AS isInSortingKey FROM system.columns WHERE database = ${quoteString(config.database)} AND table = ${quoteString(tableName)} ORDER BY position FORMAT JSON`
)
}
export async function executeClickHouseShowCreateTable(
config: ClickHouseConnectionConfig,
table: string
): Promise<string> {
const result = await runSelect(
config,
`SHOW CREATE TABLE ${sanitizeIdentifier(table)} FORMAT JSON`
)
const firstRow = result.rows[0] as Record<string, unknown> | undefined
if (!firstRow) {
return ''
}
// ClickHouse returns the DDL in a single String column (named `statement`);
// fall back to the first column value to stay robust to column-name changes.
const value = firstRow.statement ?? Object.values(firstRow)[0]
return typeof value === 'string' ? value : ''
}
export async function executeClickHouseCountRows(
config: ClickHouseConnectionConfig,
table: string,
where?: string
): Promise<number> {
let statement = `SELECT count() AS count FROM ${sanitizeIdentifier(table)}`
if (where?.trim()) {
validateWhereClause(where)
statement += ` WHERE ${where}`
}
const result = await runSelect(config, `${statement} FORMAT JSON`)
const firstRow = result.rows[0] as { count?: number | string } | undefined
return firstRow?.count != null ? Number(firstRow.count) : 0
}
export function executeClickHouseListPartitions(
config: ClickHouseConnectionConfig,
table: string
): Promise<ClickHouseRowsResult> {
const tableName = stripDatabasePrefix(table)
return runSelect(
config,
`SELECT partition, count() AS parts, sum(rows) AS rows, sum(bytes_on_disk) AS bytesOnDisk FROM system.parts WHERE database = ${quoteString(config.database)} AND table = ${quoteString(tableName)} AND active GROUP BY partition ORDER BY partition FORMAT JSON`
)
}
export function executeClickHouseListMutations(
config: ClickHouseConnectionConfig,
table?: string,
onlyRunning = false
): Promise<ClickHouseRowsResult> {
const filters = [`database = ${quoteString(config.database)}`]
if (table?.trim()) {
filters.push(`table = ${quoteString(stripDatabasePrefix(table))}`)
}
if (onlyRunning) {
filters.push('is_done = 0')
}
return runSelect(
config,
`SELECT table, mutation_id AS mutationId, command, create_time AS createTime, is_done AS isDone, parts_to_do AS partsToDo, latest_fail_reason AS latestFailReason FROM system.mutations WHERE ${filters.join(' AND ')} ORDER BY create_time DESC FORMAT JSON`
)
}
export function executeClickHouseListRunningQueries(
config: ClickHouseConnectionConfig
): Promise<ClickHouseRowsResult> {
return runSelect(
config,
'SELECT query_id AS queryId, user, toFloat64(elapsed) AS elapsedSeconds, formatReadableSize(memory_usage) AS memoryUsage, query FROM system.processes ORDER BY elapsed DESC FORMAT JSON'
)
}
export function executeClickHouseTableStats(
config: ClickHouseConnectionConfig,
table?: string
): Promise<ClickHouseRowsResult> {
const filters = ['active', `database = ${quoteString(config.database)}`]
if (table?.trim()) {
filters.push(`table = ${quoteString(stripDatabasePrefix(table))}`)
}
return runSelect(
config,
`SELECT database, table, sum(rows) AS rows, sum(bytes_on_disk) AS bytesOnDisk, formatReadableSize(sum(bytes_on_disk)) AS sizeOnDisk, count() AS parts FROM system.parts WHERE ${filters.join(' AND ')} GROUP BY database, table ORDER BY sum(bytes_on_disk) DESC FORMAT JSON`
)
}
export function executeClickHouseListClusters(
config: ClickHouseConnectionConfig
): Promise<ClickHouseRowsResult> {
return runSelect(
config,
'SELECT cluster, shard_num AS shardNum, replica_num AS replicaNum, host_name AS hostName, port, is_local AS isLocal FROM system.clusters ORDER BY cluster, shard_num, replica_num FORMAT JSON'
)
}
export async function executeClickHouseCreateDatabase(
config: ClickHouseConnectionConfig,
name: string
): Promise<void> {
await clickhouseRequest(config, `CREATE DATABASE IF NOT EXISTS ${sanitizeIdentifier(name)}`)
}
export async function executeClickHouseDropDatabase(
config: ClickHouseConnectionConfig,
name: string
): Promise<void> {
await clickhouseRequest(config, `DROP DATABASE IF EXISTS ${sanitizeIdentifier(name)}`)
}
/**
* Validates a single ClickHouse column type. Types may legitimately contain
* commas, single-quoted strings, `=`, and `-` inside their parameter parentheses
* (e.g. `Decimal(10, 2)`, `Enum8('a' = 1, 'b' = -2)`, `Map(String, UInt64)`,
* `Array(Tuple(a UInt8, b String))`). We allow those but reject anything that
* could break out of the single type literal and inject another column or SQL:
* comment/terminator sequences, a top-level (unparenthesised) comma, or an
* unbalanced closing paren.
*/
function validateColumnType(type: string): void {
const trimmed = type.trim()
if (!trimmed || !/^[A-Za-z_]/.test(trimmed)) {
throw new Error(`Invalid column type: ${type}`)
}
if (!/^[A-Za-z0-9_(),.\s'"=-]+$/.test(trimmed) || /--|;/.test(trimmed)) {
throw new Error(`Invalid column type: ${type}`)
}
let depth = 0
let inString = false
for (let i = 0; i < trimmed.length; i++) {
const ch = trimmed[i]
if (inString) {
if (ch === '\\') i++
else if (ch === "'") inString = false
continue
}
if (ch === "'") inString = true
else if (ch === '(') depth++
else if (ch === ')') {
depth--
if (depth < 0) throw new Error(`Invalid column type: ${type}`)
} else if (ch === ',' && depth === 0) {
throw new Error(`Invalid column type: ${type}`)
}
}
if (inString || depth !== 0) {
throw new Error(`Invalid column type: ${type}`)
}
}
export async function executeClickHouseCreateTable(
config: ClickHouseConnectionConfig,
table: string,
columns: Array<{ name: string; type: string }>,
engine: string,
orderBy: string,
partitionBy?: string
): Promise<void> {
if (!Array.isArray(columns) || columns.length === 0) {
throw new Error('At least one column definition is required')
}
const columnDefs = columns.map((column) => {
if (!column?.name || !column?.type) {
throw new Error('Each column requires a name and type')
}
validateColumnType(column.type)
return `${sanitizeIdentifier(column.name)} ${column.type.trim()}`
})
if (!/^[A-Za-z][A-Za-z0-9]*(\(.*\))?$/.test(engine.trim())) {
throw new Error(`Invalid table engine: ${engine}`)
}
validateExpression(engine, 'Engine')
if (!orderBy?.trim()) {
throw new Error('ORDER BY expression is required')
}
validateClauseExpression(orderBy, 'ORDER BY')
let statement = `CREATE TABLE IF NOT EXISTS ${sanitizeIdentifier(table)} (${columnDefs.join(', ')}) ENGINE = ${engine.trim()}`
if (partitionBy?.trim()) {
validateClauseExpression(partitionBy, 'PARTITION BY')
statement += ` PARTITION BY (${partitionBy.trim()})`
}
statement += ` ORDER BY (${orderBy.trim()})`
await clickhouseRequest(config, statement)
}
export async function executeClickHouseDropTable(
config: ClickHouseConnectionConfig,
table: string
): Promise<void> {
await clickhouseRequest(config, `DROP TABLE IF EXISTS ${sanitizeIdentifier(table)}`)
}
export async function executeClickHouseTruncateTable(
config: ClickHouseConnectionConfig,
table: string
): Promise<void> {
await clickhouseRequest(config, `TRUNCATE TABLE IF EXISTS ${sanitizeIdentifier(table)}`)
}
export async function executeClickHouseRenameTable(
config: ClickHouseConnectionConfig,
fromTable: string,
toTable: string
): Promise<void> {
await clickhouseRequest(
config,
`RENAME TABLE ${sanitizeIdentifier(fromTable)} TO ${sanitizeIdentifier(toTable)}`
)
}
export async function executeClickHouseOptimizeTable(
config: ClickHouseConnectionConfig,
table: string,
final: boolean
): Promise<void> {
await clickhouseRequest(
config,
`OPTIMIZE TABLE ${sanitizeIdentifier(table)}${final ? ' FINAL' : ''}`
)
}
export async function executeClickHouseDropPartition(
config: ClickHouseConnectionConfig,
table: string,
partition: string
): Promise<void> {
validatePartitionExpression(partition)
await clickhouseRequest(
config,
`ALTER TABLE ${sanitizeIdentifier(table)} DROP PARTITION ${partition.trim()}`
)
}
export function executeClickHouseKillQuery(
config: ClickHouseConnectionConfig,
queryId: string
): Promise<ClickHouseRowsResult> {
return runSelect(config, `KILL QUERY WHERE query_id = ${quoteString(queryId)} SYNC FORMAT JSON`)
}
export async function executeClickHouseInsertRows(
config: ClickHouseConnectionConfig,
table: string,
rows: Array<Record<string, unknown>>
): Promise<ClickHouseRowsResult> {
if (!Array.isArray(rows) || rows.length === 0) {
throw new Error('At least one row is required')
}
const sanitizedTable = sanitizeIdentifier(table)
const payload = rows.map((row) => JSON.stringify(row)).join('\n')
const statement = `INSERT INTO ${sanitizedTable} FORMAT JSONEachRow\n${payload}`
const written = await runStatement(config, statement)
return { rows: [], rowCount: written || rows.length }
}
function stripDatabasePrefix(table: string): string {
const parts = table.split('.')
return parts[parts.length - 1].replace(/`/g, '')
}
+466
View File
@@ -0,0 +1,466 @@
import { getErrorMessage } from '@sim/utils/errors'
import { ClickHouseIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { ClickHouseResponse } from '@/tools/clickhouse/types'
const CLICKHOUSE_QUERY_PROMPT = `You are an expert ClickHouse database developer. Write ClickHouse SQL queries based on the user's request.
### CONTEXT
{context}
### CRITICAL INSTRUCTION
Return ONLY the SQL query. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw SQL query.
### QUERY GUIDELINES
1. **Syntax**: Use ClickHouse-specific SQL syntax and functions
2. **Performance**: Filter on primary/sorting key columns and use PREWHERE where helpful
3. **Readability**: Format queries with proper indentation and spacing
4. **Best Practices**: Add a LIMIT clause for exploratory queries
### CLICKHOUSE FEATURES
- Use ClickHouse functions (toDateTime, toStartOfInterval, uniqExact, quantile, arrayJoin, etc.)
- Use ClickHouse data types (UInt64, Float64, String, DateTime, LowCardinality, etc.)
- Leverage aggregate combinators (-If, -Array, -State, -Merge) when appropriate
### EXAMPLES
**Simple Select**: "Get the 100 most recent events"
→ SELECT event_time, user_id, event_type
FROM events
ORDER BY event_time DESC
LIMIT 100;
**Aggregation**: "Count unique users per day for the last 7 days"
→ SELECT
toDate(event_time) AS day,
uniqExact(user_id) AS unique_users
FROM events
WHERE event_time >= now() - INTERVAL 7 DAY
GROUP BY day
ORDER BY day;
### REMEMBER
Return ONLY the SQL query - no explanations, no markdown, no extra text.`
const TABLE_REQUIRED_OPERATIONS = [
'insert',
'insert_rows',
'update',
'delete',
'describe_table',
'show_create_table',
'count_rows',
'list_partitions',
'create_table',
'drop_table',
'truncate_table',
'rename_table',
'optimize_table',
'drop_partition',
]
export const ClickHouseBlock: BlockConfig<ClickHouseResponse> = {
type: 'clickhouse',
name: 'ClickHouse',
description: 'Connect to a ClickHouse database',
longDescription:
'Integrate ClickHouse into the workflow. Query and insert data, manage databases and tables, inspect schemas, monitor mutations and running queries, manage partitions, and execute raw SQL over the ClickHouse HTTP interface.',
docsLink: 'https://docs.sim.ai/tools/clickhouse',
category: 'tools',
integrationType: IntegrationType.Databases,
tags: ['data-warehouse', 'data-analytics'],
bgColor: '#f9ff69',
icon: ClickHouseIcon,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'Query (SELECT)', id: 'query' },
{ label: 'Execute Raw SQL', id: 'execute' },
{ label: 'Insert Row', id: 'insert' },
{ label: 'Insert Rows (Bulk)', id: 'insert_rows' },
{ label: 'Update Data', id: 'update' },
{ label: 'Delete Data', id: 'delete' },
{ label: 'List Databases', id: 'list_databases' },
{ label: 'List Tables', id: 'list_tables' },
{ label: 'Describe Table', id: 'describe_table' },
{ label: 'Show Create Table', id: 'show_create_table' },
{ label: 'Count Rows', id: 'count_rows' },
{ label: 'Introspect Schema', id: 'introspect' },
{ label: 'Create Database', id: 'create_database' },
{ label: 'Drop Database', id: 'drop_database' },
{ label: 'Create Table', id: 'create_table' },
{ label: 'Drop Table', id: 'drop_table' },
{ label: 'Truncate Table', id: 'truncate_table' },
{ label: 'Rename Table', id: 'rename_table' },
{ label: 'Optimize Table', id: 'optimize_table' },
{ label: 'List Partitions', id: 'list_partitions' },
{ label: 'Drop Partition', id: 'drop_partition' },
{ label: 'List Mutations', id: 'list_mutations' },
{ label: 'List Running Queries', id: 'list_running_queries' },
{ label: 'Kill Query', id: 'kill_query' },
{ label: 'Table Stats', id: 'table_stats' },
{ label: 'List Clusters', id: 'list_clusters' },
],
value: () => 'query',
},
{
id: 'host',
title: 'Host',
type: 'short-input',
placeholder: 'your-instance.clickhouse.cloud',
required: true,
},
{
id: 'port',
title: 'Port',
type: 'short-input',
placeholder: '8443',
value: () => '8443',
required: true,
},
{
id: 'database',
title: 'Database Name',
type: 'short-input',
placeholder: 'default',
value: () => 'default',
required: true,
},
{
id: 'username',
title: 'Username',
type: 'short-input',
placeholder: 'default',
value: () => 'default',
required: true,
},
{
id: 'password',
title: 'Password',
type: 'short-input',
password: true,
placeholder: 'Your ClickHouse password',
},
{
id: 'secure',
title: 'Use HTTPS',
type: 'switch',
value: () => 'true',
},
{
id: 'table',
title: 'Table Name',
type: 'short-input',
placeholder: 'events',
condition: { field: 'operation', value: TABLE_REQUIRED_OPERATIONS },
required: { field: 'operation', value: TABLE_REQUIRED_OPERATIONS },
},
{
id: 'table',
title: 'Table Name (Optional)',
type: 'short-input',
placeholder: 'Leave blank for all tables',
condition: { field: 'operation', value: ['list_mutations', 'table_stats'] },
},
{
id: 'query',
title: 'SQL Query',
type: 'code',
placeholder: 'SELECT * FROM events ORDER BY event_time DESC LIMIT 100',
condition: { field: 'operation', value: 'query' },
required: { field: 'operation', value: 'query' },
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: CLICKHOUSE_QUERY_PROMPT,
placeholder: 'Describe the ClickHouse query you need...',
generationType: 'sql-query',
},
},
{
id: 'query',
title: 'SQL Statement',
type: 'code',
placeholder: 'CREATE TABLE events (id UInt64, name String) ENGINE = MergeTree ORDER BY id',
condition: { field: 'operation', value: 'execute' },
required: { field: 'operation', value: 'execute' },
wandConfig: {
enabled: true,
maintainHistory: true,
prompt: CLICKHOUSE_QUERY_PROMPT,
placeholder: 'Describe the ClickHouse statement you need...',
generationType: 'sql-query',
},
},
{
id: 'data',
title: 'Data (JSON)',
type: 'code',
placeholder: '{\n "id": 1,\n "name": "Example",\n "created_at": "2024-01-01 00:00:00"\n}',
condition: { field: 'operation', value: 'insert' },
required: { field: 'operation', value: 'insert' },
},
{
id: 'rows',
title: 'Rows (JSON Array)',
type: 'code',
placeholder: '[\n { "id": 1, "name": "A" },\n { "id": 2, "name": "B" }\n]',
condition: { field: 'operation', value: 'insert_rows' },
required: { field: 'operation', value: 'insert_rows' },
},
{
id: 'data',
title: 'Update Data (JSON)',
type: 'code',
placeholder: '{\n "name": "Updated name",\n "status": "active"\n}',
condition: { field: 'operation', value: 'update' },
required: { field: 'operation', value: 'update' },
},
{
id: 'where',
title: 'WHERE Condition',
type: 'short-input',
placeholder: 'id = 1',
condition: { field: 'operation', value: 'update' },
required: { field: 'operation', value: 'update' },
},
{
id: 'where',
title: 'WHERE Condition',
type: 'short-input',
placeholder: 'id = 1',
condition: { field: 'operation', value: 'delete' },
required: { field: 'operation', value: 'delete' },
},
{
id: 'where',
title: 'WHERE Condition (Optional)',
type: 'short-input',
placeholder: "status = 'active'",
condition: { field: 'operation', value: 'count_rows' },
},
{
id: 'name',
title: 'Database Name',
type: 'short-input',
placeholder: 'analytics',
condition: { field: 'operation', value: ['create_database', 'drop_database'] },
required: { field: 'operation', value: ['create_database', 'drop_database'] },
},
{
id: 'columns',
title: 'Columns (JSON Array)',
type: 'code',
placeholder:
'[\n { "name": "id", "type": "UInt64" },\n { "name": "ts", "type": "DateTime" }\n]',
condition: { field: 'operation', value: 'create_table' },
required: { field: 'operation', value: 'create_table' },
},
{
id: 'engine',
title: 'Engine',
type: 'short-input',
placeholder: 'MergeTree',
value: () => 'MergeTree',
condition: { field: 'operation', value: 'create_table' },
},
{
id: 'orderBy',
title: 'Order By',
type: 'short-input',
placeholder: 'id or (id, ts)',
condition: { field: 'operation', value: 'create_table' },
required: { field: 'operation', value: 'create_table' },
},
{
id: 'partitionBy',
title: 'Partition By (Optional)',
type: 'short-input',
placeholder: 'toYYYYMM(ts)',
condition: { field: 'operation', value: 'create_table' },
},
{
id: 'newTable',
title: 'New Table Name',
type: 'short-input',
placeholder: 'events_archive',
condition: { field: 'operation', value: 'rename_table' },
required: { field: 'operation', value: 'rename_table' },
},
{
id: 'final',
title: 'Force Final Merge',
type: 'switch',
condition: { field: 'operation', value: 'optimize_table' },
},
{
id: 'partition',
title: 'Partition',
type: 'short-input',
placeholder: "202401 or '2024-01'",
condition: { field: 'operation', value: 'drop_partition' },
required: { field: 'operation', value: 'drop_partition' },
},
{
id: 'queryId',
title: 'Query ID',
type: 'short-input',
placeholder: 'The query_id to kill',
condition: { field: 'operation', value: 'kill_query' },
required: { field: 'operation', value: 'kill_query' },
},
{
id: 'onlyRunning',
title: 'Only Running Mutations',
type: 'switch',
condition: { field: 'operation', value: 'list_mutations' },
},
],
tools: {
access: [
'clickhouse_query',
'clickhouse_execute',
'clickhouse_insert',
'clickhouse_insert_rows',
'clickhouse_update',
'clickhouse_delete',
'clickhouse_list_databases',
'clickhouse_list_tables',
'clickhouse_describe_table',
'clickhouse_show_create_table',
'clickhouse_count_rows',
'clickhouse_introspect',
'clickhouse_create_database',
'clickhouse_drop_database',
'clickhouse_create_table',
'clickhouse_drop_table',
'clickhouse_truncate_table',
'clickhouse_rename_table',
'clickhouse_optimize_table',
'clickhouse_list_partitions',
'clickhouse_drop_partition',
'clickhouse_list_mutations',
'clickhouse_list_running_queries',
'clickhouse_kill_query',
'clickhouse_table_stats',
'clickhouse_list_clusters',
],
config: {
tool: (params) => {
if (!params.operation) {
throw new Error('Operation is required')
}
return `clickhouse_${params.operation}`
},
params: (params) => {
const { operation, data, columns, rows, secure, ...rest } = params
const parseJsonField = (value: unknown, label: string): unknown => {
if (value && typeof value === 'string' && value.trim()) {
try {
return JSON.parse(value)
} catch (parseError) {
const errorMsg = getErrorMessage(parseError, 'Unknown JSON error')
throw new Error(`Invalid JSON in ${label}: ${errorMsg}. Please check your syntax.`)
}
}
if (value && typeof value === 'object') {
return value
}
return undefined
}
const parsedData = parseJsonField(data, 'data')
const parsedColumns = parseJsonField(columns, 'columns')
const parsedRows = parseJsonField(rows, 'rows')
const isSecure = secure !== false && secure !== 'false'
const result: Record<string, unknown> = {
host: rest.host,
port: typeof rest.port === 'string' ? Number.parseInt(rest.port, 10) : rest.port || 8443,
database: rest.database || 'default',
username: rest.username || 'default',
password: rest.password ?? '',
secure: isSecure,
}
if (rest.table) result.table = rest.table
if (rest.query) result.query = rest.query
if (rest.where) result.where = rest.where
if (rest.name) result.name = rest.name
if (rest.newTable) result.newTable = rest.newTable
if (rest.partition) result.partition = rest.partition
if (rest.queryId) result.queryId = rest.queryId
if (rest.engine) result.engine = rest.engine
if (rest.orderBy) result.orderBy = rest.orderBy
if (rest.partitionBy) result.partitionBy = rest.partitionBy
if (rest.final !== undefined) {
result.final = rest.final === true || rest.final === 'true'
}
if (rest.onlyRunning !== undefined) {
result.onlyRunning = rest.onlyRunning === true || rest.onlyRunning === 'true'
}
if (parsedData !== undefined) result.data = parsedData
if (parsedColumns !== undefined) result.columns = parsedColumns
if (parsedRows !== undefined) result.rows = parsedRows
return result
},
},
},
inputs: {
operation: { type: 'string', description: 'Database operation to perform' },
host: { type: 'string', description: 'ClickHouse host' },
port: { type: 'string', description: 'ClickHouse HTTP port' },
database: { type: 'string', description: 'Database name' },
username: { type: 'string', description: 'ClickHouse username' },
password: { type: 'string', description: 'ClickHouse password' },
secure: { type: 'boolean', description: 'Use a secure HTTPS connection' },
table: { type: 'string', description: 'Table name' },
query: { type: 'string', description: 'SQL statement to execute' },
data: { type: 'json', description: 'Data for insert/update operations' },
rows: { type: 'json', description: 'Array of row objects for bulk insert' },
columns: { type: 'json', description: 'Column definitions for create table' },
where: { type: 'string', description: 'WHERE clause for update/delete/count' },
name: { type: 'string', description: 'Database name for create/drop database' },
newTable: { type: 'string', description: 'Target table name for rename' },
partition: { type: 'string', description: 'Partition expression for drop partition' },
queryId: { type: 'string', description: 'Query ID for kill query' },
engine: { type: 'string', description: 'Table engine for create table' },
orderBy: { type: 'string', description: 'ORDER BY expression for create table' },
partitionBy: { type: 'string', description: 'PARTITION BY expression for create table' },
final: { type: 'boolean', description: 'Force a final merge for optimize table' },
onlyRunning: { type: 'boolean', description: 'Filter to running mutations only' },
},
outputs: {
message: {
type: 'string',
description: 'Success or error message describing the operation outcome',
},
rows: {
type: 'array',
description: 'Array of rows returned from the operation',
},
rowCount: {
type: 'number',
description: 'Number of rows returned or affected by the operation',
},
count: {
type: 'number',
description: 'Row count (count rows operation)',
},
ddl: {
type: 'string',
description: 'CREATE TABLE statement (show create table operation)',
},
tables: {
type: 'array',
description: 'Array of table schemas with columns and engines (introspect operation)',
},
},
}
+259 -11
View File
@@ -3,6 +3,13 @@ import type { BlockConfig } from '@/blocks/types'
import { IntegrationType } from '@/blocks/types'
import type { DagsterResponse } from '@/tools/dagster/types'
/** Coerces a subBlock value to a finite number, returning undefined for empty or non-numeric input. */
function toFiniteNumber(value: unknown): number | undefined {
if (value == null || value === '') return undefined
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : undefined
}
export const DagsterBlock: BlockConfig<DagsterResponse> = {
type: 'dagster',
name: 'Dagster',
@@ -37,6 +44,11 @@ export const DagsterBlock: BlockConfig<DagsterResponse> = {
{ label: 'List Sensors', id: 'list_sensors' },
{ label: 'Start Sensor', id: 'start_sensor' },
{ label: 'Stop Sensor', id: 'stop_sensor' },
{ label: 'List Assets', id: 'list_assets' },
{ label: 'Get Asset', id: 'get_asset' },
{ label: 'Materialize Assets', id: 'materialize_assets' },
{ label: 'Report Asset Materialization', id: 'report_asset_materialization' },
{ label: 'Wipe Asset', id: 'wipe_asset' },
],
value: () => 'launch_run',
},
@@ -49,11 +61,25 @@ export const DagsterBlock: BlockConfig<DagsterResponse> = {
placeholder: 'e.g., my_code_location',
condition: {
field: 'operation',
value: ['launch_run', 'list_schedules', 'start_schedule', 'list_sensors', 'start_sensor'],
value: [
'launch_run',
'list_schedules',
'start_schedule',
'list_sensors',
'start_sensor',
'materialize_assets',
],
},
required: {
field: 'operation',
value: ['launch_run', 'list_schedules', 'start_schedule', 'list_sensors', 'start_sensor'],
value: [
'launch_run',
'list_schedules',
'start_schedule',
'list_sensors',
'start_sensor',
'materialize_assets',
],
},
},
{
@@ -63,11 +89,25 @@ export const DagsterBlock: BlockConfig<DagsterResponse> = {
placeholder: 'e.g., __repository__',
condition: {
field: 'operation',
value: ['launch_run', 'list_schedules', 'start_schedule', 'list_sensors', 'start_sensor'],
value: [
'launch_run',
'list_schedules',
'start_schedule',
'list_sensors',
'start_sensor',
'materialize_assets',
],
},
required: {
field: 'operation',
value: ['launch_run', 'list_schedules', 'start_schedule', 'list_sensors', 'start_sensor'],
value: [
'launch_run',
'list_schedules',
'start_schedule',
'list_sensors',
'start_sensor',
'materialize_assets',
],
},
},
@@ -105,7 +145,7 @@ Return ONLY a valid JSON object - no explanations, no extra text.`,
title: 'Tags',
type: 'code',
placeholder: '[{"key": "env", "value": "prod"}]',
condition: { field: 'operation', value: 'launch_run' },
condition: { field: 'operation', value: ['launch_run', 'materialize_assets'] },
mode: 'advanced',
wandConfig: {
enabled: true,
@@ -210,6 +250,46 @@ Return ONLY the comma-separated status values - no explanations, no extra text.`
condition: { field: 'operation', value: 'list_runs' },
mode: 'advanced',
},
{
id: 'createdAfter',
title: 'Created After',
type: 'short-input',
placeholder: 'Unix timestamp in seconds (optional)',
condition: { field: 'operation', value: 'list_runs' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Convert the user's description of a start time into a Unix timestamp in seconds.
Return ONLY the integer Unix timestamp in seconds - no explanations, no extra text.`,
placeholder: 'Describe the earliest creation time...',
generationType: 'timestamp',
},
},
{
id: 'createdBefore',
title: 'Created Before',
type: 'short-input',
placeholder: 'Unix timestamp in seconds (optional)',
condition: { field: 'operation', value: 'list_runs' },
mode: 'advanced',
wandConfig: {
enabled: true,
prompt: `Convert the user's description of an end time into a Unix timestamp in seconds.
Return ONLY the integer Unix timestamp in seconds - no explanations, no extra text.`,
placeholder: 'Describe the latest creation time...',
generationType: 'timestamp',
},
},
{
id: 'runsCursor',
title: 'Cursor',
type: 'short-input',
placeholder: 'Run ID from a previous response cursor (for pagination)',
condition: { field: 'operation', value: 'list_runs' },
mode: 'advanced',
},
// ── Schedule operations ────────────────────────────────────────────────────
{
@@ -267,6 +347,95 @@ Return ONLY the comma-separated status values - no explanations, no extra text.`
required: { field: 'operation', value: ['stop_schedule', 'stop_sensor'] },
},
// ── Asset operations ───────────────────────────────────────────────────────
{
id: 'assetKey',
title: 'Asset Key',
type: 'short-input',
placeholder: 'e.g., my_asset or raw/events',
condition: {
field: 'operation',
value: ['get_asset', 'report_asset_materialization', 'wipe_asset'],
},
required: {
field: 'operation',
value: ['get_asset', 'report_asset_materialization', 'wipe_asset'],
},
},
{
id: 'assetJobName',
title: 'Asset Job',
type: 'short-input',
placeholder: 'e.g., __ASSET_JOB or a named asset job',
condition: { field: 'operation', value: 'materialize_assets' },
required: { field: 'operation', value: 'materialize_assets' },
},
{
id: 'assetSelection',
title: 'Asset Selection',
type: 'long-input',
placeholder: 'Comma- or newline-separated asset keys, e.g. raw/events, summary',
condition: { field: 'operation', value: 'materialize_assets' },
required: { field: 'operation', value: 'materialize_assets' },
wandConfig: {
enabled: true,
prompt: `Generate a comma-separated list of Dagster asset keys to materialize based on the user's description. Multi-part keys use slashes (e.g. raw/events).
Return ONLY the comma-separated asset keys - no explanations, no extra text.`,
placeholder: 'Describe which assets to materialize...',
},
},
{
id: 'assetPrefix',
title: 'Key Prefix',
type: 'short-input',
placeholder: 'Filter by asset key prefix, e.g. raw (optional)',
condition: { field: 'operation', value: 'list_assets' },
},
{
id: 'assetsLimit',
title: 'Limit',
type: 'short-input',
placeholder: '100',
condition: { field: 'operation', value: 'list_assets' },
mode: 'advanced',
},
{
id: 'assetsCursor',
title: 'Cursor',
type: 'short-input',
placeholder: 'Cursor from a previous list_assets response (for pagination)',
condition: { field: 'operation', value: 'list_assets' },
mode: 'advanced',
},
{
id: 'reportEventType',
title: 'Event Type',
type: 'dropdown',
options: [
{ label: 'Materialization', id: 'ASSET_MATERIALIZATION' },
{ label: 'Observation', id: 'ASSET_OBSERVATION' },
],
value: () => 'ASSET_MATERIALIZATION',
condition: { field: 'operation', value: 'report_asset_materialization' },
},
{
id: 'reportPartitionKeys',
title: 'Partition Keys',
type: 'short-input',
placeholder: 'Comma-separated partition keys (optional)',
condition: { field: 'operation', value: 'report_asset_materialization' },
mode: 'advanced',
},
{
id: 'reportDescription',
title: 'Description',
type: 'long-input',
placeholder: 'Description for the reported event (optional)',
condition: { field: 'operation', value: 'report_asset_materialization' },
mode: 'advanced',
},
// ── Connection (common to all operations) ──────────────────────────────────
{
id: 'host',
@@ -300,22 +469,29 @@ Return ONLY the comma-separated status values - no explanations, no extra text.`
'dagster_list_sensors',
'dagster_start_sensor',
'dagster_stop_sensor',
'dagster_list_assets',
'dagster_get_asset',
'dagster_materialize_assets',
'dagster_report_asset_materialization',
'dagster_wipe_asset',
],
config: {
tool: (params) => `dagster_${params.operation}`,
params: (params) => {
const result: Record<string, unknown> = {}
// list_runs: type-coerce limit and remap job name filter
// list_runs: type-coerce limit + time filters, remap job name filter and cursor
if (params.operation === 'list_runs') {
if (params.limit != null && params.limit !== '') result.limit = Number(params.limit)
result.limit = toFiniteNumber(params.limit)
result.jobName = params.listRunsJobName || undefined
result.createdAfter = toFiniteNumber(params.createdAfter)
result.createdBefore = toFiniteNumber(params.createdBefore)
result.cursor = params.runsCursor || undefined
}
// get_run_logs: remap logsLimit → limit
if (params.operation === 'get_run_logs') {
if (params.logsLimit != null && params.logsLimit !== '')
result.limit = Number(params.logsLimit)
result.limit = toFiniteNumber(params.logsLimit)
}
// reexecute_run: remap runId → parentRunId
@@ -331,6 +507,25 @@ Return ONLY the comma-separated status values - no explanations, no extra text.`
result.sensorStatus = undefined
}
// list_assets: type-coerce limit and remap prefix/cursor
if (params.operation === 'list_assets') {
result.prefix = params.assetPrefix || undefined
result.limit = toFiniteNumber(params.assetsLimit)
result.cursor = params.assetsCursor || undefined
}
// materialize_assets: remap asset job name → jobName
if (params.operation === 'materialize_assets') {
result.jobName = params.assetJobName
}
// report_asset_materialization: remap report-prefixed fields to tool params
if (params.operation === 'report_asset_materialization') {
result.eventType = params.reportEventType || 'ASSET_MATERIALIZATION'
result.partitionKeys = params.reportPartitionKeys || undefined
result.description = params.reportDescription || undefined
}
return result
},
},
@@ -362,6 +557,15 @@ Return ONLY the comma-separated status values - no explanations, no extra text.`
// List Runs
listRunsJobName: { type: 'string', description: 'Filter list_runs by job name' },
statuses: { type: 'string', description: 'Comma-separated run statuses to filter by' },
createdAfter: {
type: 'number',
description: 'Only return runs created at/after this Unix time',
},
createdBefore: {
type: 'number',
description: 'Only return runs created at/before this Unix time',
},
runsCursor: { type: 'string', description: 'Run ID cursor for list_runs pagination' },
limit: { type: 'number', description: 'Maximum results to return' },
// Schedules
scheduleName: { type: 'string', description: 'Schedule name' },
@@ -374,6 +578,25 @@ Return ONLY the comma-separated status values - no explanations, no extra text.`
sensorStatus: { type: 'string', description: 'Filter sensors by status (RUNNING or STOPPED)' },
// Stop schedule / sensor
instigationStateId: { type: 'string', description: 'InstigationState ID for stop operations' },
// Assets
assetKey: { type: 'string', description: 'Slash-delimited asset key' },
assetJobName: { type: 'string', description: 'Asset job to launch for materialization' },
assetSelection: {
type: 'string',
description: 'Comma/newline-separated asset keys to materialize',
},
assetPrefix: { type: 'string', description: 'Filter list_assets by key prefix' },
assetsLimit: { type: 'number', description: 'Maximum assets to return' },
assetsCursor: { type: 'string', description: 'Cursor for list_assets pagination' },
reportEventType: {
type: 'string',
description: 'Runless event type (ASSET_MATERIALIZATION or ASSET_OBSERVATION)',
},
reportPartitionKeys: {
type: 'string',
description: 'Comma-separated partition keys for the reported event',
},
reportDescription: { type: 'string', description: 'Description for the reported event' },
},
outputs: {
@@ -382,8 +605,14 @@ Return ONLY the comma-separated status values - no explanations, no extra text.`
// Get Run
jobName: { type: 'string', description: 'Job name the run belongs to' },
status: { type: 'string', description: 'Run or schedule/sensor status' },
mode: { type: 'string', description: 'Execution mode of the run' },
startTime: { type: 'number', description: 'Run start time (Unix timestamp)' },
endTime: { type: 'number', description: 'Run end time (Unix timestamp)' },
creationTime: { type: 'number', description: 'Run creation time (Unix timestamp)' },
updateTime: { type: 'number', description: 'Run last-update time (Unix timestamp)' },
parentRunId: { type: 'string', description: 'Immediate parent run ID (re-executions)' },
rootRunId: { type: 'string', description: 'Root run ID of the re-execution group' },
canTerminate: { type: 'boolean', description: 'Whether the run can be terminated' },
runConfigYaml: { type: 'string', description: 'Run configuration as YAML' },
tags: { type: 'json', description: 'Run tags as array of {key, value} objects' },
// List Runs
@@ -401,10 +630,13 @@ Return ONLY the comma-separated status values - no explanations, no extra text.`
type: 'json',
description: 'Log events (type, message, timestamp, level, stepKey, eventType)',
},
cursor: { type: 'string', description: 'Pagination cursor for the next page of logs' },
cursor: {
type: 'string',
description: 'Pagination cursor for the next page (logs, runs, or assets)',
},
hasMore: {
type: 'boolean',
description: 'Whether more log events are available beyond this page',
description: 'Whether more items are available beyond this page',
},
// List Schedules
schedules: {
@@ -419,5 +651,21 @@ Return ONLY the comma-separated status values - no explanations, no extra text.`
},
// Start/Stop schedule or sensor
id: { type: 'string', description: 'Instigator state ID of the schedule or sensor' },
// Get Run / Get Asset (asset key selection)
assetSelection: { type: 'json', description: 'Asset keys targeted by the run' },
// List Assets
assets: { type: 'json', description: 'List of assets (assetKey, path)' },
// Get Asset
assetKey: { type: 'string', description: 'Slash-joined asset key' },
path: { type: 'json', description: 'Asset key path segments' },
groupName: { type: 'string', description: 'Asset group name' },
description: { type: 'string', description: 'Asset description' },
jobNames: { type: 'json', description: 'Jobs that can materialize the asset' },
computeKind: { type: 'string', description: 'Asset compute kind tag' },
isPartitioned: { type: 'boolean', description: 'Whether the asset is partitioned' },
latestMaterialization: {
type: 'json',
description: 'Latest materialization (runId, timestamp, partition, stepKey)',
},
},
}
+184 -7
View File
@@ -6,11 +6,11 @@ import type { TinybirdResponse } from '@/tools/tinybird/types'
export const TinybirdBlock: BlockConfig<TinybirdResponse> = {
type: 'tinybird',
name: 'Tinybird',
description: 'Send events and query data with Tinybird',
description: 'Send events, query data, and manage Data Sources with Tinybird',
authMode: AuthMode.ApiKey,
longDescription:
'Interact with Tinybird using the Events API to stream JSON or NDJSON events, or use the Query API to execute SQL queries against Pipes and Data Sources.',
docsLink: 'https://www.tinybird.co/docs/api-reference',
'Interact with Tinybird: stream JSON or NDJSON events with the Events API, run SQL with the Query API, call published Pipe API Endpoints by name with dynamic parameters, and manage Data Sources by appending from a URL, truncating, or deleting rows by condition.',
docsLink: 'https://docs.sim.ai/tools/tinybird',
category: 'tools',
integrationType: IntegrationType.Analytics,
tags: ['data-warehouse', 'data-analytics'],
@@ -24,6 +24,10 @@ export const TinybirdBlock: BlockConfig<TinybirdResponse> = {
options: [
{ label: 'Send Events', id: 'tinybird_events' },
{ label: 'Query', id: 'tinybird_query' },
{ label: 'Query Pipe Endpoint', id: 'tinybird_query_pipe' },
{ label: 'Append Data Source (from URL)', id: 'tinybird_append_datasource' },
{ label: 'Truncate Data Source', id: 'tinybird_truncate_datasource' },
{ label: 'Delete Data Source Rows', id: 'tinybird_delete_datasource_rows' },
],
value: () => 'tinybird_events',
},
@@ -42,13 +46,21 @@ export const TinybirdBlock: BlockConfig<TinybirdResponse> = {
password: true,
required: true,
},
// Send Events operation inputs
// Data Source name (Send Events + Data Source management operations)
{
id: 'datasource',
title: 'Data Source',
type: 'short-input',
placeholder: 'my_events_datasource',
condition: { field: 'operation', value: 'tinybird_events' },
condition: {
field: 'operation',
value: [
'tinybird_events',
'tinybird_append_datasource',
'tinybird_truncate_datasource',
'tinybird_delete_datasource_rows',
],
},
required: true,
},
{
@@ -105,11 +117,95 @@ export const TinybirdBlock: BlockConfig<TinybirdResponse> = {
title: 'Pipeline Name',
type: 'short-input',
placeholder: 'my_pipe (optional)',
mode: 'advanced',
condition: { field: 'operation', value: 'tinybird_query' },
},
// Query Pipe Endpoint operation inputs
{
id: 'pipe',
title: 'Pipe Name',
type: 'short-input',
placeholder: 'top_pages',
condition: { field: 'operation', value: 'tinybird_query_pipe' },
required: true,
},
{
id: 'parameters',
title: 'Parameters',
type: 'code',
placeholder: '{\n "start_date": "2024-01-01",\n "limit": 10\n}',
condition: { field: 'operation', value: 'tinybird_query_pipe' },
wandConfig: {
enabled: true,
generationType: 'json-object',
placeholder: 'Describe the Pipe parameters to pass',
prompt:
'Generate a JSON object of dynamic parameters to pass to a Tinybird Pipe API Endpoint. Keys are parameter names and values are their values. Return ONLY the JSON object - no explanations, no extra text.',
},
},
{
id: 'pipe_sql',
title: 'SQL (on top of Pipe)',
type: 'code',
placeholder: 'SELECT count() FROM _',
mode: 'advanced',
condition: { field: 'operation', value: 'tinybird_query_pipe' },
},
// Append Data Source operation inputs
{
id: 'source_url',
title: 'Source File URL',
type: 'short-input',
placeholder: 'https://example.com/data.csv',
condition: { field: 'operation', value: 'tinybird_append_datasource' },
required: true,
},
{
id: 'source_format',
title: 'Source Format',
type: 'dropdown',
options: [
{ label: 'CSV', id: 'csv' },
{ label: 'NDJSON', id: 'ndjson' },
{ label: 'Parquet', id: 'parquet' },
],
value: () => 'csv',
condition: { field: 'operation', value: 'tinybird_append_datasource' },
},
// Delete Data Source Rows operation inputs
{
id: 'delete_condition',
title: 'Delete Condition',
type: 'long-input',
placeholder: "country = 'ES'",
condition: { field: 'operation', value: 'tinybird_delete_datasource_rows' },
required: true,
wandConfig: {
enabled: true,
generationType: 'sql-query',
placeholder: 'Describe which rows to delete',
prompt:
'Generate a SQL WHERE-clause condition (without the WHERE keyword) selecting rows to delete from a table. Example: "event_date < \'2024-01-01\'". Return ONLY the SQL condition - no explanations, no extra text.',
},
},
{
id: 'dry_run',
title: 'Dry Run',
type: 'switch',
value: () => 'false',
mode: 'advanced',
condition: { field: 'operation', value: 'tinybird_delete_datasource_rows' },
},
],
tools: {
access: ['tinybird_events', 'tinybird_query'],
access: [
'tinybird_events',
'tinybird_query',
'tinybird_query_pipe',
'tinybird_append_datasource',
'tinybird_truncate_datasource',
'tinybird_delete_datasource_rows',
],
config: {
tool: (params) => params.operation || 'tinybird_events',
params: (params) => {
@@ -133,7 +229,6 @@ export const TinybirdBlock: BlockConfig<TinybirdResponse> = {
result.format = params.format || 'ndjson'
result.compression = params.compression || 'none'
// Convert wait from string to boolean
// Convert wait from string to boolean
if (params.wait !== undefined) {
const waitValue =
@@ -150,6 +245,56 @@ export const TinybirdBlock: BlockConfig<TinybirdResponse> = {
if (params.pipeline) {
result.pipeline = params.pipeline
}
} else if (operation === 'tinybird_query_pipe') {
// Query Pipe Endpoint operation
if (!params.pipe) {
throw new Error('Pipe Name is required for Query Pipe Endpoint operation')
}
result.pipe = params.pipe
if (params.parameters) {
result.parameters = params.parameters
}
if (params.pipe_sql) {
result.q = params.pipe_sql
}
} else if (operation === 'tinybird_append_datasource') {
// Append Data Source from URL operation
if (!params.datasource) {
throw new Error('Data Source is required for Append Data Source operation')
}
if (!params.source_url) {
throw new Error('Source File URL is required for Append Data Source operation')
}
result.datasource = params.datasource
result.url = params.source_url
result.format = params.source_format || 'csv'
} else if (operation === 'tinybird_truncate_datasource') {
// Truncate Data Source operation
if (!params.datasource) {
throw new Error('Data Source is required for Truncate Data Source operation')
}
result.datasource = params.datasource
} else if (operation === 'tinybird_delete_datasource_rows') {
// Delete Data Source Rows operation
if (!params.datasource) {
throw new Error('Data Source is required for Delete Data Source Rows operation')
}
if (!params.delete_condition) {
throw new Error('Delete Condition is required for Delete Data Source Rows operation')
}
result.datasource = params.datasource
result.delete_condition = params.delete_condition
// Convert dry_run from string to boolean
if (params.dry_run !== undefined) {
const dryRunValue =
typeof params.dry_run === 'string' ? params.dry_run.toLowerCase() : params.dry_run
result.dry_run = dryRunValue === 'true' || dryRunValue === true
}
}
return result
@@ -180,6 +325,16 @@ export const TinybirdBlock: BlockConfig<TinybirdResponse> = {
// Query inputs
query: { type: 'string', description: 'SQL query to execute' },
pipeline: { type: 'string', description: 'Optional pipeline name' },
// Query Pipe Endpoint inputs
pipe: { type: 'string', description: 'Published Pipe API Endpoint name' },
parameters: { type: 'json', description: 'Dynamic Pipe parameters as a JSON object' },
pipe_sql: { type: 'string', description: 'Optional SQL to run on top of the Pipe result' },
// Append Data Source inputs
source_url: { type: 'string', description: 'URL of the file to append' },
source_format: { type: 'string', description: 'Source file format (csv, ndjson, parquet)' },
// Delete Data Source Rows inputs
delete_condition: { type: 'string', description: 'SQL condition selecting rows to delete' },
dry_run: { type: 'boolean', description: 'Test the delete without removing data' },
// Common
token: { type: 'string', description: 'Tinybird API Token' },
},
@@ -199,11 +354,33 @@ export const TinybirdBlock: BlockConfig<TinybirdResponse> = {
description:
'Query result data. FORMAT JSON: array of objects. Other formats (CSV, TSV, etc.): raw text string.',
},
meta: {
type: 'json',
description: 'Column metadata for the result set: [{name, type}] (only with FORMAT JSON)',
},
rows: { type: 'number', description: 'Number of rows returned (only with FORMAT JSON)' },
rows_before_limit_at_least: {
type: 'number',
description: 'Minimum rows without a LIMIT clause (only with FORMAT JSON)',
},
statistics: {
type: 'json',
description:
'Query execution statistics - elapsed time, rows read, bytes read (only with FORMAT JSON)',
},
// Data Source management outputs (append / truncate / delete)
id: { type: 'string', description: 'Operation identifier (append/delete)' },
import_id: { type: 'string', description: 'Import identifier (append)' },
job_id: { type: 'string', description: 'Job identifier to poll status (append/delete)' },
delete_id: { type: 'string', description: 'Deletion identifier (delete)' },
job_url: { type: 'string', description: 'URL to query job status (append/delete)' },
status: { type: 'string', description: 'Current job status (append/delete)' },
job: {
type: 'json',
description: 'Full job details: kind, id, status, datasource, rows_affected (append/delete)',
},
datasource: { type: 'json', description: 'Target Data Source metadata (append)' },
truncated: { type: 'boolean', description: 'Whether the Data Source was truncated' },
result: { type: 'json', description: 'Raw truncate response body, if any' },
},
}
+2
View File
@@ -28,6 +28,7 @@ import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
import { CirclebackBlock } from '@/blocks/blocks/circleback'
import { ClayBlock } from '@/blocks/blocks/clay'
import { ClerkBlock } from '@/blocks/blocks/clerk'
import { ClickHouseBlock } from '@/blocks/blocks/clickhouse'
import { CloudflareBlock } from '@/blocks/blocks/cloudflare'
import { CloudFormationBlock } from '@/blocks/blocks/cloudformation'
import { CloudWatchBlock } from '@/blocks/blocks/cloudwatch'
@@ -285,6 +286,7 @@ export const registry: Record<string, BlockConfig> = {
crowdstrike: CrowdStrikeBlock,
clay: ClayBlock,
clerk: ClerkBlock,
clickhouse: ClickHouseBlock,
condition: ConditionBlock,
credential: CredentialBlock,
confluence: ConfluenceBlock,
+12
View File
@@ -2743,6 +2743,18 @@ export function ClerkIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function ClickHouseIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg {...props} viewBox='0 0 150 150' xmlns='http://www.w3.org/2000/svg'>
<path d='m0 0h150v150h-150z' fill='#f9ff69' />
<path
d='m30 28.3c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v93.3c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1zm21.2 0c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v93.3c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1zm21.2 0c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v93.3c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1zm21.3 0c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v93.3c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1zm21.2 37.2c0-.6.5-1.1 1.1-1.1h8.4c.6 0 1.1.5 1.1 1.1v19c0 .6-.5 1.1-1.1 1.1h-8.4c-.6 0-1.1-.5-1.1-1.1z'
fill='#161616'
/>
</svg>
)
}
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 23 23' {...props}>
@@ -0,0 +1,319 @@
import { z } from 'zod'
import {
introspectionResponseSchema,
nonEmptyRecordSchema,
sqlRowsResponseSchema,
} from '@/lib/api/contracts/tools/databases/shared'
import {
type ContractBodyInput,
type ContractJsonResponse,
defineRouteContract,
} from '@/lib/api/contracts/types'
const secureFlagSchema = z
.union([z.boolean(), z.string()])
.transform((value) => (typeof value === 'string' ? value.toLowerCase() === 'true' : value))
.default(true)
export const clickhouseConnectionBodySchema = z.object({
host: z.string().min(1, 'Host is required'),
port: z.coerce.number().int().positive('Port must be a positive integer'),
database: z.string().min(1, 'Database name is required'),
username: z.string().min(1, 'Username is required'),
password: z.string().default(''),
secure: secureFlagSchema,
})
export const clickhouseQueryBodySchema = clickhouseConnectionBodySchema.extend({
query: z.string().min(1, 'Query is required'),
})
export const clickhouseExecuteBodySchema = clickhouseQueryBodySchema
export const clickhouseInsertBodySchema = clickhouseConnectionBodySchema.extend({
table: z.string().min(1, 'Table name is required'),
data: nonEmptyRecordSchema('Data object cannot be empty'),
})
export const clickhouseUpdateBodySchema = clickhouseConnectionBodySchema.extend({
table: z.string().min(1, 'Table name is required'),
data: nonEmptyRecordSchema('Data object cannot be empty'),
where: z.string().min(1, 'WHERE clause is required'),
})
export const clickhouseDeleteBodySchema = clickhouseConnectionBodySchema.extend({
table: z.string().min(1, 'Table name is required'),
where: z.string().min(1, 'WHERE clause is required'),
})
export const clickhouseIntrospectBodySchema = clickhouseConnectionBodySchema
export const clickhouseQueryContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/query',
body: clickhouseQueryBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseExecuteContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/execute',
body: clickhouseExecuteBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseInsertContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/insert',
body: clickhouseInsertBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseUpdateContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/update',
body: clickhouseUpdateBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseDeleteContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/delete',
body: clickhouseDeleteBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseIntrospectContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/introspect',
body: clickhouseIntrospectBodySchema,
response: { mode: 'json', schema: introspectionResponseSchema },
})
const clickhouseTableBodySchema = clickhouseConnectionBodySchema.extend({
table: z.string().min(1, 'Table name is required'),
})
const clickhouseCountResponseSchema = z.object({
message: z.string(),
count: z.number(),
})
const clickhouseDdlResponseSchema = z.object({
message: z.string(),
ddl: z.string(),
})
export const clickhouseListDatabasesBodySchema = clickhouseConnectionBodySchema
export const clickhouseListTablesBodySchema = clickhouseConnectionBodySchema
export const clickhouseDescribeTableBodySchema = clickhouseTableBodySchema
export const clickhouseShowCreateTableBodySchema = clickhouseTableBodySchema
export const clickhouseCountRowsBodySchema = clickhouseTableBodySchema.extend({
where: z.string().optional(),
})
export const clickhouseListPartitionsBodySchema = clickhouseTableBodySchema
export const clickhouseListMutationsBodySchema = clickhouseConnectionBodySchema.extend({
table: z.string().optional(),
onlyRunning: z
.union([z.boolean(), z.string()])
.transform((value) => (typeof value === 'string' ? value.toLowerCase() === 'true' : value))
.default(false),
})
export const clickhouseListRunningQueriesBodySchema = clickhouseConnectionBodySchema
export const clickhouseTableStatsBodySchema = clickhouseConnectionBodySchema.extend({
table: z.string().optional(),
})
export const clickhouseListClustersBodySchema = clickhouseConnectionBodySchema
export const clickhouseCreateDatabaseBodySchema = clickhouseConnectionBodySchema.extend({
name: z.string().min(1, 'Database name is required'),
})
export const clickhouseDropDatabaseBodySchema = clickhouseConnectionBodySchema.extend({
name: z.string().min(1, 'Database name is required'),
})
export const clickhouseCreateTableBodySchema = clickhouseConnectionBodySchema.extend({
table: z.string().min(1, 'Table name is required'),
columns: z
.array(
z.object({
name: z.string().min(1, 'Column name is required'),
type: z.string().min(1, 'Column type is required'),
})
)
.min(1, 'At least one column is required'),
engine: z.string().min(1).default('MergeTree'),
orderBy: z.string().min(1, 'ORDER BY expression is required'),
partitionBy: z.string().optional(),
})
export const clickhouseDropTableBodySchema = clickhouseTableBodySchema
export const clickhouseTruncateTableBodySchema = clickhouseTableBodySchema
export const clickhouseRenameTableBodySchema = clickhouseTableBodySchema.extend({
newTable: z.string().min(1, 'New table name is required'),
})
export const clickhouseOptimizeTableBodySchema = clickhouseTableBodySchema.extend({
final: z
.union([z.boolean(), z.string()])
.transform((value) => (typeof value === 'string' ? value.toLowerCase() === 'true' : value))
.default(false),
})
export const clickhouseDropPartitionBodySchema = clickhouseTableBodySchema.extend({
partition: z.string().min(1, 'Partition expression is required'),
})
export const clickhouseKillQueryBodySchema = clickhouseConnectionBodySchema.extend({
queryId: z.string().min(1, 'Query ID is required'),
})
export const clickhouseInsertRowsBodySchema = clickhouseTableBodySchema.extend({
rows: z.array(z.record(z.string(), z.unknown())).min(1, 'At least one row is required'),
})
export const clickhouseListDatabasesContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/list-databases',
body: clickhouseListDatabasesBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseListTablesContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/list-tables',
body: clickhouseListTablesBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseDescribeTableContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/describe-table',
body: clickhouseDescribeTableBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseShowCreateTableContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/show-create-table',
body: clickhouseShowCreateTableBodySchema,
response: { mode: 'json', schema: clickhouseDdlResponseSchema },
})
export const clickhouseCountRowsContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/count-rows',
body: clickhouseCountRowsBodySchema,
response: { mode: 'json', schema: clickhouseCountResponseSchema },
})
export const clickhouseListPartitionsContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/list-partitions',
body: clickhouseListPartitionsBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseListMutationsContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/list-mutations',
body: clickhouseListMutationsBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseListRunningQueriesContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/list-running-queries',
body: clickhouseListRunningQueriesBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseTableStatsContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/table-stats',
body: clickhouseTableStatsBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseListClustersContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/list-clusters',
body: clickhouseListClustersBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseCreateDatabaseContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/create-database',
body: clickhouseCreateDatabaseBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseDropDatabaseContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/drop-database',
body: clickhouseDropDatabaseBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseCreateTableContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/create-table',
body: clickhouseCreateTableBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseDropTableContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/drop-table',
body: clickhouseDropTableBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseTruncateTableContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/truncate-table',
body: clickhouseTruncateTableBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseRenameTableContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/rename-table',
body: clickhouseRenameTableBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseOptimizeTableContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/optimize-table',
body: clickhouseOptimizeTableBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseDropPartitionContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/drop-partition',
body: clickhouseDropPartitionBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseKillQueryContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/kill-query',
body: clickhouseKillQueryBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export const clickhouseInsertRowsContract = defineRouteContract({
method: 'POST',
path: '/api/tools/clickhouse/insert-rows',
body: clickhouseInsertRowsBodySchema,
response: { mode: 'json', schema: sqlRowsResponseSchema },
})
export type ClickHouseQueryRequest = ContractBodyInput<typeof clickhouseQueryContract>
export type ClickHouseQueryResponse = ContractJsonResponse<typeof clickhouseQueryContract>
export type ClickHouseExecuteRequest = ContractBodyInput<typeof clickhouseExecuteContract>
export type ClickHouseExecuteResponse = ContractJsonResponse<typeof clickhouseExecuteContract>
export type ClickHouseInsertRequest = ContractBodyInput<typeof clickhouseInsertContract>
export type ClickHouseInsertResponse = ContractJsonResponse<typeof clickhouseInsertContract>
export type ClickHouseUpdateRequest = ContractBodyInput<typeof clickhouseUpdateContract>
export type ClickHouseUpdateResponse = ContractJsonResponse<typeof clickhouseUpdateContract>
export type ClickHouseDeleteRequest = ContractBodyInput<typeof clickhouseDeleteContract>
export type ClickHouseDeleteResponse = ContractJsonResponse<typeof clickhouseDeleteContract>
export type ClickHouseIntrospectRequest = ContractBodyInput<typeof clickhouseIntrospectContract>
export type ClickHouseIntrospectResponse = ContractJsonResponse<typeof clickhouseIntrospectContract>
+100
View File
@@ -0,0 +1,100 @@
import type { ClickHouseCountResponse, ClickHouseCountRowsParams } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const countRowsTool: ToolConfig<ClickHouseCountRowsParams, ClickHouseCountResponse> = {
id: 'clickhouse_count_rows',
name: 'ClickHouse Count Rows',
description: 'Count rows in a ClickHouse table, optionally filtered',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to count rows in',
},
where: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional WHERE clause condition without the WHERE keyword',
},
},
request: {
url: '/api/tools/clickhouse/count-rows',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
where: params.where,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse count rows failed')
}
return {
success: true,
output: {
message: data.message || 'Row count retrieved',
count: data.count ?? 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
count: { type: 'number', description: 'Number of rows' },
},
}
@@ -0,0 +1,97 @@
import type {
ClickHouseCreateDatabaseParams,
ClickHouseMessageResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const createDatabaseTool: ToolConfig<
ClickHouseCreateDatabaseParams,
ClickHouseMessageResponse
> = {
id: 'clickhouse_create_database',
name: 'ClickHouse Create Database',
description: 'Create a new database on a ClickHouse server',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the database to create',
},
},
request: {
url: '/api/tools/clickhouse/create-database',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
name: params.name,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse create database failed')
}
return {
success: true,
output: {
message: data.message || 'Database created',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
},
}
+123
View File
@@ -0,0 +1,123 @@
import type {
ClickHouseCreateTableParams,
ClickHouseMessageResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const createTableTool: ToolConfig<ClickHouseCreateTableParams, ClickHouseMessageResponse> = {
id: 'clickhouse_create_table',
name: 'ClickHouse Create Table',
description: 'Create a new MergeTree-family table in ClickHouse',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the table to create',
},
columns: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Array of column definitions, each an object with name and type, e.g. [{"name":"id","type":"UInt64"},{"name":"ts","type":"DateTime"}]',
},
engine: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Table engine (default MergeTree)',
},
orderBy: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ORDER BY expression, e.g. "id" or "(id, ts)"',
},
partitionBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional PARTITION BY expression, e.g. toYYYYMM(ts)',
},
},
request: {
url: '/api/tools/clickhouse/create-table',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
columns: params.columns,
engine: params.engine,
orderBy: params.orderBy,
partitionBy: params.partitionBy,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse create table failed')
}
return {
success: true,
output: {
message: data.message || 'Table created',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
},
}
+102
View File
@@ -0,0 +1,102 @@
import type { ClickHouseDeleteParams, ClickHouseDeleteResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const deleteTool: ToolConfig<ClickHouseDeleteParams, ClickHouseDeleteResponse> = {
id: 'clickhouse_delete',
name: 'ClickHouse Delete',
description: 'Delete rows from a ClickHouse table via an ALTER TABLE ... DELETE mutation',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to delete data from',
},
where: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'WHERE clause condition (without the WHERE keyword)',
},
},
request: {
url: '/api/tools/clickhouse/delete',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
where: params.where,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse delete failed')
}
return {
success: true,
output: {
message: data.message || 'Delete mutation submitted successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Deleted rows (empty for ClickHouse mutations)' },
rowCount: { type: 'number', description: 'Number of rows affected by the mutation' },
},
}
@@ -0,0 +1,99 @@
import type {
ClickHouseDescribeTableParams,
ClickHouseRowsResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const describeTableTool: ToolConfig<ClickHouseDescribeTableParams, ClickHouseRowsResponse> =
{
id: 'clickhouse_describe_table',
name: 'ClickHouse Describe Table',
description: 'Describe the columns of a ClickHouse table',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to describe',
},
},
request: {
url: '/api/tools/clickhouse/describe-table',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse describe table failed')
}
return {
success: true,
output: {
message: data.message || 'Table described',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of rows returned from the query' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
@@ -0,0 +1,95 @@
import type {
ClickHouseDropDatabaseParams,
ClickHouseMessageResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const dropDatabaseTool: ToolConfig<ClickHouseDropDatabaseParams, ClickHouseMessageResponse> =
{
id: 'clickhouse_drop_database',
name: 'ClickHouse Drop Database',
description: 'Drop a database from a ClickHouse server',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the database to drop',
},
},
request: {
url: '/api/tools/clickhouse/drop-database',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
name: params.name,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse drop database failed')
}
return {
success: true,
output: {
message: data.message || 'Database dropped',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
},
}
+104
View File
@@ -0,0 +1,104 @@
import type {
ClickHouseDropPartitionParams,
ClickHouseMessageResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const dropPartitionTool: ToolConfig<
ClickHouseDropPartitionParams,
ClickHouseMessageResponse
> = {
id: 'clickhouse_drop_partition',
name: 'ClickHouse Drop Partition',
description: 'Drop a partition from a ClickHouse table',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name',
},
partition: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: "Partition expression, e.g. '2024-01' or 202401",
},
},
request: {
url: '/api/tools/clickhouse/drop-partition',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
partition: params.partition,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse drop partition failed')
}
return {
success: true,
output: {
message: data.message || 'Partition dropped',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
},
}
+91
View File
@@ -0,0 +1,91 @@
import type { ClickHouseDropTableParams, ClickHouseMessageResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const dropTableTool: ToolConfig<ClickHouseDropTableParams, ClickHouseMessageResponse> = {
id: 'clickhouse_drop_table',
name: 'ClickHouse Drop Table',
description: 'Drop a table from a ClickHouse database',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to drop',
},
},
request: {
url: '/api/tools/clickhouse/drop-table',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse drop table failed')
}
return {
success: true,
output: {
message: data.message || 'Table dropped',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
},
}
+95
View File
@@ -0,0 +1,95 @@
import type { ClickHouseExecuteParams, ClickHouseExecuteResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const executeTool: ToolConfig<ClickHouseExecuteParams, ClickHouseExecuteResponse> = {
id: 'clickhouse_execute',
name: 'ClickHouse Execute',
description: 'Execute raw SQL (DDL, mutations, or queries) on a ClickHouse database',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Raw SQL statement to execute',
},
},
request: {
url: '/api/tools/clickhouse/execute',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
query: params.query,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse execute failed')
}
return {
success: true,
output: {
message: data.message || 'Statement executed successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of rows returned from the statement' },
rowCount: { type: 'number', description: 'Number of rows returned or affected' },
},
}
+53
View File
@@ -0,0 +1,53 @@
import { countRowsTool } from './count-rows'
import { createDatabaseTool } from './create-database'
import { createTableTool } from './create-table'
import { deleteTool } from './delete'
import { describeTableTool } from './describe-table'
import { dropDatabaseTool } from './drop-database'
import { dropPartitionTool } from './drop-partition'
import { dropTableTool } from './drop-table'
import { executeTool } from './execute'
import { insertTool } from './insert'
import { insertRowsTool } from './insert-rows'
import { introspectTool } from './introspect'
import { killQueryTool } from './kill-query'
import { listClustersTool } from './list-clusters'
import { listDatabasesTool } from './list-databases'
import { listMutationsTool } from './list-mutations'
import { listPartitionsTool } from './list-partitions'
import { listRunningQueriesTool } from './list-running-queries'
import { listTablesTool } from './list-tables'
import { optimizeTableTool } from './optimize-table'
import { queryTool } from './query'
import { renameTableTool } from './rename-table'
import { showCreateTableTool } from './show-create-table'
import { tableStatsTool } from './table-stats'
import { truncateTableTool } from './truncate-table'
import { updateTool } from './update'
export const clickhouseQueryTool = queryTool
export const clickhouseExecuteTool = executeTool
export const clickhouseInsertTool = insertTool
export const clickhouseInsertRowsTool = insertRowsTool
export const clickhouseUpdateTool = updateTool
export const clickhouseDeleteTool = deleteTool
export const clickhouseIntrospectTool = introspectTool
export const clickhouseListDatabasesTool = listDatabasesTool
export const clickhouseListTablesTool = listTablesTool
export const clickhouseDescribeTableTool = describeTableTool
export const clickhouseShowCreateTableTool = showCreateTableTool
export const clickhouseCountRowsTool = countRowsTool
export const clickhouseListPartitionsTool = listPartitionsTool
export const clickhouseListMutationsTool = listMutationsTool
export const clickhouseListRunningQueriesTool = listRunningQueriesTool
export const clickhouseTableStatsTool = tableStatsTool
export const clickhouseListClustersTool = listClustersTool
export const clickhouseCreateDatabaseTool = createDatabaseTool
export const clickhouseDropDatabaseTool = dropDatabaseTool
export const clickhouseCreateTableTool = createTableTool
export const clickhouseDropTableTool = dropTableTool
export const clickhouseTruncateTableTool = truncateTableTool
export const clickhouseRenameTableTool = renameTableTool
export const clickhouseOptimizeTableTool = optimizeTableTool
export const clickhouseDropPartitionTool = dropPartitionTool
export const clickhouseKillQueryTool = killQueryTool
+102
View File
@@ -0,0 +1,102 @@
import type { ClickHouseInsertRowsParams, ClickHouseRowsResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const insertRowsTool: ToolConfig<ClickHouseInsertRowsParams, ClickHouseRowsResponse> = {
id: 'clickhouse_insert_rows',
name: 'ClickHouse Insert Rows',
description: 'Insert multiple rows into a ClickHouse table',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table to insert into',
},
rows: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Array of row objects to insert, e.g. [{"id":1,"name":"a"},{"id":2,"name":"b"}]',
},
},
request: {
url: '/api/tools/clickhouse/insert-rows',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
rows: params.rows,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse insert rows failed')
}
return {
success: true,
output: {
message: data.message || 'Rows inserted',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Inserted rows (empty for ClickHouse inserts)' },
rowCount: { type: 'number', description: 'Number of rows inserted' },
},
}
+102
View File
@@ -0,0 +1,102 @@
import type { ClickHouseInsertParams, ClickHouseInsertResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const insertTool: ToolConfig<ClickHouseInsertParams, ClickHouseInsertResponse> = {
id: 'clickhouse_insert',
name: 'ClickHouse Insert',
description: 'Insert a row into a ClickHouse table',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to insert data into',
},
data: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Data object to insert (key-value pairs mapping column names to values)',
},
},
request: {
url: '/api/tools/clickhouse/insert',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
data: params.data,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse insert failed')
}
return {
success: true,
output: {
message: data.message || 'Data inserted successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Inserted rows (empty for ClickHouse inserts)' },
rowCount: { type: 'number', description: 'Number of rows inserted' },
},
}
+99
View File
@@ -0,0 +1,99 @@
import {
CLICKHOUSE_TABLE_OUTPUT_PROPERTIES,
type ClickHouseIntrospectParams,
type ClickHouseIntrospectResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const introspectTool: ToolConfig<ClickHouseIntrospectParams, ClickHouseIntrospectResponse> =
{
id: 'clickhouse_introspect',
name: 'ClickHouse Introspect',
description:
'Introspect a ClickHouse database to retrieve table structures, columns, and engines',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to introspect',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
},
request: {
url: '/api/tools/clickhouse/introspect',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse introspection failed')
}
return {
success: true,
output: {
message: data.message || 'Schema introspection completed successfully',
tables: data.tables || [],
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
tables: {
type: 'array',
description: 'Array of table schemas with columns and engines',
items: {
type: 'object',
properties: CLICKHOUSE_TABLE_OUTPUT_PROPERTIES,
},
},
},
}
+95
View File
@@ -0,0 +1,95 @@
import type { ClickHouseKillQueryParams, ClickHouseRowsResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const killQueryTool: ToolConfig<ClickHouseKillQueryParams, ClickHouseRowsResponse> = {
id: 'clickhouse_kill_query',
name: 'ClickHouse Kill Query',
description: 'Kill a running query by its query ID',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
queryId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The query_id of the running query to kill',
},
},
request: {
url: '/api/tools/clickhouse/kill-query',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
queryId: params.queryId,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse kill query failed')
}
return {
success: true,
output: {
message: data.message || 'Kill command executed',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Kill status rows' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
@@ -0,0 +1,88 @@
import type { ClickHouseListClustersParams, ClickHouseRowsResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const listClustersTool: ToolConfig<ClickHouseListClustersParams, ClickHouseRowsResponse> = {
id: 'clickhouse_list_clusters',
name: 'ClickHouse List Clusters',
description: 'List configured clusters, shards, and replicas',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
},
request: {
url: '/api/tools/clickhouse/list-clusters',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse list clusters failed')
}
return {
success: true,
output: {
message: data.message || 'Clusters retrieved',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of cluster node rows' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
@@ -0,0 +1,92 @@
import type {
ClickHouseListDatabasesParams,
ClickHouseRowsResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const listDatabasesTool: ToolConfig<ClickHouseListDatabasesParams, ClickHouseRowsResponse> =
{
id: 'clickhouse_list_databases',
name: 'ClickHouse List Databases',
description: 'List all databases on a ClickHouse server',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
},
request: {
url: '/api/tools/clickhouse/list-databases',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse list databases failed')
}
return {
success: true,
output: {
message: data.message || 'Databases retrieved',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'List of databases with engine and comment' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
+106
View File
@@ -0,0 +1,106 @@
import type {
ClickHouseListMutationsParams,
ClickHouseRowsResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const listMutationsTool: ToolConfig<ClickHouseListMutationsParams, ClickHouseRowsResponse> =
{
id: 'clickhouse_list_mutations',
name: 'ClickHouse List Mutations',
description: 'List mutations (async ALTER UPDATE/DELETE) for the connected database',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional table name to filter mutations',
},
onlyRunning: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Only show mutations that are still running',
},
},
request: {
url: '/api/tools/clickhouse/list-mutations',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
onlyRunning: params.onlyRunning,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse list mutations failed')
}
return {
success: true,
output: {
message: data.message || 'Mutations retrieved',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of mutation rows' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
@@ -0,0 +1,101 @@
import type {
ClickHouseListPartitionsParams,
ClickHouseRowsResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const listPartitionsTool: ToolConfig<
ClickHouseListPartitionsParams,
ClickHouseRowsResponse
> = {
id: 'clickhouse_list_partitions',
name: 'ClickHouse List Partitions',
description: 'List active partitions for a ClickHouse table',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to inspect partitions for',
},
},
request: {
url: '/api/tools/clickhouse/list-partitions',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse list partitions failed')
}
return {
success: true,
output: {
message: data.message || 'Partitions retrieved',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of rows returned from the query' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
@@ -0,0 +1,94 @@
import type {
ClickHouseListRunningQueriesParams,
ClickHouseRowsResponse,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const listRunningQueriesTool: ToolConfig<
ClickHouseListRunningQueriesParams,
ClickHouseRowsResponse
> = {
id: 'clickhouse_list_running_queries',
name: 'ClickHouse List Running Queries',
description: 'List currently running queries on a ClickHouse server',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
},
request: {
url: '/api/tools/clickhouse/list-running-queries',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse list running queries failed')
}
return {
success: true,
output: {
message: data.message || 'Running queries retrieved',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of rows returned from the query' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
+88
View File
@@ -0,0 +1,88 @@
import type { ClickHouseListTablesParams, ClickHouseRowsResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const listTablesTool: ToolConfig<ClickHouseListTablesParams, ClickHouseRowsResponse> = {
id: 'clickhouse_list_tables',
name: 'ClickHouse List Tables',
description: 'List tables in the connected ClickHouse database',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
},
request: {
url: '/api/tools/clickhouse/list-tables',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse list tables failed')
}
return {
success: true,
output: {
message: data.message || 'Tables retrieved',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of rows returned from the query' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
+104
View File
@@ -0,0 +1,104 @@
import type {
ClickHouseMessageResponse,
ClickHouseOptimizeTableParams,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const optimizeTableTool: ToolConfig<
ClickHouseOptimizeTableParams,
ClickHouseMessageResponse
> = {
id: 'clickhouse_optimize_table',
name: 'ClickHouse Optimize Table',
description: 'Trigger a merge of table parts via OPTIMIZE TABLE',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table to optimize',
},
final: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Force a merge to a single part using FINAL',
},
},
request: {
url: '/api/tools/clickhouse/optimize-table',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
final: params.final,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse optimize table failed')
}
return {
success: true,
output: {
message: data.message || 'Optimize submitted',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
},
}
+95
View File
@@ -0,0 +1,95 @@
import type { ClickHouseQueryParams, ClickHouseQueryResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const queryTool: ToolConfig<ClickHouseQueryParams, ClickHouseQueryResponse> = {
id: 'clickhouse_query',
name: 'ClickHouse Query',
description: 'Execute a SELECT query on a ClickHouse database',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'SQL SELECT query to execute',
},
},
request: {
url: '/api/tools/clickhouse/query',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
query: params.query,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse query failed')
}
return {
success: true,
output: {
message: data.message || 'Query executed successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of rows returned from the query' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
+101
View File
@@ -0,0 +1,101 @@
import type {
ClickHouseMessageResponse,
ClickHouseRenameTableParams,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const renameTableTool: ToolConfig<ClickHouseRenameTableParams, ClickHouseMessageResponse> = {
id: 'clickhouse_rename_table',
name: 'ClickHouse Rename Table',
description: 'Rename a ClickHouse table',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Current table name',
},
newTable: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New table name',
},
},
request: {
url: '/api/tools/clickhouse/rename-table',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
newTable: params.newTable,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse rename table failed')
}
return {
success: true,
output: {
message: data.message || 'Table renamed',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
},
}
@@ -0,0 +1,99 @@
import type {
ClickHouseDdlResponse,
ClickHouseShowCreateTableParams,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const showCreateTableTool: ToolConfig<
ClickHouseShowCreateTableParams,
ClickHouseDdlResponse
> = {
id: 'clickhouse_show_create_table',
name: 'ClickHouse Show Create Table',
description: 'Get the CREATE TABLE statement (DDL) for a ClickHouse table',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to get the CREATE statement for',
},
},
request: {
url: '/api/tools/clickhouse/show-create-table',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse show create table failed')
}
return {
success: true,
output: {
message: data.message || 'CREATE statement retrieved',
ddl: data.ddl ?? '',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
ddl: { type: 'string', description: 'The CREATE TABLE statement' },
},
}
+95
View File
@@ -0,0 +1,95 @@
import type { ClickHouseRowsResponse, ClickHouseTableStatsParams } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const tableStatsTool: ToolConfig<ClickHouseTableStatsParams, ClickHouseRowsResponse> = {
id: 'clickhouse_table_stats',
name: 'ClickHouse Table Stats',
description: 'Get row counts and on-disk size for tables in the connected database',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional table name to get stats for',
},
},
request: {
url: '/api/tools/clickhouse/table-stats',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse table stats failed')
}
return {
success: true,
output: {
message: data.message || 'Table stats retrieved',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Array of table stats rows' },
rowCount: { type: 'number', description: 'Number of rows returned' },
},
}
@@ -0,0 +1,97 @@
import type {
ClickHouseMessageResponse,
ClickHouseTruncateTableParams,
} from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const truncateTableTool: ToolConfig<
ClickHouseTruncateTableParams,
ClickHouseMessageResponse
> = {
id: 'clickhouse_truncate_table',
name: 'ClickHouse Truncate Table',
description: 'Remove all rows from a ClickHouse table',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to truncate',
},
},
request: {
url: '/api/tools/clickhouse/truncate-table',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse truncate table failed')
}
return {
success: true,
output: {
message: data.message || 'Table truncated',
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
},
}
+213
View File
@@ -0,0 +1,213 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
* Output property definitions for ClickHouse introspection and query responses.
* @see https://clickhouse.com/docs/sql-reference/statements/system
*/
/**
* Output definition for table column objects from introspection.
* @see https://clickhouse.com/docs/operations/system-tables/columns
*/
export const CLICKHOUSE_COLUMN_OUTPUT_PROPERTIES = {
name: { type: 'string', description: 'Column name' },
type: { type: 'string', description: 'ClickHouse data type (e.g., UInt32, String, DateTime)' },
defaultKind: {
type: 'string',
description: 'Kind of default expression (DEFAULT, MATERIALIZED, ALIAS)',
optional: true,
},
defaultExpression: {
type: 'string',
description: 'Default value expression for the column',
optional: true,
},
isInPrimaryKey: { type: 'boolean', description: 'Whether the column is part of the primary key' },
isInSortingKey: { type: 'boolean', description: 'Whether the column is part of the sorting key' },
} as const satisfies Record<string, OutputProperty>
/**
* Output definition for table schema objects from introspection.
* @see https://clickhouse.com/docs/operations/system-tables/tables
*/
export const CLICKHOUSE_TABLE_OUTPUT_PROPERTIES = {
name: { type: 'string', description: 'Table name' },
database: { type: 'string', description: 'Database the table belongs to' },
engine: { type: 'string', description: 'Table engine (e.g., MergeTree, Log)' },
totalRows: {
type: 'number',
description: 'Approximate total number of rows in the table',
optional: true,
},
columns: {
type: 'array',
description: 'Table columns',
items: {
type: 'object',
properties: CLICKHOUSE_COLUMN_OUTPUT_PROPERTIES,
},
},
} as const satisfies Record<string, OutputProperty>
export interface ClickHouseConnectionConfig {
host: string
port: number
database: string
username: string
password: string
secure: boolean
}
export interface ClickHouseQueryParams extends ClickHouseConnectionConfig {
query: string
}
export interface ClickHouseExecuteParams extends ClickHouseConnectionConfig {
query: string
}
export interface ClickHouseInsertParams extends ClickHouseConnectionConfig {
table: string
data: Record<string, unknown>
}
export interface ClickHouseUpdateParams extends ClickHouseConnectionConfig {
table: string
data: Record<string, unknown>
where: string
}
export interface ClickHouseDeleteParams extends ClickHouseConnectionConfig {
table: string
where: string
}
export interface ClickHouseIntrospectParams extends ClickHouseConnectionConfig {}
export interface ClickHouseRowsResponse extends ToolResponse {
output: {
message: string
rows: unknown[]
rowCount: number
}
error?: string
}
export interface ClickHouseMessageResponse extends ToolResponse {
output: {
message: string
}
error?: string
}
export interface ClickHouseCountResponse extends ToolResponse {
output: {
message: string
count: number
}
error?: string
}
export interface ClickHouseDdlResponse extends ToolResponse {
output: {
message: string
ddl: string
}
error?: string
}
export interface ClickHouseListDatabasesParams extends ClickHouseConnectionConfig {}
export interface ClickHouseListTablesParams extends ClickHouseConnectionConfig {}
export interface ClickHouseDescribeTableParams extends ClickHouseConnectionConfig {
table: string
}
export interface ClickHouseShowCreateTableParams extends ClickHouseConnectionConfig {
table: string
}
export interface ClickHouseCountRowsParams extends ClickHouseConnectionConfig {
table: string
where?: string
}
export interface ClickHouseListPartitionsParams extends ClickHouseConnectionConfig {
table: string
}
export interface ClickHouseListMutationsParams extends ClickHouseConnectionConfig {
table?: string
onlyRunning?: boolean
}
export interface ClickHouseListRunningQueriesParams extends ClickHouseConnectionConfig {}
export interface ClickHouseTableStatsParams extends ClickHouseConnectionConfig {
table?: string
}
export interface ClickHouseListClustersParams extends ClickHouseConnectionConfig {}
export interface ClickHouseCreateDatabaseParams extends ClickHouseConnectionConfig {
name: string
}
export interface ClickHouseDropDatabaseParams extends ClickHouseConnectionConfig {
name: string
}
export interface ClickHouseCreateTableParams extends ClickHouseConnectionConfig {
table: string
columns: Array<{ name: string; type: string }>
engine: string
orderBy: string
partitionBy?: string
}
export interface ClickHouseDropTableParams extends ClickHouseConnectionConfig {
table: string
}
export interface ClickHouseTruncateTableParams extends ClickHouseConnectionConfig {
table: string
}
export interface ClickHouseRenameTableParams extends ClickHouseConnectionConfig {
table: string
newTable: string
}
export interface ClickHouseOptimizeTableParams extends ClickHouseConnectionConfig {
table: string
final?: boolean
}
export interface ClickHouseDropPartitionParams extends ClickHouseConnectionConfig {
table: string
partition: string
}
export interface ClickHouseKillQueryParams extends ClickHouseConnectionConfig {
queryId: string
}
export interface ClickHouseInsertRowsParams extends ClickHouseConnectionConfig {
table: string
rows: Array<Record<string, unknown>>
}
export interface ClickHouseQueryResponse extends ClickHouseRowsResponse {}
export interface ClickHouseExecuteResponse extends ClickHouseRowsResponse {}
export interface ClickHouseInsertResponse extends ClickHouseRowsResponse {}
export interface ClickHouseUpdateResponse extends ClickHouseRowsResponse {}
export interface ClickHouseDeleteResponse extends ClickHouseRowsResponse {}
interface ClickHouseTableColumn {
name: string
type: string
defaultKind?: string
defaultExpression?: string
isInPrimaryKey: boolean
isInSortingKey: boolean
}
interface ClickHouseTableSchema {
name: string
database: string
engine: string
totalRows?: number
columns: ClickHouseTableColumn[]
}
export interface ClickHouseIntrospectResponse extends ToolResponse {
output: {
message: string
tables: ClickHouseTableSchema[]
}
error?: string
}
export interface ClickHouseResponse extends ClickHouseRowsResponse {}
+109
View File
@@ -0,0 +1,109 @@
import type { ClickHouseUpdateParams, ClickHouseUpdateResponse } from '@/tools/clickhouse/types'
import type { ToolConfig } from '@/tools/types'
export const updateTool: ToolConfig<ClickHouseUpdateParams, ClickHouseUpdateResponse> = {
id: 'clickhouse_update',
name: 'ClickHouse Update',
description: 'Update rows in a ClickHouse table via an ALTER TABLE ... UPDATE mutation',
version: '1.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse server hostname (e.g., your-instance.clickhouse.cloud)',
},
port: {
type: 'number',
required: true,
visibility: 'user-only',
description: 'ClickHouse HTTP interface port (8443 for HTTPS, 8123 for HTTP)',
},
database: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Database name to connect to',
},
username: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'ClickHouse username',
},
password: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'ClickHouse password',
},
secure: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Use a secure HTTPS connection (default: true)',
},
table: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Table name to update data in',
},
data: {
type: 'object',
required: true,
visibility: 'user-or-llm',
description: 'Data object with fields to update (key-value pairs)',
},
where: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'WHERE clause condition (without the WHERE keyword)',
},
},
request: {
url: '/api/tools/clickhouse/update',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params) => ({
host: params.host,
port: Number(params.port),
database: params.database,
username: params.username,
password: params.password,
secure: params.secure,
table: params.table,
data: params.data,
where: params.where,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'ClickHouse update failed')
}
return {
success: true,
output: {
message: data.message || 'Update mutation submitted successfully',
rows: data.rows || [],
rowCount: data.rowCount || 0,
},
error: undefined,
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
rows: { type: 'array', description: 'Updated rows (empty for ClickHouse mutations)' },
rowCount: { type: 'number', description: 'Number of rows written by the mutation' },
},
}
+8 -7
View File
@@ -1,5 +1,10 @@
import type { DagsterDeleteRunParams, DagsterDeleteRunResponse } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface DeleteRunResult {
@@ -57,13 +62,9 @@ export const deleteRunTool: ToolConfig<DagsterDeleteRunParams, DagsterDeleteRunR
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: DELETE_RUN_MUTATION,
variables: { runId: params.runId },
+167
View File
@@ -0,0 +1,167 @@
import type { DagsterGetAssetParams, DagsterGetAssetResponse } from '@/tools/dagster/types'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
parseAssetKeyPath,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
/** Fields selected on `assetOrError` when the union resolves to `Asset`. */
interface DagsterGetAssetGraphqlAsset {
key: { path: string[] }
definition: {
groupName: string | null
description: string | null
jobNames: string[] | null
computeKind: string | null
isPartitioned: boolean | null
} | null
assetMaterializations: Array<{
runId: string
timestamp: string
partition: string | null
stepKey: string | null
}> | null
}
const GET_ASSET_QUERY = `
query GetAsset($assetKey: AssetKeyInput!) {
assetOrError(assetKey: $assetKey) {
... on Asset {
key {
path
}
definition {
groupName
description
jobNames
computeKind
isPartitioned
}
assetMaterializations(limit: 1) {
runId
timestamp
partition
stepKey
}
}
... on AssetNotFoundError {
__typename
message
}
}
}
`
export const getAssetTool: ToolConfig<DagsterGetAssetParams, DagsterGetAssetResponse> = {
id: 'dagster_get_asset',
name: 'Dagster Get Asset',
description: 'Get an asset definition and its latest materialization by asset key.',
version: '1.0.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'Dagster host URL (e.g., https://myorg.dagster.cloud/prod or http://localhost:3001)',
},
apiKey: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Dagster+ API token (leave blank for OSS / self-hosted)',
},
assetKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Slash-delimited asset key, e.g. "my_asset" or "raw/events"',
},
},
request: {
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: GET_ASSET_QUERY,
variables: { assetKey: { path: parseAssetKeyPath(params.assetKey) } },
}),
},
transformResponse: async (response: Response) => {
const data = await parseDagsterGraphqlResponse<{ assetOrError?: unknown }>(response)
const raw = data.data?.assetOrError
if (!raw || typeof raw !== 'object') throw new Error('Unexpected response from Dagster')
if (!('key' in raw)) {
const errResult = raw as { message?: string }
throw new Error(errResult.message ?? 'Asset not found')
}
const asset = raw as DagsterGetAssetGraphqlAsset
const latest = asset.assetMaterializations?.[0] ?? null
return {
success: true,
output: {
assetKey: asset.key.path.join('/'),
path: asset.key.path,
groupName: asset.definition?.groupName ?? null,
description: asset.definition?.description ?? null,
jobNames: asset.definition?.jobNames ?? null,
computeKind: asset.definition?.computeKind ?? null,
isPartitioned: asset.definition?.isPartitioned ?? null,
latestMaterialization: latest
? {
runId: latest.runId,
timestamp: latest.timestamp,
partition: latest.partition ?? null,
stepKey: latest.stepKey ?? null,
}
: null,
},
}
},
outputs: {
assetKey: { type: 'string', description: 'Slash-joined asset key' },
path: { type: 'json', description: 'Asset key path segments' },
groupName: {
type: 'string',
description: 'Asset group the definition belongs to',
optional: true,
},
description: { type: 'string', description: 'Asset description', optional: true },
jobNames: {
type: 'json',
description: 'Names of jobs that can materialize this asset',
optional: true,
},
computeKind: {
type: 'string',
description: 'Compute kind tag (e.g., python, dbt, spark)',
optional: true,
},
isPartitioned: {
type: 'boolean',
description: 'Whether the asset is partitioned',
optional: true,
},
latestMaterialization: {
type: 'json',
description: 'Most recent materialization (runId, timestamp, partition, stepKey)',
optional: true,
properties: {
runId: { type: 'string', description: 'Run that produced the materialization' },
timestamp: { type: 'string', description: 'Materialization timestamp (epoch ms string)' },
partition: { type: 'string', description: 'Partition key, if partitioned', optional: true },
stepKey: { type: 'string', description: 'Step key that emitted it', optional: true },
},
},
},
}
+67 -7
View File
@@ -1,5 +1,10 @@
import type { DagsterGetRunParams, DagsterGetRunResponse } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
/** Fields selected on `runOrError` when the union resolves to `Run`. */
@@ -7,8 +12,15 @@ interface DagsterGetRunGraphqlRun {
runId: string
jobName: string | null
status: string
mode: string | null
startTime: number | null
endTime: number | null
creationTime: number | null
updateTime: number | null
parentRunId: string | null
rootRunId: string | null
canTerminate: boolean
assetSelection: Array<{ path: string[] }> | null
runConfigYaml: string | null
tags: Array<{ key: string; value: string }> | null
}
@@ -20,8 +32,17 @@ const GET_RUN_QUERY = `
runId
jobName
status
mode
startTime
endTime
creationTime
updateTime
parentRunId
rootRunId
canTerminate
assetSelection {
path
}
runConfigYaml
tags {
key
@@ -69,13 +90,9 @@ export const getRunTool: ToolConfig<DagsterGetRunParams, DagsterGetRunResponse>
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: GET_RUN_QUERY,
variables: { runId: params.runId },
@@ -102,8 +119,17 @@ export const getRunTool: ToolConfig<DagsterGetRunParams, DagsterGetRunResponse>
runId: run.runId,
jobName: run.jobName ?? null,
status: run.status,
mode: run.mode ?? null,
startTime: run.startTime ?? null,
endTime: run.endTime ?? null,
creationTime: run.creationTime ?? null,
updateTime: run.updateTime ?? null,
parentRunId: run.parentRunId ?? null,
rootRunId: run.rootRunId ?? null,
canTerminate: run.canTerminate ?? false,
assetSelection: run.assetSelection
? run.assetSelection.map((key) => key.path.join('/'))
: null,
runConfigYaml: run.runConfigYaml ?? null,
tags: run.tags ?? null,
},
@@ -125,6 +151,11 @@ export const getRunTool: ToolConfig<DagsterGetRunParams, DagsterGetRunResponse>
description:
'Run status (QUEUED, NOT_STARTED, STARTING, MANAGED, STARTED, SUCCESS, FAILURE, CANCELING, CANCELED)',
},
mode: {
type: 'string',
description: 'Execution mode of the run',
optional: true,
},
startTime: {
type: 'number',
description: 'Run start time as Unix timestamp',
@@ -135,6 +166,35 @@ export const getRunTool: ToolConfig<DagsterGetRunParams, DagsterGetRunResponse>
description: 'Run end time as Unix timestamp',
optional: true,
},
creationTime: {
type: 'number',
description: 'Time the run was created as Unix timestamp',
optional: true,
},
updateTime: {
type: 'number',
description: 'Time the run was last updated as Unix timestamp',
optional: true,
},
parentRunId: {
type: 'string',
description: 'ID of the immediate parent run (for re-executions)',
optional: true,
},
rootRunId: {
type: 'string',
description: 'ID of the root run in the re-execution group',
optional: true,
},
canTerminate: {
type: 'boolean',
description: 'Whether the run can currently be terminated',
},
assetSelection: {
type: 'json',
description: 'Asset keys targeted by the run, as slash-joined strings',
optional: true,
},
runConfigYaml: {
type: 'string',
description: 'Run configuration as YAML',
+7 -7
View File
@@ -1,5 +1,9 @@
import type { DagsterGetRunLogsParams, DagsterGetRunLogsResponse } from '@/tools/dagster/types'
import { parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface DagsterRunEvent {
@@ -87,13 +91,9 @@ export const getRunLogsTool: ToolConfig<DagsterGetRunLogsParams, DagsterGetRunLo
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => {
const variables: Record<string, unknown> = { runId: params.runId }
if (params.afterCursor) variables.afterCursor = params.afterCursor
+10
View File
@@ -1,17 +1,22 @@
import { deleteRunTool } from '@/tools/dagster/delete_run'
import { getAssetTool } from '@/tools/dagster/get_asset'
import { getRunTool } from '@/tools/dagster/get_run'
import { getRunLogsTool } from '@/tools/dagster/get_run_logs'
import { launchRunTool } from '@/tools/dagster/launch_run'
import { listAssetsTool } from '@/tools/dagster/list_assets'
import { listJobsTool } from '@/tools/dagster/list_jobs'
import { listRunsTool } from '@/tools/dagster/list_runs'
import { listSchedulesTool } from '@/tools/dagster/list_schedules'
import { listSensorsTool } from '@/tools/dagster/list_sensors'
import { materializeAssetsTool } from '@/tools/dagster/materialize_assets'
import { reexecuteRunTool } from '@/tools/dagster/reexecute_run'
import { reportAssetMaterializationTool } from '@/tools/dagster/report_asset_materialization'
import { startScheduleTool } from '@/tools/dagster/start_schedule'
import { startSensorTool } from '@/tools/dagster/start_sensor'
import { stopScheduleTool } from '@/tools/dagster/stop_schedule'
import { stopSensorTool } from '@/tools/dagster/stop_sensor'
import { terminateRunTool } from '@/tools/dagster/terminate_run'
import { wipeAssetTool } from '@/tools/dagster/wipe_asset'
export const dagsterLaunchRunTool = launchRunTool
export const dagsterGetRunTool = getRunTool
@@ -27,5 +32,10 @@ export const dagsterStopScheduleTool = stopScheduleTool
export const dagsterListSensorsTool = listSensorsTool
export const dagsterStartSensorTool = startSensorTool
export const dagsterStopSensorTool = stopSensorTool
export const dagsterListAssetsTool = listAssetsTool
export const dagsterGetAssetTool = getAssetTool
export const dagsterMaterializeAssetsTool = materializeAssetsTool
export const dagsterReportAssetMaterializationTool = reportAssetMaterializationTool
export const dagsterWipeAssetTool = wipeAssetTool
export * from './types'
+8 -7
View File
@@ -1,5 +1,10 @@
import type { DagsterLaunchRunParams, DagsterLaunchRunResponse } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface LaunchRunResult {
@@ -143,13 +148,9 @@ export const launchRunTool: ToolConfig<DagsterLaunchRunParams, DagsterLaunchRunR
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => {
const variables: Record<string, unknown> = {
repositoryLocationName: params.repositoryLocationName,
+147
View File
@@ -0,0 +1,147 @@
import type { DagsterListAssetsParams, DagsterListAssetsResponse } from '@/tools/dagster/types'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseAssetKeyPath,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
/** Default page size applied when the caller omits `limit`, so paging stays bounded and `hasMore` is meaningful. */
const DEFAULT_LIST_ASSETS_LIMIT = 100
/** Shape of each asset node in the `assetsOrError` → `AssetConnection.nodes` selection set. */
interface DagsterAssetGraphqlNode {
key: { path: string[] }
}
const LIST_ASSETS_QUERY = `
query ListAssets($cursor: String, $limit: Int, $prefix: [String!]) {
assetsOrError(cursor: $cursor, limit: $limit, prefix: $prefix) {
... on AssetConnection {
nodes {
key {
path
}
}
cursor
}
... on PythonError {
__typename
message
}
}
}
`
export const listAssetsTool: ToolConfig<DagsterListAssetsParams, DagsterListAssetsResponse> = {
id: 'dagster_list_assets',
name: 'Dagster List Assets',
description: 'List assets tracked by a Dagster instance, optionally filtered by key prefix.',
version: '1.0.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'Dagster host URL (e.g., https://myorg.dagster.cloud/prod or http://localhost:3001)',
},
apiKey: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Dagster+ API token (leave blank for OSS / self-hosted)',
},
prefix: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Slash-delimited asset key prefix to filter by, e.g. "raw" or "raw/events" (optional)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Asset key cursor from a previous response, for pagination (optional)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of assets to return per page (default 100)',
},
},
request: {
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => dagsterRequestHeaders(params),
body: (params) => {
const pageSize = params.limit ?? DEFAULT_LIST_ASSETS_LIMIT
// Request one extra row so `hasMore` is exact even when the final page is exactly `pageSize` long.
const variables: Record<string, unknown> = { limit: pageSize + 1 }
if (params.prefix) variables.prefix = parseAssetKeyPath(params.prefix)
if (params.cursor) variables.cursor = params.cursor
return { query: LIST_ASSETS_QUERY, variables }
},
},
transformResponse: async (response: Response, params?: DagsterListAssetsParams) => {
const data = await parseDagsterGraphqlResponse<{ assetsOrError?: unknown }>(response)
const result = data.data?.assetsOrError as
| { nodes?: DagsterAssetGraphqlNode[]; cursor?: string | null; message?: string }
| undefined
if (!result) throw new Error('Unexpected response from Dagster')
if (!Array.isArray(result.nodes)) {
throw new Error(dagsterUnionErrorMessage(result, 'List assets failed'))
}
const pageSize = params?.limit ?? DEFAULT_LIST_ASSETS_LIMIT
const hasMore = result.nodes.length > pageSize
const pageNodes = hasMore ? result.nodes.slice(0, pageSize) : result.nodes
const assets = pageNodes.map((node) => ({
assetKey: node.key.path.join('/'),
path: node.key.path,
}))
// Asset cursors are the JSON-serialized key path; Dagster normalizes JS/Python whitespace on the
// way back in, so we derive the cursor from the last RETURNED asset (not the extra probe row).
const lastPath = pageNodes.length > 0 ? pageNodes[pageNodes.length - 1].key.path : null
return {
success: true,
output: {
assets,
cursor: lastPath ? JSON.stringify(lastPath) : null,
hasMore,
},
}
},
outputs: {
assets: {
type: 'json',
description: 'Array of assets (assetKey, path)',
properties: {
assetKey: { type: 'string', description: 'Slash-joined asset key' },
path: { type: 'json', description: 'Asset key path segments' },
},
},
cursor: {
type: 'string',
description: 'Cursor to pass on the next call to fetch more assets',
optional: true,
},
hasMore: {
type: 'boolean',
description: 'Whether more assets are likely available beyond this page',
},
},
}
+8 -7
View File
@@ -1,5 +1,10 @@
import type { DagsterBaseParams, DagsterListJobsResponse } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
const LIST_JOBS_QUERY = `
@@ -48,13 +53,9 @@ export const listJobsTool: ToolConfig<DagsterBaseParams, DagsterListJobsResponse
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: () => ({
query: LIST_JOBS_QUERY,
variables: {},
+60 -14
View File
@@ -1,7 +1,15 @@
import type { DagsterListRunsParams, DagsterListRunsResponse } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
/** Default page size applied when the caller omits `limit`, so paging stays bounded and `hasMore` is meaningful. */
const DEFAULT_LIST_RUNS_LIMIT = 20
/** Shape of each run in the `runsOrError` → `Runs.results` GraphQL selection set. */
interface DagsterListRunsGraphqlRow {
runId: string
@@ -14,8 +22,8 @@ interface DagsterListRunsGraphqlRow {
function buildListRunsQuery(hasFilter: boolean) {
return `
query ListRuns($limit: Int${hasFilter ? ', $filter: RunsFilter' : ''}) {
runsOrError(limit: $limit${hasFilter ? ', filter: $filter' : ''}) {
query ListRuns($limit: Int, $cursor: String${hasFilter ? ', $filter: RunsFilter' : ''}) {
runsOrError(limit: $limit, cursor: $cursor${hasFilter ? ', filter: $filter' : ''}) {
... on Runs {
results {
runId
@@ -45,7 +53,8 @@ function buildListRunsQuery(hasFilter: boolean) {
export const listRunsTool: ToolConfig<DagsterListRunsParams, DagsterListRunsResponse> = {
id: 'dagster_list_runs',
name: 'Dagster List Runs',
description: 'List recent Dagster runs, optionally filtered by job name.',
description:
'List Dagster runs with optional filters by job name, status, and creation-time range, plus cursor pagination.',
version: '1.0.0',
params: {
@@ -74,6 +83,25 @@ export const listRunsTool: ToolConfig<DagsterListRunsParams, DagsterListRunsResp
visibility: 'user-or-llm',
description: 'Comma-separated run statuses to filter by, e.g. "SUCCESS,FAILURE" (optional)',
},
createdAfter: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Only return runs created at or after this Unix timestamp in seconds (optional)',
},
createdBefore: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description:
'Only return runs created at or before this Unix timestamp in seconds (optional)',
},
cursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Run ID to page after, from a previous response cursor (optional)',
},
limit: {
type: 'number',
required: false,
@@ -83,13 +111,9 @@ export const listRunsTool: ToolConfig<DagsterListRunsParams, DagsterListRunsResp
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => {
const filter: Record<string, unknown> = {}
if (params.jobName) filter.pipelineName = params.jobName
@@ -99,9 +123,14 @@ export const listRunsTool: ToolConfig<DagsterListRunsParams, DagsterListRunsResp
.map((s: string) => s.trim())
.filter(Boolean)
}
if (params.createdAfter != null) filter.createdAfter = params.createdAfter
if (params.createdBefore != null) filter.createdBefore = params.createdBefore
const hasFilter = Object.keys(filter).length > 0
const variables: Record<string, unknown> = { limit: params.limit || 20 }
const pageSize = params.limit || DEFAULT_LIST_RUNS_LIMIT
// Request one extra row so `hasMore` is exact even when the final page is exactly `pageSize` long.
const variables: Record<string, unknown> = { limit: pageSize + 1 }
if (params.cursor) variables.cursor = params.cursor
if (hasFilter) variables.filter = filter
return {
@@ -111,7 +140,7 @@ export const listRunsTool: ToolConfig<DagsterListRunsParams, DagsterListRunsResp
},
},
transformResponse: async (response: Response) => {
transformResponse: async (response: Response, params?: DagsterListRunsParams) => {
const data = await parseDagsterGraphqlResponse<{ runsOrError?: unknown }>(response)
const result = data.data?.runsOrError as
@@ -123,7 +152,11 @@ export const listRunsTool: ToolConfig<DagsterListRunsParams, DagsterListRunsResp
throw new Error(dagsterUnionErrorMessage(result, 'Dagster returned an error listing runs'))
}
const runs = result.results.map((r: DagsterListRunsGraphqlRow) => ({
const pageSize = params?.limit || DEFAULT_LIST_RUNS_LIMIT
const hasMore = result.results.length > pageSize
const pageRows = hasMore ? result.results.slice(0, pageSize) : result.results
const runs = pageRows.map((r: DagsterListRunsGraphqlRow) => ({
runId: r.runId,
jobName: r.jobName ?? null,
status: r.status,
@@ -134,7 +167,11 @@ export const listRunsTool: ToolConfig<DagsterListRunsParams, DagsterListRunsResp
return {
success: true,
output: { runs },
output: {
runs,
cursor: runs.length > 0 ? runs[runs.length - 1].runId : null,
hasMore,
},
}
},
@@ -151,5 +188,14 @@ export const listRunsTool: ToolConfig<DagsterListRunsParams, DagsterListRunsResp
endTime: { type: 'number', description: 'End time as Unix timestamp' },
},
},
cursor: {
type: 'string',
description: 'Run ID of the last returned run — pass as cursor to fetch the next page',
optional: true,
},
hasMore: {
type: 'boolean',
description: 'Whether more runs are likely available beyond this page',
},
},
}
+8 -7
View File
@@ -2,7 +2,12 @@ import type {
DagsterListSchedulesParams,
DagsterListSchedulesResponse,
} from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface DagsterScheduleGraphql {
@@ -91,13 +96,9 @@ export const listSchedulesTool: ToolConfig<
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => {
const hasStatus = Boolean(params.scheduleStatus)
const variables: Record<string, unknown> = {
+9 -8
View File
@@ -1,5 +1,10 @@
import type { DagsterListSensorsParams, DagsterListSensorsResponse } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface DagsterSensorGraphql {
@@ -81,13 +86,9 @@ export const listSensorsTool: ToolConfig<DagsterListSensorsParams, DagsterListSe
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => {
const hasStatus = Boolean(params.sensorStatus)
const variables: Record<string, unknown> = {
@@ -136,7 +137,7 @@ export const listSensorsTool: ToolConfig<DagsterListSensorsParams, DagsterListSe
sensorType: {
type: 'string',
description:
'Sensor type (ASSET, AUTO_MATERIALIZE, FRESHNESS_POLICY, MULTI_ASSET, RUN_STATUS, STANDARD)',
'Sensor type (ASSET, AUTO_MATERIALIZE, FRESHNESS_POLICY, MULTI_ASSET, RUN_STATUS, STANDARD, UNKNOWN)',
},
status: { type: 'string', description: 'Sensor status: RUNNING or STOPPED' },
id: {
@@ -0,0 +1,201 @@
import type {
DagsterMaterializeAssetsParams,
DagsterMaterializeAssetsResponse,
} from '@/tools/dagster/types'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseAssetSelection,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface MaterializeAssetsResult {
type: string
run?: { runId: string }
message?: string
errors?: Array<{ message: string }>
}
function buildMaterializeMutation(hasTags: boolean) {
const varDefs = [
'$repositoryLocationName: String!',
'$repositoryName: String!',
'$jobName: String!',
'$assetSelection: [AssetKeyInput!]',
]
if (hasTags) varDefs.push('$tags: [ExecutionTag!]')
const execParams = [
`selector: {
repositoryLocationName: $repositoryLocationName
repositoryName: $repositoryName
jobName: $jobName
assetSelection: $assetSelection
}`,
]
if (hasTags) execParams.push('executionMetadata: { tags: $tags }')
return `
mutation MaterializeAssets(${varDefs.join(', ')}) {
launchRun(
executionParams: {
${execParams.join('\n ')}
}
) {
type: __typename
... on LaunchRunSuccess {
run {
runId
}
}
... on RunConfigValidationInvalid {
errors {
message
}
}
... on PipelineNotFoundError {
message
}
... on InvalidSubsetError {
message
}
... on UnauthorizedError {
message
}
... on ConflictingExecutionParamsError {
message
}
... on PresetNotFoundError {
message
}
... on RunConflict {
message
}
... on PythonError {
message
}
}
}
`
}
export const materializeAssetsTool: ToolConfig<
DagsterMaterializeAssetsParams,
DagsterMaterializeAssetsResponse
> = {
id: 'dagster_materialize_assets',
name: 'Dagster Materialize Assets',
description: 'Materialize selected assets by launching their asset job with an asset selection.',
version: '1.0.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'Dagster host URL (e.g., https://myorg.dagster.cloud/prod or http://localhost:3001)',
},
apiKey: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Dagster+ API token (leave blank for OSS / self-hosted)',
},
repositoryLocationName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Repository location (code location) name',
},
repositoryName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Repository name within the code location',
},
jobName: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Asset job that contains the assets, e.g. "__ASSET_JOB" or a named asset job',
},
assetSelection: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma- or newline-separated asset keys to materialize, each slash-delimited (e.g. "raw/events, summary")',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Tags as a JSON array of {key, value} objects (optional)',
},
},
request: {
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => dagsterRequestHeaders(params),
body: (params) => {
const assetSelection = parseAssetSelection(params.assetSelection)
if (assetSelection.length === 0) {
throw new Error('assetSelection must contain at least one asset key')
}
const variables: Record<string, unknown> = {
repositoryLocationName: params.repositoryLocationName,
repositoryName: params.repositoryName,
jobName: params.jobName,
assetSelection,
}
let hasTags = false
if (params.tags) {
try {
variables.tags = JSON.parse(params.tags)
hasTags = true
} catch {
throw new Error('Invalid JSON in tags')
}
}
return { query: buildMaterializeMutation(hasTags), variables }
},
},
transformResponse: async (response: Response) => {
const data = await parseDagsterGraphqlResponse<{ launchRun?: unknown }>(response)
const result = data.data?.launchRun as MaterializeAssetsResult | undefined
if (!result) throw new Error('Unexpected response from Dagster')
if (result.type === 'LaunchRunSuccess' && result.run) {
return {
success: true,
output: { runId: result.run.runId },
}
}
if (result.type === 'RunConfigValidationInvalid' && result.errors?.length) {
throw new Error(
`RunConfigValidationInvalid: ${result.errors.map((e) => e.message).join('; ')}`
)
}
throw new Error(
`${result.type}: ${dagsterUnionErrorMessage(result, 'Materialize assets failed')}`
)
},
outputs: {
runId: {
type: 'string',
description: 'The globally unique ID of the launched materialization run',
},
},
}
+8 -7
View File
@@ -1,5 +1,10 @@
import type { DagsterReexecuteRunParams, DagsterReexecuteRunResponse } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface ReexecuteRunResult {
@@ -106,13 +111,9 @@ export const reexecuteRunTool: ToolConfig<DagsterReexecuteRunParams, DagsterReex
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: REEXECUTE_RUN_MUTATION,
variables: {
@@ -0,0 +1,140 @@
import type {
DagsterReportAssetMaterializationParams,
DagsterReportAssetMaterializationResponse,
} from '@/tools/dagster/types'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseAssetKeyPath,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface ReportAssetEventResult {
type: string
assetKey?: { path: string[] }
message?: string
}
const REPORT_ASSET_EVENT_MUTATION = `
mutation ReportRunlessAssetEvents($eventParams: ReportRunlessAssetEventsParams!) {
reportRunlessAssetEvents(eventParams: $eventParams) {
type: __typename
... on ReportRunlessAssetEventsSuccess {
assetKey {
path
}
}
... on UnauthorizedError {
message
}
... on PythonError {
message
}
}
}
`
export const reportAssetMaterializationTool: ToolConfig<
DagsterReportAssetMaterializationParams,
DagsterReportAssetMaterializationResponse
> = {
id: 'dagster_report_asset_materialization',
name: 'Dagster Report Asset Materialization',
description: 'Report an external (runless) materialization or observation for an asset.',
version: '1.0.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'Dagster host URL (e.g., https://myorg.dagster.cloud/prod or http://localhost:3001)',
},
apiKey: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Dagster+ API token (leave blank for OSS / self-hosted)',
},
assetKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Slash-delimited asset key to report against, e.g. "my_asset" or "raw/events"',
},
eventType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Event type to report: ASSET_MATERIALIZATION (default) or ASSET_OBSERVATION',
},
partitionKeys: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated partition keys to report against (optional)',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Human-readable description for the reported event (optional)',
},
},
request: {
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => dagsterRequestHeaders(params),
body: (params) => {
const eventParams: Record<string, unknown> = {
eventType: params.eventType || 'ASSET_MATERIALIZATION',
assetKey: { path: parseAssetKeyPath(params.assetKey) },
}
if (params.partitionKeys) {
eventParams.partitionKeys = params.partitionKeys
.split(',')
.map((key) => key.trim())
.filter(Boolean)
}
if (params.description) eventParams.description = params.description
return { query: REPORT_ASSET_EVENT_MUTATION, variables: { eventParams } }
},
},
transformResponse: async (response: Response) => {
const data = await parseDagsterGraphqlResponse<{ reportRunlessAssetEvents?: unknown }>(response)
const result = data.data?.reportRunlessAssetEvents as ReportAssetEventResult | undefined
if (!result) throw new Error('Unexpected response from Dagster')
if (result.type === 'ReportRunlessAssetEventsSuccess' && result.assetKey) {
return {
success: true,
output: {
success: true,
assetKey: result.assetKey.path.join('/'),
},
}
}
throw new Error(
`${result.type}: ${dagsterUnionErrorMessage(result, 'Report asset event failed')}`
)
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the event was reported successfully',
},
assetKey: {
type: 'string',
description: 'Slash-joined asset key the event was reported against',
},
},
}
+8 -7
View File
@@ -2,7 +2,12 @@ import type {
DagsterScheduleMutationResponse,
DagsterStartScheduleParams,
} from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface ScheduleMutationResult {
@@ -84,13 +89,9 @@ export const startScheduleTool: ToolConfig<
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: START_SCHEDULE_MUTATION,
variables: {
+8 -7
View File
@@ -1,5 +1,10 @@
import type { DagsterSensorMutationResponse, DagsterStartSensorParams } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface SensorMutationResult {
@@ -79,13 +84,9 @@ export const startSensorTool: ToolConfig<DagsterStartSensorParams, DagsterSensor
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: START_SENSOR_MUTATION,
variables: {
+8 -7
View File
@@ -2,7 +2,12 @@ import type {
DagsterScheduleMutationResponse,
DagsterStopScheduleParams,
} from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface ScheduleMutationResult {
@@ -73,13 +78,9 @@ export const stopScheduleTool: ToolConfig<
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: STOP_SCHEDULE_MUTATION,
variables: { id: params.instigationStateId },
+8 -7
View File
@@ -1,5 +1,10 @@
import type { DagsterSensorMutationResponse, DagsterStopSensorParams } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface StopSensorResult {
@@ -63,13 +68,9 @@ export const stopSensorTool: ToolConfig<DagsterStopSensorParams, DagsterSensorMu
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: STOP_SENSOR_MUTATION,
variables: { id: params.instigationStateId },
+8 -7
View File
@@ -1,5 +1,10 @@
import type { DagsterTerminateRunParams, DagsterTerminateRunResponse } from '@/tools/dagster/types'
import { dagsterUnionErrorMessage, parseDagsterGraphqlResponse } from '@/tools/dagster/utils'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
/** Fields returned from `terminateRun` for all union members. */
@@ -67,13 +72,9 @@ export const terminateRunTool: ToolConfig<DagsterTerminateRunParams, DagsterTerm
},
request: {
url: (params) => `${params.host.replace(/\/$/, '')}/graphql`,
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey
return headers
},
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: TERMINATE_RUN_MUTATION,
variables: { runId: params.runId },
+92
View File
@@ -28,8 +28,15 @@ export interface DagsterGetRunResponse extends ToolResponse {
runId: string
jobName: string | null
status: string
mode: string | null
startTime: number | null
endTime: number | null
creationTime: number | null
updateTime: number | null
parentRunId: string | null
rootRunId: string | null
canTerminate: boolean
assetSelection: string[] | null
runConfigYaml: string | null
tags: Array<{ key: string; value: string }> | null
}
@@ -38,6 +45,9 @@ export interface DagsterGetRunResponse extends ToolResponse {
export interface DagsterListRunsParams extends DagsterBaseParams {
jobName?: string
statuses?: string
createdAfter?: number
createdBefore?: number
cursor?: string
limit?: number
}
@@ -51,6 +61,8 @@ export interface DagsterListRunsResponse extends ToolResponse {
startTime: number | null
endTime: number | null
}>
cursor: string | null
hasMore: boolean
}
}
@@ -189,6 +201,81 @@ export interface DagsterStopSensorParams extends DagsterBaseParams {
instigationStateId: string
}
export interface DagsterListAssetsParams extends DagsterBaseParams {
prefix?: string
cursor?: string
limit?: number
}
export interface DagsterListAssetsResponse extends ToolResponse {
output: {
assets: Array<{ assetKey: string; path: string[] }>
cursor: string | null
hasMore: boolean
}
}
export interface DagsterGetAssetParams extends DagsterBaseParams {
assetKey: string
}
export interface DagsterGetAssetResponse extends ToolResponse {
output: {
assetKey: string
path: string[]
groupName: string | null
description: string | null
jobNames: string[] | null
computeKind: string | null
isPartitioned: boolean | null
latestMaterialization: {
runId: string
timestamp: string
partition: string | null
stepKey: string | null
} | null
}
}
export interface DagsterMaterializeAssetsParams extends DagsterBaseParams {
repositoryLocationName: string
repositoryName: string
jobName: string
assetSelection: string
tags?: string
}
export interface DagsterMaterializeAssetsResponse extends ToolResponse {
output: {
runId: string
}
}
export interface DagsterReportAssetMaterializationParams extends DagsterBaseParams {
assetKey: string
eventType?: string
partitionKeys?: string
description?: string
}
export interface DagsterReportAssetMaterializationResponse extends ToolResponse {
output: {
success: boolean
assetKey: string
}
}
export interface DagsterWipeAssetParams extends DagsterBaseParams {
assetKey: string
}
export interface DagsterWipeAssetResponse extends ToolResponse {
output: {
success: boolean
assetKey: string
}
}
export type DagsterResponse =
| DagsterLaunchRunResponse
| DagsterGetRunResponse
@@ -202,3 +289,8 @@ export type DagsterResponse =
| DagsterScheduleMutationResponse
| DagsterListSensorsResponse
| DagsterSensorMutationResponse
| DagsterListAssetsResponse
| DagsterGetAssetResponse
| DagsterMaterializeAssetsResponse
| DagsterReportAssetMaterializationResponse
| DagsterWipeAssetResponse
+41
View File
@@ -1,3 +1,44 @@
/**
* Builds the GraphQL endpoint URL from a Dagster host, tolerating surrounding whitespace and a
* trailing slash (e.g. `https://myorg.dagster.cloud/prod` → `https://myorg.dagster.cloud/prod/graphql`).
*/
export function dagsterGraphqlUrl(host: string): string {
return `${host.trim().replace(/\/$/, '')}/graphql`
}
/**
* Builds the request headers for a Dagster GraphQL call, attaching the Dagster+ API token when one
* is provided (omitted for OSS / self-hosted instances).
*/
export function dagsterRequestHeaders(params: { apiKey?: string }): Record<string, string> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (params.apiKey) headers['Dagster-Cloud-Api-Token'] = params.apiKey.trim()
return headers
}
/**
* Splits a slash-delimited asset key string into a Dagster asset key path
* (e.g. `prefix/my_asset` → `['prefix', 'my_asset']`).
*/
export function parseAssetKeyPath(input: string): string[] {
return input
.split('/')
.map((segment) => segment.trim())
.filter(Boolean)
}
/**
* Parses a comma- or newline-separated list of slash-delimited asset keys into the
* `[AssetKeyInput!]` shape expected by Dagster (`{ path: string[] }[]`).
*/
export function parseAssetSelection(input: string): Array<{ path: string[] }> {
return input
.split(/[\n,]/)
.map((key) => key.trim())
.filter(Boolean)
.map((key) => ({ path: parseAssetKeyPath(key) }))
}
/**
* Parses a Dagster GraphQL JSON body and throws if the HTTP status is not OK or the payload
* contains top-level GraphQL errors.
+115
View File
@@ -0,0 +1,115 @@
import type { DagsterWipeAssetParams, DagsterWipeAssetResponse } from '@/tools/dagster/types'
import {
dagsterGraphqlUrl,
dagsterRequestHeaders,
dagsterUnionErrorMessage,
parseAssetKeyPath,
parseDagsterGraphqlResponse,
} from '@/tools/dagster/utils'
import type { ToolConfig } from '@/tools/types'
interface WipeAssetResult {
type: string
assetPartitionRanges?: Array<{ assetKey: { path: string[] } }>
message?: string
}
const WIPE_ASSET_MUTATION = `
mutation WipeAsset($assetPartitionRanges: [PartitionsByAssetSelector!]!) {
wipeAssets(assetPartitionRanges: $assetPartitionRanges) {
type: __typename
... on AssetWipeSuccess {
assetPartitionRanges {
assetKey {
path
}
}
}
... on AssetNotFoundError {
message
}
... on UnauthorizedError {
message
}
... on UnsupportedOperationError {
message
}
... on PythonError {
message
}
}
}
`
export const wipeAssetTool: ToolConfig<DagsterWipeAssetParams, DagsterWipeAssetResponse> = {
id: 'dagster_wipe_asset',
name: 'Dagster Wipe Asset',
description:
'DESTRUCTIVE: permanently wipes ALL materialization history (every partition) for an asset. This cannot be undone.',
version: '1.0.0',
params: {
host: {
type: 'string',
required: true,
visibility: 'user-only',
description:
'Dagster host URL (e.g., https://myorg.dagster.cloud/prod or http://localhost:3001)',
},
apiKey: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Dagster+ API token (leave blank for OSS / self-hosted)',
},
assetKey: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Slash-delimited asset key to wipe, e.g. "my_asset" or "raw/events"',
},
},
request: {
url: (params) => dagsterGraphqlUrl(params.host),
method: 'POST',
headers: (params) => dagsterRequestHeaders(params),
body: (params) => ({
query: WIPE_ASSET_MUTATION,
variables: {
assetPartitionRanges: [{ assetKey: { path: parseAssetKeyPath(params.assetKey) } }],
},
}),
},
transformResponse: async (response: Response) => {
const data = await parseDagsterGraphqlResponse<{ wipeAssets?: unknown }>(response)
const result = data.data?.wipeAssets as WipeAssetResult | undefined
if (!result) throw new Error('Unexpected response from Dagster')
if (result.type === 'AssetWipeSuccess') {
const wipedKey = result.assetPartitionRanges?.[0]?.assetKey.path.join('/') ?? ''
return {
success: true,
output: {
success: true,
assetKey: wipedKey,
},
}
}
throw new Error(`${result.type}: ${dagsterUnionErrorMessage(result, 'Wipe asset failed')}`)
},
outputs: {
success: {
type: 'boolean',
description: 'Whether the asset was wiped successfully',
},
assetKey: {
type: 'string',
description: 'Slash-joined asset key that was wiped',
},
},
}
+76 -1
View File
@@ -343,6 +343,34 @@ import {
clerkRevokeSessionTool,
clerkUpdateUserTool,
} from '@/tools/clerk'
import {
clickhouseCountRowsTool,
clickhouseCreateDatabaseTool,
clickhouseCreateTableTool,
clickhouseDeleteTool,
clickhouseDescribeTableTool,
clickhouseDropDatabaseTool,
clickhouseDropPartitionTool,
clickhouseDropTableTool,
clickhouseExecuteTool,
clickhouseInsertRowsTool,
clickhouseInsertTool,
clickhouseIntrospectTool,
clickhouseKillQueryTool,
clickhouseListClustersTool,
clickhouseListDatabasesTool,
clickhouseListMutationsTool,
clickhouseListPartitionsTool,
clickhouseListRunningQueriesTool,
clickhouseListTablesTool,
clickhouseOptimizeTableTool,
clickhouseQueryTool,
clickhouseRenameTableTool,
clickhouseShowCreateTableTool,
clickhouseTableStatsTool,
clickhouseTruncateTableTool,
clickhouseUpdateTool,
} from '@/tools/clickhouse'
import {
cloudflareCreateDnsRecordTool,
cloudflareCreateZoneTool,
@@ -460,19 +488,24 @@ import {
} from '@/tools/cursor'
import {
dagsterDeleteRunTool,
dagsterGetAssetTool,
dagsterGetRunLogsTool,
dagsterGetRunTool,
dagsterLaunchRunTool,
dagsterListAssetsTool,
dagsterListJobsTool,
dagsterListRunsTool,
dagsterListSchedulesTool,
dagsterListSensorsTool,
dagsterMaterializeAssetsTool,
dagsterReexecuteRunTool,
dagsterReportAssetMaterializationTool,
dagsterStartScheduleTool,
dagsterStartSensorTool,
dagsterStopScheduleTool,
dagsterStopSensorTool,
dagsterTerminateRunTool,
dagsterWipeAssetTool,
} from '@/tools/dagster'
import {
databricksCancelRunTool,
@@ -2974,7 +3007,14 @@ import {
} from '@/tools/telegram'
import { textractParserTool, textractParserV2Tool } from '@/tools/textract'
import { thinkingTool } from '@/tools/thinking'
import { tinybirdEventsTool, tinybirdQueryTool } from '@/tools/tinybird'
import {
tinybirdAppendDatasourceTool,
tinybirdDeleteDatasourceRowsTool,
tinybirdEventsTool,
tinybirdQueryPipeTool,
tinybirdQueryTool,
tinybirdTruncateDatasourceTool,
} from '@/tools/tinybird'
import {
trelloAddCommentTool,
trelloCreateCardTool,
@@ -4124,19 +4164,24 @@ export const tools: Record<string, ToolConfig> = {
devin_archive_session: devinArchiveSessionTool,
devin_terminate_session: devinTerminateSessionTool,
dagster_delete_run: dagsterDeleteRunTool,
dagster_get_asset: dagsterGetAssetTool,
dagster_get_run: dagsterGetRunTool,
dagster_get_run_logs: dagsterGetRunLogsTool,
dagster_launch_run: dagsterLaunchRunTool,
dagster_list_assets: dagsterListAssetsTool,
dagster_list_jobs: dagsterListJobsTool,
dagster_list_runs: dagsterListRunsTool,
dagster_list_schedules: dagsterListSchedulesTool,
dagster_list_sensors: dagsterListSensorsTool,
dagster_materialize_assets: dagsterMaterializeAssetsTool,
dagster_reexecute_run: dagsterReexecuteRunTool,
dagster_report_asset_materialization: dagsterReportAssetMaterializationTool,
dagster_start_schedule: dagsterStartScheduleTool,
dagster_start_sensor: dagsterStartSensorTool,
dagster_stop_schedule: dagsterStopScheduleTool,
dagster_stop_sensor: dagsterStopSensorTool,
dagster_terminate_run: dagsterTerminateRunTool,
dagster_wipe_asset: dagsterWipeAssetTool,
databricks_cancel_run: databricksCancelRunTool,
databricks_execute_sql: databricksExecuteSqlTool,
databricks_get_run: databricksGetRunTool,
@@ -5106,6 +5151,10 @@ export const tools: Record<string, ToolConfig> = {
thinking_tool: thinkingTool,
tinybird_events: tinybirdEventsTool,
tinybird_query: tinybirdQueryTool,
tinybird_query_pipe: tinybirdQueryPipeTool,
tinybird_append_datasource: tinybirdAppendDatasourceTool,
tinybird_truncate_datasource: tinybirdTruncateDatasourceTool,
tinybird_delete_datasource_rows: tinybirdDeleteDatasourceRowsTool,
stagehand_extract: stagehandExtractTool,
stagehand_agent: stagehandAgentTool,
mem0_add_memories: mem0AddMemoriesTool,
@@ -5205,6 +5254,32 @@ export const tools: Record<string, ToolConfig> = {
telegram_send_video: telegramSendVideoTool,
telegram_send_document: telegramSendDocumentTool,
clay_populate: clayPopulateTool,
clickhouse_query: clickhouseQueryTool,
clickhouse_insert: clickhouseInsertTool,
clickhouse_insert_rows: clickhouseInsertRowsTool,
clickhouse_update: clickhouseUpdateTool,
clickhouse_delete: clickhouseDeleteTool,
clickhouse_execute: clickhouseExecuteTool,
clickhouse_introspect: clickhouseIntrospectTool,
clickhouse_list_databases: clickhouseListDatabasesTool,
clickhouse_list_tables: clickhouseListTablesTool,
clickhouse_describe_table: clickhouseDescribeTableTool,
clickhouse_show_create_table: clickhouseShowCreateTableTool,
clickhouse_count_rows: clickhouseCountRowsTool,
clickhouse_list_partitions: clickhouseListPartitionsTool,
clickhouse_list_mutations: clickhouseListMutationsTool,
clickhouse_list_running_queries: clickhouseListRunningQueriesTool,
clickhouse_table_stats: clickhouseTableStatsTool,
clickhouse_list_clusters: clickhouseListClustersTool,
clickhouse_create_database: clickhouseCreateDatabaseTool,
clickhouse_drop_database: clickhouseDropDatabaseTool,
clickhouse_create_table: clickhouseCreateTableTool,
clickhouse_drop_table: clickhouseDropTableTool,
clickhouse_truncate_table: clickhouseTruncateTableTool,
clickhouse_rename_table: clickhouseRenameTableTool,
clickhouse_optimize_table: clickhouseOptimizeTableTool,
clickhouse_drop_partition: clickhouseDropPartitionTool,
clickhouse_kill_query: clickhouseKillQueryTool,
clerk_list_users: clerkListUsersTool,
clerk_get_user: clerkGetUserTool,
clerk_create_user: clerkCreateUserTool,
@@ -0,0 +1,143 @@
import { createLogger } from '@sim/logger'
import type {
TinybirdAppendDatasourceParams,
TinybirdAppendDatasourceResponse,
} from '@/tools/tinybird/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('tinybird-append-datasource')
/**
* Tinybird Append Data Source Tool
*
* Appends data to an existing Data Source from a remote file URL using the
* Data Sources API (`mode=append`). This is asynchronous and returns an import
* job that can be polled for completion.
*/
export const appendDatasourceTool: ToolConfig<
TinybirdAppendDatasourceParams,
TinybirdAppendDatasourceResponse
> = {
id: 'tinybird_append_datasource',
name: 'Tinybird Append Data Source',
description:
'Append data to a Tinybird Data Source from a remote file URL (CSV, NDJSON, Parquet).',
version: '1.0.0',
errorExtractor: 'nested-error-object',
params: {
base_url: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tinybird API base URL (e.g., https://api.tinybird.co)',
},
datasource: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the existing Data Source to append to. Example: "events_raw"',
},
url: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Publicly accessible URL of the file to append. Example: "https://example.com/data.csv"',
},
format: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Format of the source file: "csv" (default), "ndjson", or "parquet"',
},
token: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tinybird API Token with DATASOURCES:CREATE scope',
},
},
request: {
url: (params) => {
const baseUrl = params.base_url.trim().replace(/\/+$/, '')
return `${baseUrl}/v0/datasources`
},
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${params.token.trim()}`,
}),
body: (params) => {
const searchParams = new URLSearchParams()
searchParams.set('mode', 'append')
searchParams.set('name', params.datasource.trim())
searchParams.set('url', params.url.trim())
if (params.format) {
searchParams.set('format', params.format)
}
return searchParams.toString()
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
logger.info('Started Tinybird append-from-URL import', {
importId: data.import_id ?? data.job?.import_id,
status: data.status ?? data.job?.status,
})
return {
success: true,
output: {
id: data.id ?? null,
import_id: data.import_id ?? data.job?.import_id ?? null,
job_id: data.job_id ?? data.job?.job_id ?? data.job?.id ?? null,
job_url: data.job_url ?? data.job?.job_url ?? null,
status: data.status ?? data.job?.status ?? null,
job: data.job ?? null,
datasource: data.datasource ?? data.job?.datasource ?? null,
},
}
},
outputs: {
id: {
type: 'string',
description: 'Identifier of the append operation',
optional: true,
},
import_id: {
type: 'string',
description: 'Import identifier for the append job',
optional: true,
},
job_id: {
type: 'string',
description: 'Job identifier used to poll import status',
optional: true,
},
job_url: {
type: 'string',
description: 'URL to query the import job status',
optional: true,
},
status: {
type: 'string',
description: 'Initial job status (e.g., "waiting")',
optional: true,
},
job: {
type: 'json',
description: 'Full import job details (kind, id, status, created_at, datasource, ...)',
optional: true,
},
datasource: {
type: 'json',
description: 'Target Data Source metadata (id, name, ...)',
optional: true,
},
},
}
@@ -0,0 +1,136 @@
import { createLogger } from '@sim/logger'
import type {
TinybirdDeleteDatasourceRowsParams,
TinybirdDeleteDatasourceRowsResponse,
} from '@/tools/tinybird/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('tinybird-delete-datasource-rows')
/**
* Tinybird Delete Data Source Rows Tool
*
* Deletes rows from a Data Source that match a SQL condition. This is asynchronous
* and returns a delete job that can be polled for completion. Set `dry_run` to test
* the condition without deleting any data.
*/
export const deleteDatasourceRowsTool: ToolConfig<
TinybirdDeleteDatasourceRowsParams,
TinybirdDeleteDatasourceRowsResponse
> = {
id: 'tinybird_delete_datasource_rows',
name: 'Tinybird Delete Data Source Rows',
description: 'Delete rows from a Tinybird Data Source matching a SQL condition.',
version: '1.0.0',
errorExtractor: 'nested-error-object',
params: {
base_url: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tinybird API base URL (e.g., https://api.tinybird.co)',
},
datasource: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the Data Source to delete rows from. Example: "events_raw"',
},
delete_condition: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'SQL WHERE-clause condition selecting the rows to delete. Example: "country = \'ES\'" or "event_date < \'2024-01-01\'"',
},
dry_run: {
type: 'boolean',
required: false,
visibility: 'user-only',
description:
'When true, returns how many rows would be deleted without deleting them. Defaults to false.',
},
token: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tinybird API Token with DATASOURCES:CREATE scope',
},
},
request: {
url: (params) => {
const baseUrl = params.base_url.trim().replace(/\/+$/, '')
return `${baseUrl}/v0/datasources/${encodeURIComponent(params.datasource.trim())}/delete`
},
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${params.token.trim()}`,
}),
body: (params) => {
const searchParams = new URLSearchParams()
searchParams.set('delete_condition', params.delete_condition.trim())
if (params.dry_run) {
searchParams.set('dry_run', 'true')
}
return searchParams.toString()
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
logger.info('Started Tinybird delete-by-condition job', {
deleteId: data.delete_id,
status: data.status ?? data.job?.status,
})
return {
success: true,
output: {
id: data.id ?? null,
job_id: data.job_id ?? data.job?.job_id ?? data.job?.id ?? null,
delete_id: data.delete_id ?? null,
job_url: data.job_url ?? data.job?.job_url ?? null,
status: data.status ?? data.job?.status ?? null,
job: data.job ?? null,
},
}
},
outputs: {
id: {
type: 'string',
description: 'Identifier of the delete operation',
optional: true,
},
job_id: {
type: 'string',
description: 'Job identifier used to poll delete status',
optional: true,
},
delete_id: {
type: 'string',
description: 'Deletion identifier',
optional: true,
},
job_url: {
type: 'string',
description: 'URL to query the delete job status',
optional: true,
},
status: {
type: 'string',
description: 'Current job status (e.g., "waiting", "done")',
optional: true,
},
job: {
type: 'json',
description:
'Full delete job details (kind, id, status, delete_condition, rows_affected, ...)',
optional: true,
},
},
}
+8 -3
View File
@@ -64,9 +64,14 @@ export const eventsTool: ToolConfig<TinybirdEventsParams, TinybirdEventsResponse
request: {
url: (params) => {
const baseUrl = params.base_url.endsWith('/') ? params.base_url.slice(0, -1) : params.base_url
const baseUrl = params.base_url.trim().replace(/\/+$/, '')
const url = new URL(`${baseUrl}/v0/events`)
url.searchParams.set('name', params.datasource)
url.searchParams.set('name', params.datasource.trim())
// Tinybird selects JSON parsing via the `format=json` query parameter, not the
// Content-Type header. Default (omitted) is NDJSON.
if (params.format === 'json') {
url.searchParams.set('format', 'json')
}
if (params.wait) {
url.searchParams.set('wait', 'true')
}
@@ -75,7 +80,7 @@ export const eventsTool: ToolConfig<TinybirdEventsParams, TinybirdEventsResponse
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.token}`,
Authorization: `Bearer ${params.token.trim()}`,
}
if (params.compression === 'gzip') {
+8
View File
@@ -1,5 +1,13 @@
import { appendDatasourceTool } from '@/tools/tinybird/append_datasource'
import { deleteDatasourceRowsTool } from '@/tools/tinybird/delete_datasource_rows'
import { eventsTool } from '@/tools/tinybird/events'
import { queryTool } from '@/tools/tinybird/query'
import { queryPipeTool } from '@/tools/tinybird/query_pipe'
import { truncateDatasourceTool } from '@/tools/tinybird/truncate_datasource'
export const tinybirdEventsTool = eventsTool
export const tinybirdQueryTool = queryTool
export const tinybirdQueryPipeTool = queryPipeTool
export const tinybirdAppendDatasourceTool = appendDatasourceTool
export const tinybirdTruncateDatasourceTool = truncateDatasourceTool
export const tinybirdDeleteDatasourceRowsTool = deleteDatasourceRowsTool
+25 -5
View File
@@ -52,19 +52,19 @@ export const queryTool: ToolConfig<TinybirdQueryParams, TinybirdQueryResponse> =
request: {
url: (params) => {
const baseUrl = params.base_url.endsWith('/') ? params.base_url.slice(0, -1) : params.base_url
const baseUrl = params.base_url.trim().replace(/\/+$/, '')
return `${baseUrl}/v0/sql`
},
method: 'POST',
headers: (params) => ({
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${params.token}`,
Authorization: `Bearer ${params.token.trim()}`,
}),
body: (params) => {
const searchParams = new URLSearchParams()
searchParams.set('q', params.query)
if (params.pipeline) {
searchParams.set('pipeline', params.pipeline)
searchParams.set('pipeline', params.pipeline.trim())
}
return searchParams.toString()
},
@@ -88,8 +88,10 @@ export const queryTool: ToolConfig<TinybirdQueryParams, TinybirdQueryResponse> =
return {
success: true,
output: {
data: data.data || [],
rows: data.rows || 0,
data: data.data ?? [],
meta: data.meta ?? undefined,
rows: data.rows ?? 0,
rows_before_limit_at_least: data.rows_before_limit_at_least ?? undefined,
statistics: data.statistics
? {
elapsed: data.statistics.elapsed,
@@ -126,10 +128,28 @@ export const queryTool: ToolConfig<TinybirdQueryParams, TinybirdQueryResponse> =
description:
'Query result data. For FORMAT JSON: array of objects. For other formats (CSV, TSV, etc.): raw text string.',
},
meta: {
type: 'array',
description: 'Column metadata for the result set (only available with FORMAT JSON)',
optional: true,
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Column name' },
type: { type: 'string', description: 'Column data type' },
},
},
},
rows: {
type: 'number',
description: 'Number of rows returned (only available with FORMAT JSON)',
},
rows_before_limit_at_least: {
type: 'number',
description:
'Minimum number of rows there would be without a LIMIT clause (only available with FORMAT JSON)',
optional: true,
},
statistics: {
type: 'json',
description:
+174
View File
@@ -0,0 +1,174 @@
import { createLogger } from '@sim/logger'
import type { TinybirdQueryPipeParams, TinybirdQueryPipeResponse } from '@/tools/tinybird/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('tinybird-query-pipe')
/**
* Parses the dynamic-parameters input, which may arrive as a JSON object or a
* JSON string from a code/json subBlock. An omitted or empty value means "no
* parameters"; a non-empty value that is not a valid JSON object throws, so a
* mistyped input fails loudly instead of silently dropping the filters.
*/
function parsePipeParameters(
input: TinybirdQueryPipeParams['parameters']
): Record<string, unknown> {
if (input === undefined || input === null) return {}
if (typeof input === 'object') return input as Record<string, unknown>
const trimmed = input.trim()
if (!trimmed) return {}
let parsed: unknown
try {
parsed = JSON.parse(trimmed)
} catch {
throw new Error(
'Invalid Pipe parameters: expected a JSON object of key/value pairs (e.g. {"limit": 10})'
)
}
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('Invalid Pipe parameters: expected a JSON object, not a primitive or array')
}
return parsed as Record<string, unknown>
}
/**
* Tinybird Query Pipe Tool
*
* Calls a published Tinybird Pipe API Endpoint by name using the `.json` format,
* which is an alias for `SELECT * FROM {pipe}`. Templated Pipe parameters are passed
* as query-string arguments, and an optional `q` lets you run SQL on top of the result
* (using `_` to reference the Pipe).
*/
export const queryPipeTool: ToolConfig<TinybirdQueryPipeParams, TinybirdQueryPipeResponse> = {
id: 'tinybird_query_pipe',
name: 'Tinybird Query Pipe',
description:
'Call a published Tinybird Pipe API Endpoint by name, passing dynamic parameters and receiving structured JSON results.',
version: '1.0.0',
errorExtractor: 'nested-error-object',
params: {
base_url: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tinybird API base URL (e.g., https://api.tinybird.co)',
},
pipe: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the published Pipe API Endpoint to call. Example: "top_pages"',
},
parameters: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Dynamic Pipe parameters as a JSON object, sent as query-string arguments. Example: {"start_date": "2024-01-01", "limit": 10}',
},
q: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Optional SQL to run on top of the Pipe result. Use "_" to reference the Pipe. Example: "SELECT count() FROM _"',
},
token: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tinybird API Token with PIPE:READ scope',
},
},
request: {
url: (params) => {
const baseUrl = params.base_url.trim().replace(/\/+$/, '')
const url = new URL(`${baseUrl}/v0/pipes/${encodeURIComponent(params.pipe.trim())}.json`)
if (params.q) {
url.searchParams.set('q', params.q)
}
const dynamic = parsePipeParameters(params.parameters)
for (const [key, value] of Object.entries(dynamic)) {
// Don't let a dynamic parameter clobber the reserved `q` set above
if (key === 'q') continue
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value))
}
}
return url.toString()
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.token.trim()}`,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
logger.info('Successfully called Tinybird Pipe endpoint', {
rows: data.rows,
elapsed: data.statistics?.elapsed,
})
return {
success: true,
output: {
data: data.data ?? [],
meta: data.meta ?? undefined,
rows: data.rows ?? undefined,
rows_before_limit_at_least: data.rows_before_limit_at_least ?? undefined,
statistics: data.statistics
? {
elapsed: data.statistics.elapsed,
rows_read: data.statistics.rows_read,
bytes_read: data.statistics.bytes_read,
}
: undefined,
},
}
},
outputs: {
data: {
type: 'json',
description: 'Pipe result data as an array of row objects',
},
meta: {
type: 'array',
description: 'Column metadata for the result set',
optional: true,
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Column name' },
type: { type: 'string', description: 'Column data type' },
},
},
},
rows: {
type: 'number',
description: 'Number of rows returned',
optional: true,
},
rows_before_limit_at_least: {
type: 'number',
description: 'Minimum number of rows there would be without a LIMIT clause',
optional: true,
},
statistics: {
type: 'json',
description: 'Query execution statistics - elapsed time, rows read, bytes read',
optional: true,
properties: {
elapsed: { type: 'number', description: 'Query execution time in seconds' },
rows_read: { type: 'number', description: 'Number of rows processed' },
bytes_read: { type: 'number', description: 'Number of bytes processed' },
},
},
},
}
@@ -0,0 +1,91 @@
import { createLogger } from '@sim/logger'
import type {
TinybirdTruncateDatasourceParams,
TinybirdTruncateDatasourceResponse,
} from '@/tools/tinybird/types'
import type { ToolConfig } from '@/tools/types'
const logger = createLogger('tinybird-truncate-datasource')
/**
* Tinybird Truncate Data Source Tool
*
* Deletes all rows from a Data Source. Dependent Materialized Views are not
* truncated in cascade. The endpoint returns a minimal (often empty) body on success.
*/
export const truncateDatasourceTool: ToolConfig<
TinybirdTruncateDatasourceParams,
TinybirdTruncateDatasourceResponse
> = {
id: 'tinybird_truncate_datasource',
name: 'Tinybird Truncate Data Source',
description: 'Delete all rows from a Tinybird Data Source.',
version: '1.0.0',
errorExtractor: 'nested-error-object',
params: {
base_url: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tinybird API base URL (e.g., https://api.tinybird.co)',
},
datasource: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the Data Source to truncate. Example: "events_raw"',
},
token: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tinybird API Token with DATASOURCES:CREATE scope',
},
},
request: {
url: (params) => {
const baseUrl = params.base_url.trim().replace(/\/+$/, '')
return `${baseUrl}/v0/datasources/${encodeURIComponent(params.datasource.trim())}/truncate`
},
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.token.trim()}`,
}),
},
transformResponse: async (response: Response) => {
const text = await response.text()
let result: Record<string, unknown> | null = null
if (text) {
try {
result = JSON.parse(text)
} catch {
result = null
}
}
logger.info('Successfully truncated Tinybird Data Source')
return {
success: true,
output: {
truncated: true,
result,
},
}
},
outputs: {
truncated: {
type: 'boolean',
description: 'Whether the Data Source was truncated successfully',
},
result: {
type: 'json',
description: 'Raw response body from the truncate endpoint, if any',
optional: true,
},
},
}
+109 -6
View File
@@ -44,16 +44,119 @@ export interface TinybirdQueryParams extends TinybirdBaseParams {
export interface TinybirdQueryResponse extends ToolResponse {
output: {
data: unknown[] | string
meta?: Array<{ name: string; type: string }>
rows?: number
statistics?: {
elapsed: number
rows_read: number
bytes_read: number
}
rows_before_limit_at_least?: number
statistics?: TinybirdQueryStatistics
}
}
/**
* Query execution statistics returned by the Query API and Pipe endpoints
*/
interface TinybirdQueryStatistics {
elapsed: number
rows_read: number
bytes_read: number
}
/**
* Parameters for calling a published Pipe API Endpoint by name
*/
export interface TinybirdQueryPipeParams extends TinybirdBaseParams {
base_url: string
pipe: string
parameters?: Record<string, unknown> | string
q?: string
}
/**
* Response from calling a published Pipe API Endpoint (`.json` format)
*/
export interface TinybirdQueryPipeResponse extends ToolResponse {
output: {
data: unknown[]
meta?: Array<{ name: string; type: string }>
rows?: number
rows_before_limit_at_least?: number
statistics?: TinybirdQueryStatistics
}
}
/**
* Parameters for appending data to a Data Source from a URL
*/
export interface TinybirdAppendDatasourceParams extends TinybirdBaseParams {
base_url: string
datasource: string
url: string
format?: 'csv' | 'ndjson' | 'parquet'
}
/**
* Response from an append-from-URL import job
*/
export interface TinybirdAppendDatasourceResponse extends ToolResponse {
output: {
id: string | null
import_id: string | null
job_id: string | null
job_url: string | null
status: string | null
job: Record<string, unknown> | null
datasource: Record<string, unknown> | null
}
}
/**
* Parameters for truncating (deleting all rows from) a Data Source
*/
export interface TinybirdTruncateDatasourceParams extends TinybirdBaseParams {
base_url: string
datasource: string
}
/**
* Response from truncating a Data Source
*/
export interface TinybirdTruncateDatasourceResponse extends ToolResponse {
output: {
truncated: boolean
result: Record<string, unknown> | null
}
}
/**
* Parameters for deleting rows from a Data Source by condition
*/
export interface TinybirdDeleteDatasourceRowsParams extends TinybirdBaseParams {
base_url: string
datasource: string
delete_condition: string
dry_run?: boolean
}
/**
* Response from a delete-by-condition job
*/
export interface TinybirdDeleteDatasourceRowsResponse extends ToolResponse {
output: {
id: string | null
job_id: string | null
delete_id: string | null
job_url: string | null
status: string | null
job: Record<string, unknown> | null
}
}
/**
* Union type for all possible Tinybird responses
*/
export type TinybirdResponse = TinybirdEventsResponse | TinybirdQueryResponse
export type TinybirdResponse =
| TinybirdEventsResponse
| TinybirdQueryResponse
| TinybirdQueryPipeResponse
| TinybirdAppendDatasourceResponse
| TinybirdTruncateDatasourceResponse
| TinybirdDeleteDatasourceRowsResponse

Some files were not shown because too many files have changed in this diff Show More