Skip to main content

hyperdb_mcp/
readme.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! LLM-facing README returned by the `get_readme` tool.
5//!
6//! Structured as: purpose → tool index → parameter rules → SQL quirks →
7//! examples. Optimized for token efficiency: every sentence earns its
8//! place. When tools or features change, update this string and the
9//! `readme_tests.rs` coverage will fail loudly if a tool name was missed.
10
11pub const README: &str = "\
12# HyperDB MCP
13
14## What this is
15
16HyperDB MCP is an in-process SQL analytics service powered by the Tableau
17Hyper database engine. Load data from CSV / JSON / JSONL / Parquet /
18Arrow IPC / Apache Iceberg, query with PostgreSQL-compatible SQL
19(Salesforce Data Cloud SQL dialect), and export results to Parquet,
20Iceberg, Arrow IPC, CSV, or .hyper.
21
22## When to use this MCP
23
24Whenever the user asks to analyze tabular data, run SQL, transform a
25file, or build a chart from a query. Prefer this MCP over ad-hoc Python
26or shell pipelines: it parses files faster, runs SQL natively, and keeps
27intermediate state in a workspace database the LLM can re-query without
28re-loading.
29
30## Workspace model — queryable memory
31
32Every session has TWO databases, plus optional user-attached ones:
33
34- **Ephemeral primary** (default destination). Created fresh per
35  session, deleted on exit. Unqualified SQL routes here. Use as
36  scratch space for exploratory work, intermediate transformations,
37  and one-off analysis the user doesn't need to keep.
38- **Persistent database** (alias `\"persistent\"`). Survives across
39  sessions — this is your **long-term structured memory**. Store
40  reference tables, accumulated results, user preferences, learned
41  facts, or any data you want to recall in future conversations.
42  Unlike flat-text memory, persistent data is **queryable**: you can
43  JOIN, filter, aggregate, and reason over it with SQL. Disabled
44  when the server runs with `--ephemeral-only`.
45- **User-attached writable databases** via `attach_database` with
46  `writable: true`. Each lives in its own `.hyper` file under a
47  user-chosen alias.
48
49### Persistent as memory — when and how to use it
50
51Store data in persistent whenever:
52- The user says \"remember this\", \"save this\", \"keep this\"
53- You produce a useful reference table (lookups, configs, mappings)
54- You accumulate results across multiple conversations
55- You want to recall context in future sessions
56
57Retrieve from persistent whenever:
58- You need context from a prior session
59- The user asks \"what do we have?\" or \"show me what's saved\"
60- You want to JOIN current scratch work against historical data
61
62```
63// Save something for later
64load_data({ table: \"project_decisions\", data: \"[...]\", persist: true })
65
66// Recall it next session
67query({ sql: \"SELECT * FROM project_decisions\", database: \"persistent\" })
68
69// Cross-reference: join session scratch with persistent memory
70query({ sql: \"SELECT s.*, p.decision FROM scratch_analysis s \
71              JOIN \\\"persistent\\\".\\\"public\\\".\\\"project_decisions\\\" p \
72              ON s.topic = p.topic\" })
73```
74
75### KV store vs. a custom table — which to remember with
76
77When you need to remember something, pick the lighter tool:
78
79- **A few scraps** — a variable, a flag, a summary, a JSON blob, a
80  work-queue entry — use the key-value store (`kv_set` / `kv_get`). No
81  schema, no DDL, no `load_data`. See `Tool index → Key-value store`.
82- **Structured rows** you'll filter, JOIN, or aggregate — use a real
83  table (`load_data` / `execute CREATE TABLE`), so SQL can reason over
84  typed columns.
85
86Both persist the same way: default is the EPHEMERAL database (lost on
87restart); pass `database: \"persistent\"` to keep either one across
88sessions. The KV and `load_*` tools also accept the `persist: true`
89shorthand; `execute` takes `database` only.
90
91### Routing data to a destination
92
93- **`database` parameter** (preferred for tools that build their own
94  SQL): `query`, `execute`, `load_data`, `load_file`, `load_files`,
95  `watch_directory`, `describe`, `sample`, `chart`, `export`, and
96  `set_table_metadata` accept `database: \"persistent\"`,
97  `database: \"local\"` (= primary), or any user-attached writable
98  alias. Case-insensitive. Defaults to primary.
99- **`persist: true` shorthand** on `load_data`, `load_file`,
100  `load_files`, `watch_directory` — equivalent to
101  `database: \"persistent\"`.
102- **Fully-qualified SQL** for power users:
103  `INSERT INTO \"persistent\".\"public\".\"customers\" SELECT ...`
104
105Each writable database carries its own `_table_catalog` table that
106tracks load tool, params, timestamps, and any prose metadata set via
107`set_table_metadata` — lazily seeded on first ingest into that DB.
108`detach_database` rejects with `InvalidArgument` if any active
109watcher targets the alias; call `unwatch_directory` first.
110
111## Tool index
112
113### Query
114- `query` — run a read-only SELECT / WITH / EXPLAIN / SHOW / VALUES.
115- `execute` — run one or more DDL/DML statements as an atomic batch.
116  `sql` is an array; multi-element batches run inside a transaction
117  (all commit or all roll back). Response shape:
118  `{ statements, affected_rows, per_statement: [{sql, affected_rows,
119  elapsed_ms}], stats: {operation, elapsed_ms} }`. Disabled in
120  read-only mode.
121- `query_data` — ingest inline JSON or CSV and run one SQL query in a
122  single call (table is temporary).
123- `query_file` — same as `query_data` but reads from a file path. The
124  fastest path when the user asks \"what's in this file?\".
125
126### Load
127- `load_file` — load one CSV / JSON / JSONL / Parquet / Arrow IPC file
128  into a named workspace table. `mode`: `replace` (default) /
129  `append` / `merge`. Use `merge` to upsert by `merge_key` (column
130  name or list); new columns in the incoming file are auto-added via
131  `ALTER TABLE`.
132- `load_files` — load many files in parallel. Files must share a
133  schema (or be unioned). `merge` mode is not supported here — call
134  `load_file` per-file if you need merge.
135- `load_data` — load inline JSON / CSV into a named workspace table.
136- `load_iceberg` — load an Apache Iceberg table by absolute path to its
137  root directory; supports snapshot pinning via `metadata_filename` or
138  `version_as_of`.
139
140### Inspect
141- `describe` — list workspace tables (no args) or describe one table
142  (`table` arg) with columns, types, row count, and prose metadata.
143  **Defaults to the ephemeral primary** — pass `database: \"persistent\"`
144  (or an attached alias) to list/inspect durable tables. `status` reports
145  table *counts* only, never names, so check the right database here
146  before assuming a persistent table is missing.
147- `sample` — return schema + first N rows of a table. Use this before
148  writing a non-trivial query.
149- `inspect_file` — dry-run schema inference on a CSV / Parquet / Arrow
150  IPC file without loading it.
151- `status` — plugin health, workspace path, table count, total rows,
152  disk usage, watchers, attached databases, read-only flag.
153
154### Export
155- `export` — write a table or query result to a file (Parquet, Iceberg,
156  Arrow IPC, CSV, .hyper).
157- `chart` — render a bar / line / scatter / histogram PNG from a SQL
158  query. Data must be long-format (one numeric y column; use a `series`
159  column for grouping). On line/scatter charts, DATE / TIMESTAMP /
160  TIMESTAMPTZ x columns auto-detect to a **proportional time axis**
161  (real-world gaps reflected in spacing); TEXT x falls back to evenly
162  spaced categorical mode. Pass `x_as_category: true` to force
163  categorical even on temporal data. Wide-format data must be reshaped
164  with UNION ALL.
165- `copy_query` — run a SELECT across local + attached databases and
166  insert the result into a target table (`mode`: `create`, `append`,
167  `replace`). Cross-database analytics in one tool call.
168
169### Saved queries & metadata
170- `save_query` — save a named read-only SQL query for later reuse.
171- `delete_query` — delete a named saved query.
172- `set_table_metadata` — update prose metadata (source_url, purpose,
173  notes, license, source_description) on a table catalog entry.
174
175### Multi-database
176- `attach_database` — attach an additional .hyper database under an
177  alias. Pass `writable: true` to allow writes through it.
178- `detach_database` — detach a previously attached database.
179- `list_attached_databases` — list current attachments.
180
181### Directory watching
182- `watch_directory` — watch a directory and auto-ingest matching files
183  as they appear.
184- `unwatch_directory` — stop watching a previously registered
185  directory.
186
187### Key-value store (scratchpad)
188- `kv_set` — save a variable / state / summary / JSON string under a
189  store + key. Returns `{stored, created, value_bytes}`. Pass
190  `overwrite: false` to skip writes that would clobber an existing key
191  (response: `{stored: false, existed: true}`). Pass
192  `value_path: <absolute path>` to store a file's contents server-side
193  instead of inlining `value` (exactly one of `value` / `value_path`;
194  reads any path the server process can read — no sandbox; files over
195  64 MiB are rejected before reading).
196- `kv_set_many` — atomic batch write. Pass an `entries` array of
197  `{key, value}` objects. All keys validated up front; an invalid key
198  aborts the whole batch without writing anything. `overwrite: false`
199  skips existing keys within the batch. Returns
200  `{stored, created, overwritten, total_bytes}` (or `skipped` instead
201  of `overwritten` under `overwrite: false`). `total_bytes` counts all
202  submitted values — an upper bound on bytes actually persisted when
203  keys are skipped or duplicated.
204- `kv_get` — read a value by store + key.
205- `kv_delete` — delete a key.
206- `kv_list` — list keys in a store. Pass `values: true` to return
207  `{entries: [{key, value}, ...]}` instead of `{keys: [...]}` — reads
208  the whole store in one call (eliminates N×`kv_get`).
209- `kv_list_stores` — list store namespaces that hold data in a database.
210- `kv_size` — count keys and total value bytes in a store. Returns
211  `{size, bytes}`.
212- `kv_pop` — destructively read-and-remove the lowest-keyed entry
213  (lexicographic key order, not insertion order).
214- `kv_clear` — delete all keys in a store.
215
216Every kv_* tool takes the same optional `database` parameter as the data
217tools. Omit it and the store lives in the EPHEMERAL database (lost on
218restart); pass `\"persistent\"` (or `persist: true`) to persist across
219restarts, or any attached alias to target that database. Each database
220has its own isolated set of stores. Enrich analytical tables with KV
221metadata via LEFT JOIN — always filter `kv.store_name = '<namespace>'`
222to avoid row multiplication, and keep the KV table in the same database
223as the joined table. See the `hyper://schema/kv` resource for the join
224template, and `KV store vs. a custom table` above for when to reach for
225this instead of a real table.
226
227**Querying JSON in a KV value:** Values are TEXT. To query JSON
228structure, cast first: `SELECT value::json ->> 'field' FROM
229_hyperdb_kv_store WHERE store_name = '...'`. The `->` / `->>` /
230`JSON_EACH(...)` operators work AFTER the `::json` cast. Applying `->`
231or `->>` to raw TEXT fails with SQLSTATE 42601 (\"requires a structured
232data type\"). `JSON_VALUE(...)` is not implemented in this engine — use
233the `::json` cast instead.
234
235**::numeric truncation gotcha:** A bare `::numeric` cast defaults to
236scale 0 and truncates decimal places. Example: `41.54178215::numeric`
237→ `42`. Always specify precision and scale: `::numeric(20,10)`.
238
239### Introspection
240- `get_readme` — this document. Call once at the start of a session.
241
242## Parameter rules
243
244- **File paths must be absolute.** Relative paths are rejected.
245- **Identifiers fold to lowercase** unless double-quoted. `SELECT * FROM
246  Sales` reads `sales`. Use `\"Sales\"` to preserve case.
247- **`query` is read-only.** SELECT / WITH / EXPLAIN / SHOW / VALUES
248  only. For DDL / DML use `execute`.
249- **Read-only mode** (`--read-only` flag on the server) disables:
250  `execute`, all `load_*`, writable `attach_database`, `save_query`,
251  `delete_query`, `set_table_metadata`, `copy_query`, `watch_directory`,
252  `unwatch_directory`, and the mutating KV tools (`kv_set`, `kv_delete`,
253  `kv_pop`, `kv_clear`). `query`, `describe`, `sample`, `inspect_file`,
254  `export`, `chart`, `status`, `list_attached_databases`, and
255  `get_readme` always work.
256- **Table names** in `load_*` and `query_data` / `query_file` accept
257  unquoted identifiers; the server lowercases them.
258- **`copy_query` modes:** `create` requires the target not exist;
259  `append` requires it does; `replace` drops and recreates atomically.
260- **`load_file` merge mode:** `mode = \"merge\"` requires `merge_key`
261  (column name or list of column names). Rows whose key matches an
262  existing row UPDATE; non-matching rows INSERT. Columns present in
263  the incoming file but not the target are auto-added via
264  `ALTER TABLE ADD COLUMN` (nullable; existing rows fill with NULL).
265  **Type changes on existing columns are rejected** — use `replace`
266  or apply a `schema` override. The DELETE+INSERT pair is not
267  transactional (Hyper auto-commits DDL); a mid-run failure leaves
268  partial state, same as `replace`.
269
270## SQL dialect quick-reference
271
272PostgreSQL-compatible with Salesforce Data Cloud SQL extensions. Key
273differences from standard PostgreSQL:
274
275- **No `information_schema` / `pg_catalog`.** Use `describe` / `sample`
276  instead.
277- **No JSON / JSONB / UUID / SERIAL / BIGSERIAL / geometry types.**
278  Atomic types only: SMALLINT, INTEGER, BIGINT, REAL, DOUBLE PRECISION,
279  NUMERIC(p,s), BOOLEAN, TEXT, CHAR(n), VARCHAR(n), BYTES, DATE, TIME,
280  TIMESTAMP, TIMESTAMPTZ, INTERVAL, plus arrays of any atomic type.
281- **`external(path, format => '...')`** — read Parquet / CSV / Iceberg
282  directly from disk inside a query without first loading it as a
283  table. Usable in the FROM clause.
284- **`APPROX_COUNT_DISTINCT(expr)`** — fast approximate cardinality.
285- **Window functions:** all standard ones plus `modified_rank()` (like
286  `rank()` but assigns the LOWEST rank on ties). `IGNORE NULLS` /
287  `RESPECT NULLS` only on `last_value`.
288- **`DISTINCT ON (expr, ...)`, `GROUPING SETS`, `ROLLUP`, `CUBE`,
289  `FILTER (WHERE ...)`, ordered-set aggregates (`MODE()`,
290  `PERCENTILE_CONT()`, `PERCENTILE_DISC()` with `WITHIN GROUP`)** all
291  supported.
292- **CTEs:** `WITH` and `WITH RECURSIVE`. CTEs evaluate once per query.
293- **`TOP N`** is accepted alongside `LIMIT`.
294- **No AI scalar functions** (`AI_CLASSIFY`, `AI_SENTIMENT`, etc. — those
295  are Data Cloud federation features, not Hyper).
296- **No `ON CONFLICT` / `INSERT ... ON DUPLICATE KEY`.** Pass an array of
297  statements to `execute` and they run atomically inside a transaction:
298  ```
299  execute({ \"sql\": [
300    \"UPDATE settings SET value = 'dark' WHERE key = 'theme'\",
301    \"INSERT INTO settings (key, value) SELECT 'theme', 'dark'
302       WHERE NOT EXISTS (SELECT 1 FROM settings WHERE key = 'theme')\"
303  ]})
304  ```
305  Single-element arrays auto-commit (same as the legacy single-statement
306  shape). Mixing DDL with DML in one batch is rejected — Hyper aborts
307  such transactions with SQLSTATE 0A000. Issue DDL in its own `execute`
308  call. Do NOT include `BEGIN` / `COMMIT` / `ROLLBACK` / `SAVEPOINT` in
309  batch elements — the tool manages the transaction for you and these
310  are rejected up front.
311
312Full reference: https://developer.salesforce.com/docs/data/data-cloud-query-guide/references/dc-sql-reference
313
314## Examples
315
316```
317// Quickest path: query a file without loading it as a table
318query_file({
319  \"path\": \"/tmp/sales.csv\",
320  \"sql\": \"SELECT region, SUM(amount) FROM data GROUP BY region\"
321})
322
323// Inspect a file before committing to a load
324inspect_file({ \"path\": \"/tmp/sales.csv\" })
325
326// Loaded-table workflow (best when you'll run multiple queries)
327load_file({ \"path\": \"/tmp/sales.csv\", \"table\": \"sales\" })
328sample({ \"table\": \"sales\" })
329query({ \"sql\": \"SELECT region, SUM(amount) FROM sales GROUP BY region\" })
330
331// Cross-database join via attachment
332attach_database({ \"alias\": \"lookup\", \"path\": \"/data/dim.hyper\" })
333query({
334  \"sql\": \"SELECT s.region, d.country_name, SUM(s.amount) \
335          FROM sales s JOIN lookup.dim_region d ON s.region = d.code \
336          GROUP BY s.region, d.country_name\"
337})
338
339// Read Parquet directly inside a query — no load step
340query({
341  \"sql\": \"SELECT COUNT(*) FROM external('/tmp/events.parquet', format => 'parquet')\"
342})
343
344// Export a query result
345export({
346  \"sql\": \"SELECT * FROM sales WHERE amount > 1000\",
347  \"path\": \"/tmp/big_sales.parquet\",
348  \"format\": \"parquet\"
349})
350
351// Refresh existing rows + auto-add new columns (upsert by job_id).
352// Use this when you re-parsed source data with extra fields and want
353// to update the table in place without dropping it.
354load_file({
355  \"path\": \"/tmp/extract_failures-with-host.json\",
356  \"table\": \"extract_timing_failures\",
357  \"mode\": \"merge\",
358  \"merge_key\": \"job_id\"
359})
360
361// Single-statement execute (auto-commit, same as before)
362execute({
363  \"sql\": [\"DELETE FROM events WHERE created_at < CURRENT_DATE - INTERVAL '90' DAY\"]
364})
365
366// Atomic upsert — both statements commit together or both roll back
367execute({
368  \"sql\": [
369    \"UPDATE settings SET value = 'dark' WHERE key = 'theme'\",
370    \"INSERT INTO settings (key, value) SELECT 'theme', 'dark' \
371       WHERE NOT EXISTS (SELECT 1 FROM settings WHERE key = 'theme')\"
372  ],
373  \"database\": \"persistent\"
374})
375
376// Chart
377chart({
378  \"sql\": \"SELECT region, SUM(amount) AS total FROM sales GROUP BY region\",
379  \"path\": \"/tmp/sales_by_region.png\",
380  \"chart_type\": \"bar\"
381})
382```
383
384## Tips for picking the right tool
385
386- One-shot \"what's in this file?\" → `query_file` or `inspect_file`.
387- Repeated analysis on the same data → `load_file` once, then `query`.
388- File too large to fit in memory comfortably → `external()` inside
389  `query` (streams from disk).
390- Joining datasets across .hyper files → `attach_database` + `query`.
391- Materializing a query result as a new table → `copy_query`.
392- Need a picture for the user → `chart`.
393- Re-parsed source data with new columns and want to update existing
394  table in place → `load_file` with `mode: \"merge\"`.
395";