DruidStore — Apache Druid real-time OLAP store

Read this page in the documentation

DruidStore — Apache Druid real-time OLAP store Overview Apache Druid is a real-time OLAP / analytics datastore built for sub-second queries over large event streams. It is not a relational database: there are no classic cross-datasource joins, no row-level UPDATE/DELETE, and ingestion happens by submitting indexing tasks to an indexing service rather than by running INSERT. None of that fits the SQL-shaped Dialect interface, so DruidStore implements the minimal NoSqlStore marker interface (src/nosql/store.ts) — connection lifecycle plus a getClient() escape hatch — and talks to Druid's HTTP JSON API directly. Identity: Property | Value | --------- | --------- | name | 'druid' | library | 'fetch' | Druid has no single canonical npm client — the router/broker is just an HTTP/JSON API. Rather than depend on any package, the store speaks HTTP over the global fetch (hence library='fetch'), which is why the identity above reports 'fetch' rather than a driver name. Lazy-built client — no hard dependency There is no external driver to install. When no client is injected, connect() builds a tiny fetch-based client (via the exported createFetchClient(endpoint, headers)) against endpoint. Importing this module pulls in nothing beyond fetch. Injected client DruidStoreOptions accepts a pre-built client implementing the small DruidHttpClient shape. When provided, it is used verbatim and endpoint is ignored. This is how the test suite injects a mock (no network), and how callers can supply their own transport. Connection Build a store from connection options and call connect(): All connection options are optional: Option | Type | Purpose | ---------- | --------------------------- | ----------------------------------------------------------------------------------------- | endpoint | string | Base HTTP endpoint of the router/broker. Defaults to 'http://localhost:8888'. | client | DruidHttpClient | Pre-built HTTP client. When set it is used verbatim and endpoint/headers are ignored. | headers | Record<string, string> | Extra HTTP headers sent with every internal-fetch-client request. | Injected-client form Supply your own client (or a mock) to bypass the internal fetch client: Methods Internally every operation goes through a private post(path, body, action) that calls client.post(...); failures are wrapped in a DatabaseError (message prefixed Druid <action> failed: ...). Using any method before connect() (or after disconnect()) throws a ConnectionError. Lifecycle Method | Signature | Behavior | ------------- | -------------------------------- | ------------------------------------------------------------------------------------------- | connect | connect(): Promise<void> | Uses an injected client, otherwise builds a fetch-based client against endpoint. Idempotent. | disconnect | disconnect(): Promise<void> | Clears the client and connection state. | isConnected | isConnected(): boolean | true only when connected and a client is present. | getClient | getClient(): DruidHttpClient | Returns the underlying (internal or injected) client. Throws ConnectionError if not connected. | Query + ingestion Method | Signature | Endpoint | Behavior | ------------ | -------------------------------------------------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------- | query | query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> | POST /druid/v2/sql | Runs a Druid SQL query. Optional positional params are sent as { value } parameter objects. Returns the array of row objects as-is. | nativeQuery| nativeQuery<T = unknown>(spec: Record<string, unknown>): Promise<T> | POST /druid/v2 | Runs a native (JSON) Druid query. Returns the parsed response body (shape depends on the native query type). | ingest | ingest(target: string, rows: Array<Record<string, unknown>>): Promise<any> | POST /druid/indexer/v1/task | Ingests rows into datasource target by submitting an inline indexparallel task (rows JSON-encoded, newline-joined). Returns the task submission response (typically { task: '<id>' }). | submitTask | submitTask(spec: Record<string, unknown>): Promise<any> | POST /druid/indexer/v1/task | Submits an arbitrary task spec (e.g. a kill task). Returns the submission response. | Example Verification status Unit / mock-verified only. The tests in tests/nosql/druid.test.ts are fully mock-driven: an in-memory client implementing DruidHttpClient is injected via DruidStoreOptions.client; it records every POST (path + body) and returns canned Druid responses. There is no network and no live Druid cluster in the test run. What this proves: Each method POSTs to the correct path (/druid/v2/sql, /druid/v2, /druid/indexer/v1/task) with the expected body — including SQL parameters mapping and the inline indexparallel task spec (spec.ioConfig.inputSource.data newline-joined JSON). Return values are passed through as the store's method contracts promise. Lifecycle: connecting via an injected client, idempotent connect(), clean disconnect(), and ConnectionError before connect() (from both getClient() and query()). Error handling: client failures wrapped in DatabaseError. What this does not prove: live execution against a real Druid router/broker or indexing service. Request/response shapes are asserted against Druid's documented HTTP API, but end-to-end execution over the wire has not been exercised here. Related reading All data stores — the full catalogue, grouped by purpose Database types — where this sits among the 22 categories