# Contributing a config / check

A **declarative query config** is a monitoring check expressed as data — a plain TypeScript object that the dashboard validates against a [Zod schema](../../../apps/dashboard/src/lib/query-config/declarative/schema.ts) and renders as a table or card view. No JSX, no functions, no component imports.

> **Current status**: the declarative catalog is opt-in. Set `CHM_CONFIG_SOURCE=declarative` to route runtime lookups to it. The default (`CHM_CONFIG_SOURCE=ts`) still uses the TypeScript query configs. A drop-in community catalog directory (where contributions live outside the repo) is planned but not yet built. For now, contributing a check means adding a file to the repo's catalog directory and opening a PR.

---

## The contract

Every declarative config must satisfy the schema in:

```
apps/dashboard/src/lib/query-config/declarative/schema.ts
```

**Required fields:**

| Field | Type | Notes |
|---|---|---|
| `name` | `string` | Unique across the catalog. Used as the lookup key. |
| `sql` | `string \| VersionedSql[]` | A plain SQL string, or an array of `{ since, sql }` entries for version-aware queries. |
| `columns` | `string[]` | Column names returned by the query, in display order. Must have at least one entry. |

**Commonly used optional fields:**

| Field | Type | Notes |
|---|---|---|
| `description` | `string` | Short human-readable description shown in the dashboard. |
| `columnFormats` | `Record<string, ColumnFormat>` | Display format per column. Allowed values: `badge`, `duration`, `number`, `number-short`, `background-bar`, `colored-badge`, `boolean`, `code`, `text`, `none`, and others — see schema for the full list. |
| `columnDescriptions` | `Record<string, string>` | Tooltip text per column. |
| `defaultParams` | `Record<string, string \| number \| boolean>` | Default values for `{param: Type}` placeholders in the SQL. |
| `optional` | `boolean` | When `true`, the dashboard skips gracefully if the target table does not exist (default: `false`). |
| `tableCheck` | `string \| string[]` | Table(s) to check for existence before running the query. Useful with `optional: true`. |
| `refreshInterval` | `number` | Auto-refresh interval in milliseconds. |
| `relatedCharts` | `(string \| [string, Record])[]` | Chart names (or `[name, params]` tuples) to render above the table. |
| `card` | `{ primary?, badges?, metrics?, hidden? }` | Card view layout hints. |
| `defaultView` | `'table' \| 'cards' \| 'auto'` | Default display mode. |
| `docs` | `string` | Help text shown when the table is missing (a sentence with links — not required to be a bare URL). |
| `suggestion` | `string` | A short suggestion/tip rendered with the view. |
| `columnSizing` | `Record<string, { size?, minSize?, maxSize? }>` | Per-column width hints (pixels). |
| `tableBehavior` | `{ enableColumnResizing?, enableSorting?, … }` | Table interaction toggles. |
| `sortingFns` | `Record<string, SortingFn>` | Named sort function per column. Allowed: `sort_column_using_pct`, `sort_column_using_pct_inverted`, `sort_column_using_actual_value`. |
| `clickhouseSettings` | `Record<string, string \| number \| boolean>` | Execution-time ClickHouse settings applied per query (e.g. `{ allow_introspection_functions: 1 }`). |
| `rowStyle` | `{ rules: [{ when, className }], default? }` | Conditional row CSS classes — the declarative replacement for `rowClassName`. See [Conditional row styling](#conditional-row-styling). |
| `permission` | `{ feature, defaultAccess?, operation? }` | Feature-permission gate. See [Feature permissions](#feature-permissions). |
| `bulkActions` / `bulkActionKey` | `string[]` / `string` | Bulk row actions and the row key they operate on. |

**Fields intentionally absent from the declarative format** (runtime-only, cannot be serialized):
`expandable`, `columnIcons`, `filterSchema`.
If your check needs any of these, it must stay as a TypeScript config for now.

> Two former exclusions are now expressible as data: `rowClassName` via
> [`rowStyle`](#conditional-row-styling) rules, and `permission` via the
> [`permission`](#feature-permissions) field.

---

## Safety rule: SELECT only

All checks must be read-only. The schema validator and the SQL validator in
`apps/dashboard/src/lib/api/shared/validators/sql.ts` reject DDL and DML
(`CREATE`, `INSERT`, `DROP`, `ALTER`, `TRUNCATE`, etc.). Do not set
`disableSqlValidation: true` on contributed checks.

---

## Worked example

A minimal check that surfaces the last 100 slow queries. Based on the real `query-count-baseline` pattern in the anomaly catalog:

```typescript
import type { DeclarativeQueryConfig } from '../../schema'

export const slowQueriesRecentDeclarative: DeclarativeQueryConfig = {
  name: 'slow-queries-recent',
  description: 'Queries that took longer than 5 seconds in the last hour',
  sql: [
    {
      since: '23.8',
      sql: `
        SELECT
          query_id,
          user,
          query_duration_ms,
          formatReadableTimeDelta(query_duration_ms / 1000) AS readable_duration,
          round(memory_usage / 1048576, 1) AS memory_mb,
          left(query, 200) AS query_snippet,
          event_time
        FROM system.query_log
        WHERE type = 'QueryFinish'
          AND query_duration_ms > 5000
          AND event_time >= now() - INTERVAL 1 HOUR
        ORDER BY query_duration_ms DESC
        LIMIT 100
      `,
    },
  ],
  columns: [
    'query_id',
    'user',
    'query_duration_ms',
    'readable_duration',
    'memory_mb',
    'query_snippet',
    'event_time',
  ],
  columnFormats: {
    query_duration_ms: 'number',
    memory_mb: 'number',
    query_snippet: 'code',
  },
  columnDescriptions: {
    query_duration_ms: 'Wall-clock duration in milliseconds',
    memory_mb: 'Peak memory usage in MiB',
  },
  defaultParams: {},
  optional: false,
}
```

The field names above (`name`, `sql[].since`, `sql[].sql`, `columns`,
`columnFormats`, `columnDescriptions`, `optional`) map directly to
`declarativeQueryConfigSchema` in `schema.ts`.

---

## Step-by-step: adding a check

1. **Pick the right domain directory.** Existing domains under
   `apps/dashboard/src/lib/query-config/declarative/catalog/`:
   `anomaly`, `explorer`, `keeper`, `logs`, `merges`, `more`, `queries`,
   `security`, `system`, `tables`. Add a new file to the closest domain, or
   create a new subdirectory if the check doesn't fit any existing one.

2. **Write the config.** Export a `const` typed as `DeclarativeQueryConfig`.
   Use `sql: [{ since: 'X.Y', sql: '...' }]` when querying a system column
   that was added in a specific ClickHouse version (check
   `docs/clickhouse-schemas/tables/` for column availability). Use a plain
   `sql: '...'` string when the query works across all supported versions.

3. **Register it in the catalog index.** Import your export in
   `apps/dashboard/src/lib/query-config/declarative/catalog/index.ts` and add
   it to the `ALL_DECLARATIVE` array. The index enforces unique names at module
   load time and throws if a duplicate is detected.

4. **Validate locally.**
   ```bash
   # Validate schema + run declarative catalog tests
   bun run test:query-config

   # Biome lint (must pass before PR)
   bun run check
   ```
   The `test:query-config` suite runs the schema validator against every catalog
   entry and compares serializable fields against the legacy TS configs for
   migrated checks.

5. **Note the opt-in flag.** Your check is live in the catalog but the
   dashboard only reads from it when `CHM_CONFIG_SOURCE=declarative` is set in
   the environment. The default is `ts`. Document this in your PR if relevant.

6. **Open a PR** against `main`. The CI `unit-tests` job runs the full bun test
   suite including the declarative catalog tests.

---

## Version-aware SQL

When a system table column was introduced in a specific ClickHouse release,
use the `sql` array instead of a plain string. The loader picks the entry whose
`since` version is the highest that is still ≤ the connected cluster's version.

```typescript
sql: [
  {
    since: '23.8',
    sql: `SELECT col1 FROM system.query_log LIMIT 10`,
  },
  {
    since: '24.1',
    sql: `SELECT col1, new_col FROM system.query_log LIMIT 10`,
    description: 'Added new_col (available from 24.1)',
  },
],
```

Version strings must match `X.Y` or `X.Y.Z` (e.g. `"23.8"`, `"24.1"`,
`"25.6.0"`). The `description` field on each entry is optional but encouraged.

---

## Optional tables

If your check queries a table that may not exist (e.g. `system.backup_log`,
`system.zookeeper`, `system.error_log`), set `optional: true` and provide a
`tableCheck`:

```typescript
optional: true,
tableCheck: 'system.backup_log',
```

The dashboard will surface a graceful "table not available" message instead of
an error.

---

## Conditional row styling

To highlight rows based on their data (e.g. errored or stuck rows), use
`rowStyle` instead of a `rowClassName` function. It is an ordered list of
`{ when, className }` rules; the **first** matching rule's `className` wins, and
`default` (or nothing) applies when no rule matches.

```typescript
rowStyle: {
  rules: [
    // first match wins — put the most severe condition first
    { when: { column: 'is_stuck', op: 'truthy' }, className: 'bg-red-50 dark:bg-red-950/20' },
    {
      when: {
        all: [
          { column: 'is_done', op: 'falsy' },
          { column: 'elapsed', op: 'gt', value: 300 },
        ],
      },
      className: 'bg-amber-50 dark:bg-amber-950/20',
    },
  ],
  default: '',
},
```

**Condition operators:**

| Operator | Meaning | Coercion |
|---|---|---|
| `gt` / `gte` / `lt` / `lte` | numeric comparison against `value` | cell coerced with `Number()` |
| `truthy` / `falsy` | numeric truthiness (non-zero / zero) | cell coerced with `Number()` |
| `nonempty` / `empty` | string is non-empty / empty | cell coerced with `String()` |
| `all` / `any` | AND / OR of nested conditions | — |

Comparison operators (`gt`/`gte`/`lt`/`lte`) require a numeric `value`.
`className` is any Tailwind class string.

---

## Feature permissions

To gate a view behind a feature, set `permission`. This is plain data (no
import needed) mirroring the app's `FeaturePermission`:

```typescript
permission: { feature: 'queries' },
// or, with explicit access/operation:
permission: { feature: 'metrics', defaultAccess: 'authenticated', operation: 'read' },
```

`feature` must be one of the known feature ids (`overview`, `agent`, `insights`,
`health`, `queries`, `tables`, `metrics`, `dashboard`, `security`, `logs`,
`settings`, `cluster`, `operations`, `actions`, `mcp`, `peerdb`, `docs`,
`about`). `defaultAccess` is `public` | `authenticated`; `operation` is
`read` | `write`. Both are optional.
