druid SQL dialect (Apache Druid).druiddruidINSERT ... VALUES, UPDATE orDELETE, no transactions and no CREATE TABLE. A datasource is created by| Property | Value |
|---|---|
| Dialect name | druid |
| Extends | SQLiteDialect (sqlite) |
| Driver | none — global fetch |
| Cloud SDK | none |
| Default port | 8888 (the Router) |
| Local runtime | self-hosted; no image in this repo |
DruidDialect extends SQLiteDialect. SQLite's builders emitLIKE, LIMIT n OFFSET m and ? placeholders.? is exactly Druid's{ sql, values } that buildSelectQuery()$n, ILIKE= ANY(...), which Druid does not accept.) RqliteDialect has the samefetch transport.export class DruidDialect extends SQLiteDialect {
// name === 'druid'; library === 'fetch'
}DruidStore. src/nosql/druid already has a DruidStore. ItNoSqlStore for raw SQL, native JSON queries and nativeindex_parallel tasks. DruidDialect is the SQL Dialect that Prorm buildsdialect: 'druid', and it adds the ORM's query builders, typed parameters,:8888,fetch, accept extra headers and raise DatabaseError.$ npm install ts-prorm-orm$ pnpm add ts-prorm-orm$ yarn add ts-prorm-orm$ bun add ts-prorm-ormfetch.import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'druid',
host: 'druid-router.internal', // port defaults to 8888 (the Router)
});import { Prorm } from 'ts-prorm-orm';
export const db = new Prorm({
dialect: 'druid',
url: 'druid://druid-router.internal:8888',
});host, port, database, username and password before the dialect is built; any of those passed alongside it wins.import { DataTypes } from 'ts-prorm-orm';
const User = db.define('User', {
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING(255), allowNull: false, unique: true },
name: { type: DataTypes.STRING(100) },
});
await db.sync(); // CREATE TABLE — see Generated SQL below[!WARNING] db.sync()cannot create this table
CREATE TABLE is not supported on druid. Druid has no CREATE statements. A datasource is created by ingesting into it (buildInsertIngestion + submitIngestion).
import { Op } from 'ts-prorm-orm';
const page = await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});query(), captured with the driver stubbed out. The model layer inlines the values.[!WARNING] No row-level insert
INSERT ... VALUES is not supported on druid. Druid ingests in batches: buildInlineExtern() + buildInsertIngestion(), then submitIngestion()
[!WARNING] No row-level update
UPDATE is not supported on druid. Druid has no UPDATE. Rewrite the affected time range with REPLACE INTO ... OVERWRITE (buildReplaceIngestion) selecting from the datasource itself.
[!WARNING] No row-level delete
DELETE is not supported on druid. Druid has no row DELETE. Reindex without the rows: REPLACE INTO ... OVERWRITE with a SELECT that filters them out (buildReplaceIngestion).
[!WARNING] No upsert
Upsert is not supported on druid. Rewrite the time range with buildReplaceIngestion()
await User.findAll({
where: { name: { [Op.like]: 'A%' } },
order: [['id', 'ASC']],
limit: 20,
offset: 40,
});[!CAUTION] No transactions
Transactions is not supported on druid. Each query and each ingestion task stands alone
STRING → VARCHAR, auto-increment INTEGER → INTEGER, BOOLEAN → BOOLEAN, DATE → TIMESTAMP, UUID → VARCHAR, JSON → COMPLEX<json>. The type comparison lines the major engines up side by side.fetch is global from Node 18.import { Prorm } from 'ts-prorm-orm';
const orm = new Prorm({ dialect: 'druid', host: 'druid-router.internal', logging: false });
// or directly:
const d = new Prorm({
dialect: 'druid',
host: 'druid-router.internal', // port defaults to 8888 (the Router)
username: 'analyst', // HTTP Basic (basic-security extension)
password: process.env.DRUID_PASSWORD,
dialectOptions: {
protocol: 'https', // default 'http'
context: { sqlTimeZone: 'UTC' }, // query context on every request
},
});
await d.connect(); // probes GET /status/health| Option | Default | Effect |
|---|---|---|
url | — | Full base URL; takes precedence over protocol/host/port |
host, port, protocol | localhost, 8888, 'http' | Router (or Broker) address |
username, password | — | Authorization: Basic ... |
headers | — | Extra headers on every request (overrides Basic, same option as DruidStore) |
context | — | Query context merged into every query and ingestion |
queryTimeoutMs | 300000 | Client-side abort + cluster-side cancel, and the timeout context value |
fetch | global fetch | Injectable for tests and proxies |
| Call | Endpoint |
|---|---|
query(sql, { replacements }) | POST /druid/v2/sql |
cancel(sqlQueryId) | DELETE /druid/v2/sql/{sqlQueryId} |
connect() / ping() | GET /status/health (expects true) |
getDatabaseVersion() | GET /status → version |
submitIngestion(stmt) | POST /druid/v2/sql/task → { taskId, state } |
getTaskStatus(id) / waitForTask(id) | GET /druid/indexer/v1/task/{id}/status |
cancelTask(id) | POST /druid/indexer/v1/task/{id}/shutdown |
markDatasourceUnused(name) | DELETE /druid/indexer/v1/datasources/{name} (Overlord, via the Router) |
{ query, parameters, context, resultFormat: 'array', header: true, typesHeader: true, sqlTypesHeader: true }.fields[].type holds the SQLTIMESTAMP/DATE columns are returned as Date.sqlQueryId in its context. You can passprorm-<uuid> is generated. When queryTimeoutMs passes,DELETE /druid/v2/sql/{sqlQueryId} stops the query onDruidError is thrown.error, errorMessage,errorClass, errorCode, category, persona and host. These surface as aDruidError (a DatabaseError) that carries those fields and the HTTPstatus. A transport failure has status 0.{ type, value } parameter and is never spliced intotoDruidParameter):| JS value | Parameter |
|---|---|
| string | VARCHAR |
null / undefined | VARCHAR null |
| boolean | BOOLEAN |
safe integer, safe bigint | BIGINT |
| other finite number | DOUBLE |
Date | TIMESTAMP, 'yyyy-MM-dd HH:mm:ss.SSS' UTC |
| array of scalars | ARRAY (e.g. SCALAR_IN_ARRAY(col, ?), ARRAY_CONTAINS) |
| plain object | VARCHAR JSON (wrap as PARSE_JSON(?)) |
new DruidTypedValue('FLOAT', 1.5) | exactly as given |
NaN/Infinity, unsafe bigint, Buffer, Literal, nested arrays | refused |
:name replacements are rewritten to ? outside string literals, andDruidDialect extends SQLiteDialect (sqlite) andObject.getOwnPropertyNames(DruidDialect.prototype),| Area | Overridden |
|---|---|
| Connection and execution | commitTransaction, connect, disconnect, getConnection, isConnected, ping, query, queryStream, rollbackTransaction, startTransaction |
| DML builders | buildDeleteQuery, buildIncrementQuery, buildInlineExtern, buildInsertIngestion, buildInsertQuery, buildJsonContains, buildJsonExtract, buildJsonGroupArray, buildJsonGroupObject, buildJsonHasKey, buildJsonPatch, buildJsonPathQuery, buildJsonTable, buildLimitOffset, buildUpdateQuery, buildUpsertQuery, bulkInsert, escape |
| Types | getDataTypeSql |
| DDL and schema | addColumn, addConstraint, addForeignKey, addIndex, addPartition, analyze, attachDatabase, attachPartition, changeColumn, changeOwner, createComputedColumn, createConstraint, createDatabaseSQL, createExpressionIndex, createExtension, createForeignDataWrapper, createForeignServer, createForeignTable, createFulltextIndex, createIdentityColumn, createIndex, createMaterializedView, createPartialIndex, createPartition, createPartitionedTable, createPolicy, createProcedure, createRole, createSavepointSQL, createSchema, createSecurityPolicy, createSequence, createSpatialIndex, createStoredProcedure, createTable, createTrigger, createUser, createView, detachDatabase, detachPartition, disableRLS, dropConstraint, dropDatabaseSQL, dropExtension, dropForeignDataWrapper, dropForeignServer, dropIndex, dropMaterializedView, dropPartition, dropPolicy, dropProcedure, dropRole, dropSchema, dropSecurityPolicy, dropSequence, dropStoredProcedure, dropTable, dropTrigger, dropUser, dropView, enableRLS, enableRowLevelSecurity, refreshMaterializedView, removeColumn, removeConstraint, removeIndex, renameColumn, renameTable, vacuum |
| Introspection | assertQueryable, cancel, cancelTask, describeTable, getCreateTable, getDatabaseInfo, getDatabaseVersion, getExtensions, getMariaDBVersionInfo, getTableStatus, getTaskStatus, hasExtension, hasMaterializedView, hasPartition, hasPolicy, hasSequence, hasStoredProcedure, hasTrigger, listPartitions, listRoles, listSchemas, listSequences, listUsers, showAllSchemas, showConstraints, showIndexes, showMaterializedViews, showTables, showViews |
| Other | buildExtern, buildParameters, buildReplaceIngestion, buildRequestHeaders, bulkDelete, bulkUpdate, commentColumn, commentTable, executeStoredProcedure, fullTextSearch, grant, ingestionParts, markDatasourceUnused, nextSequenceValue, refuse, releaseSavepointSQL, renderExtract, revoke, rollbackToSavepointSQL, send, shapeResult, stAsText, stContains, stDWithin, stDistance, stGeomFromText, stIntersects, stWithin, submitIngestion, timestampLiteral, waitForTask |
| SQLite dialect | Druid dialect | |
|---|---|---|
| Offset without limit | LIMIT -1 OFFSET m | OFFSET m |
| Boolean / date literal | 1, '2024-...' | TRUE, TIMESTAMP '2024-01-01 00:00:00.000' |
| JSON extract | json_extract(...) | JSON_VALUE(col, '$.p' RETURNING VARCHAR) / JSON_QUERY |
| Types | SQLite affinities | VARCHAR, BIGINT, DOUBLE, FLOAT, BOOLEAN, TIMESTAMP, DATE, COMPLEX<json>; TIME/BLOB/GEOMETRY refused |
| Metadata | sqlite_master, PRAGMA | INFORMATION_SCHEMA.TABLES / COLUMNS / SCHEMATA (schema druid) |
__time is the primary timestamp column. TIME_FLOOR, TIME_PARSE and theLiterals. $iLike compiles to LIKE, as it does on SQLite, and Druid'sLIKE is case-sensitive because Druid has no ILIKE. ForLOWER(col) LIKE ... orICONTAINS_STRING(col, ?).const src = d.buildInlineExtern(
[{ ts: '2024-01-01T00:00:00Z', page: 'Main', added: 3 }],
[
{ name: 'ts', type: 'string' },
{ name: 'page', type: 'string' },
{ name: 'added', type: 'long' },
],
);
const stmt = d.buildInsertIngestion({
table: 'wikipedia',
select: `SELECT TIME_PARSE("ts") AS __time, "page", "added" FROM ${src}`,
partitionedBy: 'DAY',
clusteredBy: ['page'],
});
const { taskId } = await d.submitIngestion(stmt);
await d.waitForTask(taskId); // throws with Druid's errorMsg on FAILEDbuildExtern({ inputSource, inputFormat, signature }) buildsTABLE(EXTERN('<json>', '<json>', '<json>')) for any input source and format.string, long, float,double, ARRAY<...>, COMPLEX<...>).buildInlineExtern(rows, signature) builds the inline input source withbuildInsertIngestion({ table, select, values?, partitionedBy, clusteredBy? })INSERT INTO "t" <select> PARTITIONED BY <g> [CLUSTERED BY ...].select can be SQL text or SelectOptions.buildReplaceIngestion({ ..., overwrite }) builds REPLACE INTO "t" OVERWRITE
ALL, OVERWRITE WHERE __time >= TIMESTAMP '..' AND __time < TIMESTAMP '..'{ from, to }), or a raw { where } that must be on __time. DruidpartitionedBy is validated against the granularities Druid documents:HOUR, DAY, MONTH, YEAR, ALL, ALL TIME, the periods PT1S, PT1M,PT5M, PT10M, PT15M, PT30M, PT1H, PT6H, P1D, P1W, P1M, P3MP1Y, TIME_FLOOR(__time, '<period>'), and FLOOR(__time TO <unit>).submitIngestion() accepts only INSERT/REPLACE. query() refuses bothsubmitIngestion(), because /druid/v2/sql does not runUnsupportedSchemaObjectError, with the<X> is not supported on druid. <what to do instead>". query()SET ...; statements.| Call / statement | Instead |
|---|---|
UPDATE, buildUpdateQuery, buildIncrementQuery, bulkUpdate | buildReplaceIngestion over the time range |
DELETE, buildDeleteQuery, bulkDelete, TRUNCATE | REPLACE ... OVERWRITE with a filtering SELECT |
buildInsertQuery, bulkInsert, buildUpsertQuery, MERGE | buildInlineExtern + buildInsertIngestion / buildReplaceIngestion |
INSERT/REPLACE through query() | submitIngestion() |
CREATE ..., createTable, createPartitionedTable, createSchema | ingest into the datasource |
DROP ..., dropTable | markDatasourceUnused() (explicit soft delete), then a kill task |
ALTER, addColumn, removeColumn, changeColumn, renameColumn, renameTable | reingest / REPLACE |
transactions, savepoints (BEGIN, COMMIT, ROLLBACK, SAVEPOINT) | none |
| indexes (all kinds), constraints, foreign keys | none (Druid builds its own bitmap indexes) |
| sequences, triggers, views, materialized views, procedures | none |
| partitions API | PARTITIONED BY at ingestion |
RLS, policies, users, roles, GRANT/REVOKE | basic-security extension API |
extensions, FDW / foreign tables, ATTACH | server config / EXTERN |
vacuum, analyze, fullTextSearch, SQLite JSON helpers, ST_* helpers, .advanced | none |
showIndexes, showConstraints andshowMaterializedViews return [], and hasSequence, hasTrigger andfalse.fetch (tests/dialects/druid.test.ts).BOOLEAN parameters are sent as BOOLEAN, but Druid stores booleans asLONG. Comparing a stored 0/1 column to a boolean parameter may needCAST or a number.markDatasourceUnused uses the Overlord endpoint from the current docs./druid/coordinator/v1/datasources/{name} instead.queryStream uses the shared LIMIT/OFFSET pager, which re-runs the queryOFFSETs still generate thewaitForTask giving up does not stop the task. Use cancelTask() for that.capabilitiesOf() (src/transaction-support.ts),whereSupportOf() (src/query-builders/expression-render.ts),sqlObjectCapabilitiesOf() (src/sql-objects/capabilities.ts) and the| Capability | druid |
|---|---|
| Transactions atomic | no — each statement is its own request; requireAtomic refuses it |
| Savepoints | no — a nested transaction() throws |
| Isolation | up to SERIALIZABLE |
| Transaction modes | read-only no; deferrable no; constraint deferral no |
| RETURNING | refused — INSERT ... VALUES is not supported on druid. Druid ingests in batches: buildInlineExtern() + buildInsertIngestion(), then submitIngestion() |
| Upsert | refused — Upsert is not supported on druid. Rewrite the time range with buildReplaceIngestion() |
| JSON column | COMPLEX<json> (JSONB → COMPLEX<json>) |
| Vector column | VARCHAR |
| Array column | VARCHAR |
| WHERE builder | 32 operator keys; $and/$or/$not; $exists: yes; $in subquery: yes; expression operands: yes |
| SQL objects | family unknown: triggers no; procedures no; functions no; materialized views no; sequences no; enums check; RLS no |
| Streaming | queryStream() present; LISTEN absent; createExtension() present |
LIMIT/OFFSET, identifiers, dynamic parametersLIKE, no ILIKETIME_FLOOR, TIME_PARSEsqlQueryId, sqlTimeZonetimeoutEXTERN, INSERT, REPLACE, PARTITIONED BY, CLUSTERED BY/druid/v2/sql/task, task status/status/health, /status