@fjall/clickhouse
Runtime helpers for ClickHouse migration containers. Consumes the user manifest published by @fjall/components-infrastructure ClickHouseDatabase and provisions workload users in writable users_local storage. Also ships a generic SQL-file runner and a connection-retry helper.
Installation
npm install @fjall/clickhouse @clickhouse/client
@clickhouse/client is a peer dependency — the consumer supplies the runtime client.
Env-var contract
The framework's commitment is the env-var shape, not this package's API. A customer rolling their own consumer reads the same env vars.
| Env var | Shape | Producer |
|---|---|---|
CLICKHOUSE_MANAGED_USERS | JSON Array<string> of user names | ClickHouseDatabase.getMigrationContributions() |
USER_<NAME>_PASSWORD | plaintext (one per managed-users entry) | ECS executionRole + Secrets Manager (no runtime SDK call) |
The manifest carries names only — the schema admin (XML-defined) is NOT included, and profile binding is the customer SQL's job (ALTER USER <name> SETTINGS PROFILE '<profile>').
The constant name + env-name helper + schema live in @fjall/util/migration:
import {
CLICKHOUSE_MANAGED_USERS_ENV,
ManagedUserNamesSchema,
userPasswordEnvName,
} from "@fjall/util/migration";
This package re-exports them for convenience so consumers don't need two imports.
Usage
import {
connectWithRetry,
createConsoleMigrationLogger,
createFjallClickHouseClient,
provisionUsersFromEnv,
runSqlMigrations,
} from "@fjall/clickhouse";
const logger = createConsoleMigrationLogger("migration-runner");
const client = createFjallClickHouseClient({
url,
username,
password,
database,
});
await connectWithRetry(client, { label: "user-provision", logger });
await provisionUsersFromEnv({ client, logger });
await runSqlMigrations({ client, dir: "./clickhouse-init", logger });
Build the client with createFjallClickHouseClient. It installs FjallClickHouseLogger at WARN, which writes each of the client's log records through @fjall/util's logger as one masked line under the component ClickHouseClient. A failed request's record names the server's error code, its class and, for a code this package knows, its type, never the server's message: that message can quote the failing statement and its literal values. An argument that carries a request's or a response's content prints as [withheld: content], whatever it holds: the arguments @clickhouse/client 1.18.0 to 1.24.1 use for the statement, the request's URL parameters with their bound values, a request's or a response's headers, and an error's message (read 2026-10-13). A message or argument value that carries a terminal control sequence prints as [withheld: control characters], and an argument whose key carries one, or a character JSON writes as an escape, is left out. @clickhouse/client's own createClient uses its DefaultLogger, which prints the raw error and the request's arguments to the console; its default level is OFF at @clickhouse/client-common 1.18.0 and WARN from 1.18.1 (read 2026-10-13). If you build the client yourself, pass log: { LoggerClass: FjallClickHouseLogger, level: ClickHouseLogLevel.WARN }, stating the level so that no version in the peer range drops the warnings.
One path is outside any logger's reach. When the values stream of a streamed insert fails, @clickhouse/client (every release from 1.18.0 to 1.24.1, pipelineCb in dist/utils/encoder.js, read 2026-10-13) prints the stream's error to console.error itself, raw, whichever logger is installed. That error is the one the caller's own stream raised; or the one encoding a row threw, in the client's JSON.stringify or in a json.stringify you supplied (a BigInt, a circular row, whose message names a property, or a row whose own toJSON throws); or a fixed Node error (an AbortError, or socket hang up) when the upload is cut short or times out. Destroy a values stream with an error that carries no row data, and write it only rows that encode.
provisionUsersFromEnv is idempotent — it issues CREATE USER IF NOT EXISTS then ALTER USER for every manifest entry, so repeated invocations are safe.
Rolling your own consumer
The env-var contract above is stable. A customer who prefers their own provisioner reads the same env vars; this package's helpers are convenience wrappers. The framework commits to the env shape, not to this package.
If rolling your own, you'll want to replicate:
- single-quote-escape the password literal (
'→'') before string-interpolating intoIDENTIFIED WITH … BY '…' - mask passwords in any log output (use
maskSensitiveOutputfrom@fjall/util) - order
CREATE USER IF NOT EXISTSbeforeALTER USERso re-provisioning realigns drifted credentials - place provisioning BEFORE any migration-hash idempotency gate
- regex-validate user names before string-interpolating into SQL (CH doesn't bind identifier parameters)
Licence
Proprietary — see LICENSE.