Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion e2e/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ describe("e2e", () => {

test("multi-row query with markdown format", async () => {
const result =
await $`bun ${CLI_PATH} -q "SELECT number AS n FROM system.numbers LIMIT 3" -F markdown`.text();
await $`bun ${CLI_PATH} -q "SELECT 0 AS n UNION ALL SELECT 1 UNION ALL SELECT 2" -F markdown`.text();
expect(result).toContain("| n |");
expect(result).toContain("| 0 |");
expect(result).toContain("| 1 |");
Expand Down
35 changes: 32 additions & 3 deletions skills/clickhouse-query/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,35 @@ chcli -q "SELECT 1"

## Connection

Set connection details via environment variables (preferred for agent use) or CLI flags.
Set connection details via named environments (recommended), environment variables, or CLI flags.

### Named Environments

Named environments store connection profiles in `~/.config/chcli/config.json`. This is the recommended approach when working with multiple ClickHouse instances.

```bash
# Save a named environment
chcli env add prod --host ch.prod.com --port 8443 --secure -u admin --password secret

# Use it for a query
chcli -e prod -q "SELECT count() FROM events"

# Or set it as the default for the current directory
chcli env use prod
chcli -q "SELECT count() FROM events" # uses prod automatically
```

Manage environments with `chcli env`:

| Subcommand | Description |
|------------|-------------|
| `env add <name>` | Add or update an environment (merges with existing) |
| `env list` / `env ls` | List all environments |
| `env show <name>` | Show environment details (passwords masked) |
| `env remove <name>` / `env rm` | Remove an environment |
| `env use <name>` | Set default environment for the current directory |

### Environment Variables

| Flag | Env Var | Alt Env Var | Default |
|------|---------|-------------|---------|
Expand All @@ -47,10 +75,10 @@ Set connection details via environment variables (preferred for agent use) or CL
### Resolution Order

```
CLI flag > Individual env var > CLICKHOUSE_URL (parsed) > Default value
CLI flag > Named environment (--env or folder default) > Individual env var > CLICKHOUSE_URL (parsed) > Default value
```

For agent workflows, prefer setting env vars in a `.env` file (Bun loads `.env` automatically) or using a secrets manager like Doppler so every invocation uses the same connection without repeating flags.
For agent workflows, prefer setting env vars in a `.env` file (Bun loads `.env` automatically), using named environments, or a secrets manager like Doppler so every invocation uses the same connection without repeating flags.

See `references/connection.md` for detailed connection examples.

Expand Down Expand Up @@ -141,6 +169,7 @@ bunx @obsessiondb/chcli -q "SELECT * FROM events LIMIT 1000" -F json > export.js

| Flag | Description |
|------|-------------|
| `-e, --env <name>` | Use a named environment (overrides folder default) |
| `-t, --time` | Print execution time to stderr |
| `-v, --verbose` | Print query metadata (format, elapsed time) to stderr |
| `--help` | Show help text |
Expand Down
48 changes: 47 additions & 1 deletion skills/clickhouse-query/references/connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ chcli connects to ClickHouse over HTTP(S). Connection details can be set via env
## Resolution Order

```
CLI flag > Individual env var > CLICKHOUSE_URL (parsed) > Default value
CLI flag > Named environment (--env or folder default) > Individual env var > CLICKHOUSE_URL (parsed) > Default value
```

When a named environment is active (via `--env`/`-e` flag or a folder default set with `chcli env use`), its values take precedence over environment variables but are overridden by explicit CLI flags.

When `CLICKHOUSE_URL` is set (e.g. `https://host:8443`), it is parsed into host, port, secure, and password. These parsed values are used as fallbacks only when the corresponding individual env var is not set.

## Configuration Options
Expand All @@ -20,6 +22,7 @@ When `CLICKHOUSE_URL` is set (e.g. `https://host:8443`), it is parsed into host,
| `--password <pass>` | `CLICKHOUSE_PASSWORD` | | *(empty)* | Authentication password |
| `-d, --database <db>` | `CLICKHOUSE_DATABASE` | `CLICKHOUSE_DB` | `default` | Default database for queries |
| `-s, --secure` | `CLICKHOUSE_SECURE` | | `false` | Use HTTPS instead of HTTP |
| `-e, --env <name>` | *(none)* | | *(none)* | Use a named environment |
| *(none)* | `CLICKHOUSE_URL` | | *(none)* | Full connection URL (parsed into host, port, secure, password) |

## Connection URL
Expand All @@ -32,6 +35,49 @@ chcli constructs the connection URL as:

Where `protocol` is `https` if `--secure` is set or `CLICKHOUSE_SECURE=true`, otherwise `http`.

## Named Environments

Named environments store connection profiles locally in `~/.config/chcli/config.json`. Use them to switch between multiple ClickHouse instances without changing env vars.

### Managing Environments

```bash
# Add a new environment
chcli env add prod --host ch.prod.com --port 8443 --secure -u admin --password secret -d analytics

# Add another
chcli env add staging --host ch.staging.com --port 8443 --secure -u dev

# List all environments
chcli env list

# Show details (passwords are masked)
chcli env show prod

# Update an existing environment (merges — only overwrites fields you pass)
chcli env add prod --password new-secret

# Remove an environment
chcli env remove staging
```

### Using Named Environments

```bash
# Specify per-query with --env / -e
chcli -e prod -q "SELECT count() FROM events"

# Set a default for the current directory
chcli env use prod
chcli -q "SELECT count() FROM events" # uses prod automatically

# --env flag overrides the folder default
chcli -e staging -q "SELECT count() FROM events"

# CLI flags still override named environment values
chcli -e prod -d other_db -q "SHOW TABLES"
```

## Examples

### Local Development (defaults)
Expand Down
11 changes: 11 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,16 @@ describe("parseCliArgs", () => {
expect(config.file).toBeUndefined();
expect(config.host).toBeUndefined();
expect(config.format).toBeUndefined();
expect(config.env).toBeUndefined();
});

test("parses --env flag", () => {
const config = parseCliArgs(["--env", "prod", "-q", "SELECT 1"]);
expect(config.env).toBe("prod");
});

test("parses -e shorthand for --env", () => {
const config = parseCliArgs(["-e", "staging", "-q", "SELECT 1"]);
expect(config.env).toBe("staging");
});
});
5 changes: 5 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const options = {
password: { type: "string" },
database: { type: "string", short: "d" },
secure: { type: "boolean", short: "s", default: false },
env: { type: "string", short: "e" },
format: { type: "string", short: "F" },
time: { type: "boolean", short: "t", default: false },
verbose: { type: "boolean", short: "v", default: false },
Expand Down Expand Up @@ -44,11 +45,15 @@ Connection:
--password <pass> Password (env: CLICKHOUSE_PASSWORD, default: "")
-d, --database <db> Database (env: CLICKHOUSE_DATABASE or CLICKHOUSE_DB, default: default)
-s, --secure Use HTTPS (env: CLICKHOUSE_SECURE)
-e, --env <name> Use a named environment (overrides folder default)

CLICKHOUSE_URL is also supported (e.g. https://host:8443) and will be
used for host, port, secure, and password if the individual env vars
are not set.

Environments can be configured with \`chcli env add <name>\`.
Set a default environment for the current folder with \`chcli env use <name>\`.

Output:
-F, --format <fmt> Output format (json, jsonl, csv, tsv, pretty, vertical, markdown, sql)
-t, --time Print execution time to stderr
Expand Down
61 changes: 61 additions & 0 deletions src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,64 @@ describe("resolveConnectionConfig", () => {
});
});
});

describe("resolveConnectionConfig with named environment", () => {
const emptyConfig = {} as Parameters<typeof resolveConnectionConfig>[0];
const emptyEnv = {};

test("named environment provides connection settings", () => {
const result = resolveConnectionConfig(emptyConfig, emptyEnv, {
host: "named.host.com",
port: "9000",
user: "named_user",
password: "named_pass",
database: "named_db",
secure: true,
});
expect(result).toEqual({
url: "https://named.host.com:9000",
username: "named_user",
password: "named_pass",
database: "named_db",
});
});

test("named environment takes precedence over env vars", () => {
const result = resolveConnectionConfig(
emptyConfig,
{ CLICKHOUSE_HOST: "env-host", CLICKHOUSE_USER: "env_user" },
{ host: "named.host.com", user: "named_user" },
);
expect(result.url).toContain("named.host.com");
expect(result.username).toBe("named_user");
});

test("CLI flags take precedence over named environment", () => {
const config = {
host: "flag-host",
user: "flag_user",
} as Parameters<typeof resolveConnectionConfig>[0];

const result = resolveConnectionConfig(config, emptyEnv, {
host: "named.host.com",
user: "named_user",
});
expect(result.url).toContain("flag-host");
expect(result.username).toBe("flag_user");
});

test("named environment url is parsed for host/port/secure", () => {
const result = resolveConnectionConfig(emptyConfig, emptyEnv, {
url: "https://url-host.com:8443",
});
expect(result.url).toBe("https://url-host.com:8443");
});

test("named environment host takes precedence over named url", () => {
const result = resolveConnectionConfig(emptyConfig, emptyEnv, {
url: "https://url-host.com:8443",
host: "explicit-host.com",
});
expect(result.url).toContain("explicit-host.com");
});
});
30 changes: 26 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createClient } from "@clickhouse/client";
import type { CliConfig } from "./cli";
import type { Environment } from "./config";

export function parseClickHouseUrl(raw: string) {
const url = new URL(raw);
Expand All @@ -14,17 +15,32 @@ export function parseClickHouseUrl(raw: string) {
export function resolveConnectionConfig(
config: CliConfig,
env: Record<string, string | undefined> = process.env,
namedEnv?: Environment,
) {
// Parse CLICKHOUSE_URL from env vars
const parsed = env.CLICKHOUSE_URL
? parseClickHouseUrl(env.CLICKHOUSE_URL)
: undefined;

// Parse url from named environment
const namedParsed = namedEnv?.url
? parseClickHouseUrl(namedEnv.url)
: undefined;

// Priority: CLI flags > named env > env vars > CLICKHOUSE_URL > defaults
const host =
config.host || env.CLICKHOUSE_HOST || parsed?.host || "localhost";
config.host ||
namedEnv?.host || namedParsed?.host ||
env.CLICKHOUSE_HOST || parsed?.host ||
"localhost";
const port =
config.port || env.CLICKHOUSE_PORT || parsed?.port || "8123";
config.port ||
namedEnv?.port || namedParsed?.port ||
env.CLICKHOUSE_PORT || parsed?.port ||
"8123";
const secure =
config.secure ||
namedEnv?.secure || (namedParsed?.secure ?? false) ||
env.CLICKHOUSE_SECURE === "true" ||
(parsed?.secure ?? false);
const protocol = secure ? "https" : "http";
Expand All @@ -33,22 +49,28 @@ export function resolveConnectionConfig(
url: `${protocol}://${host}:${port}`,
username:
config.user ||
namedEnv?.user ||
env.CLICKHOUSE_USER ||
env.CLICKHOUSE_USERNAME ||
"default",
password:
config.password ||
namedEnv?.password || namedParsed?.password ||
env.CLICKHOUSE_PASSWORD ||
parsed?.password ||
"",
database:
config.database ||
namedEnv?.database ||
env.CLICKHOUSE_DATABASE ||
env.CLICKHOUSE_DB ||
"default",
};
}

export function createClickHouseClient(config: CliConfig) {
return createClient(resolveConnectionConfig(config));
export function createClickHouseClient(
config: CliConfig,
namedEnv?: Environment,
) {
return createClient(resolveConnectionConfig(config, process.env, namedEnv));
}
Loading
Loading