From d931a30ea4361f9c19d3e8eaaf57a45f7588e7a4 Mon Sep 17 00:00:00 2001 From: PasinduOshada <119439994+PasinduOshada@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:22:15 +0530 Subject: [PATCH] Add Drizzle ORM integration page Closes #70 Drizzle is a type-safe TypeScript SQL ORM. It has no QuestDB dialect, but reaches QuestDB over PGWire through node-postgres, which is already a tested JavaScript client. Document it the way it actually works against a time-series database: describe existing tables rather than migrating into them, use the core select API for filtering, drop to the sql template for SAMPLE BY, LATEST ON and ASOF JOIN, and ingest over ILP with the official client rather than row-by-row INSERT. Include a QuestDB-to-Drizzle column type mapping, and a limitations section covering drizzle-kit migrations, the absent DELETE statement, the lack of primary and foreign keys, and transaction semantics. Register the page in the Other Tools sidebar and the integrations overview. --- documentation/integrations/other/drizzle.md | 218 ++++++++++++++++++++ documentation/integrations/overview.md | 2 + documentation/sidebars.js | 1 + 3 files changed, 221 insertions(+) create mode 100644 documentation/integrations/other/drizzle.md diff --git a/documentation/integrations/other/drizzle.md b/documentation/integrations/other/drizzle.md new file mode 100644 index 0000000000..b8501c9aef --- /dev/null +++ b/documentation/integrations/other/drizzle.md @@ -0,0 +1,218 @@ +--- +title: Drizzle ORM +description: Guide for using Drizzle ORM with QuestDB +--- + +[Drizzle ORM](https://orm.drizzle.team/) is a lightweight, type-safe SQL ORM for +TypeScript and JavaScript. Schemas are declared in TypeScript, row types are +inferred from those declarations, and the SQL it emits stays close enough to +what you wrote that you can predict it. That last property is what makes it a +good fit for QuestDB, where the useful queries are time-series SQL rather than +object graphs. + +There is no QuestDB-specific Drizzle dialect. Drizzle connects through the +[PostgreSQL wire protocol](/docs/connect/compatibility/pgwire/overview/) using +`node-postgres` (`pg`), which is one of the JavaScript clients QuestDB is +[tested against](/docs/connect/compatibility/pgwire/nodejs/). + +:::tip + +Use Drizzle to **query** QuestDB. For **ingestion**, use the official +[QuestDB JavaScript client](/docs/connect/clients/nodejs/) over the InfluxDB +Line Protocol, which is much faster than row-by-row `INSERT` over PGWire. Both +libraries can live in the same application — see +[Ingesting data](#ingesting-data). + +::: + +## Prerequisites + +- Node.js 18 or later +- A running QuestDB instance — see the + [quick start](/docs/getting-started/quick-start/) +- QuestDB's PGWire port, `8812` by default + +## Installation + +```shell +npm install drizzle-orm pg +npm install -D @types/pg +``` + +## Connecting + +```typescript +import { drizzle } from "drizzle-orm/node-postgres" +import { Pool } from "pg" + +// QuestDB returns timestamps in UTC; set the process timezone to match so +// the pg driver does not shift them into local time. +process.env.TZ = "UTC" + +const pool = new Pool({ + host: "127.0.0.1", + port: 8812, + user: "admin", + password: "quest", + database: "qdb", +}) + +const db = drizzle(pool) +``` + +## Describing an existing table + +Create tables with QuestDB DDL, then describe them in Drizzle to get typed +queries. The Drizzle schema is a *description* of a table that already exists, +not something you migrate into place: + +```questdb-sql title="Created in QuestDB" +CREATE TABLE trades ( + timestamp TIMESTAMP, + symbol SYMBOL, + side SYMBOL, + price DOUBLE, + amount DOUBLE +) TIMESTAMP(timestamp) PARTITION BY DAY; +``` + +```typescript title="Described in Drizzle" +import { pgTable, timestamp, doublePrecision, text } from "drizzle-orm/pg-core" + +export const trades = pgTable("trades", { + timestamp: timestamp("timestamp"), + // SYMBOL is a QuestDB type with no Drizzle equivalent. It arrives over + // PGWire as text, so declare it as text(). + symbol: text("symbol"), + side: text("side"), + price: doublePrecision("price"), + amount: doublePrecision("amount"), +}) +``` + +Column type mapping: + +| QuestDB | Drizzle `pg-core` | +| ------- | ----------------- | +| `TIMESTAMP` | `timestamp()` | +| `SYMBOL` | `text()` | +| `VARCHAR` | `text()` or `varchar()` | +| `DOUBLE` | `doublePrecision()` | +| `FLOAT` | `real()` | +| `LONG` | `bigint({ mode: "number" })` | +| `INT` | `integer()` | +| `SHORT` | `smallint()` | +| `BOOLEAN` | `boolean()` | +| `UUID` | `uuid()` | + +## Querying + +The core select API works as it does against PostgreSQL: + +```typescript +import { and, desc, eq, gt } from "drizzle-orm" + +// Filter and order +const recent = await db + .select() + .from(trades) + .where(and(eq(trades.symbol, "BTC-USD"), gt(trades.price, 30000))) + .orderBy(desc(trades.timestamp)) + .limit(100) + +// Project a subset of columns +const prices = await db + .select({ ts: trades.timestamp, price: trades.price }) + .from(trades) + .where(eq(trades.side, "buy")) +``` + +## Time-series SQL + +QuestDB's time-series extensions — [`SAMPLE BY`](/docs/query/sql/sample-by/), +[`LATEST ON`](/docs/query/sql/latest-on/) and +[`ASOF JOIN`](/docs/query/sql/join/) — have no equivalent in the Drizzle query +builder. Reach for the `sql` template and `db.execute()`, which keeps +parameters bound rather than interpolated: + +```typescript +import { sql } from "drizzle-orm" + +// Hourly OHLC buckets +const candles = await db.execute(sql` + SELECT + timestamp, + first(price) AS open, + max(price) AS high, + min(price) AS low, + last(price) AS close + FROM trades + WHERE symbol = ${"BTC-USD"} + SAMPLE BY 1h +`) + +console.log(candles.rows) + +// Most recent row per symbol +const latest = await db.execute(sql` + SELECT * FROM trades LATEST ON timestamp PARTITION BY symbol +`) +``` + +## Ingesting data + +Write with the official client over ILP, and read back through Drizzle: + +```shell +npm install @questdb/nodejs-client +``` + +```typescript +import { Sender } from "@questdb/nodejs-client" + +const sender = Sender.fromConfig("http::addr=localhost:9000") + +await sender + .table("trades") + .symbol("symbol", "BTC-USD") + .symbol("side", "buy") + .floatColumn("price", 30123.5) + .floatColumn("amount", 0.25) + .at(Date.now(), "ms") + +await sender.flush() +await sender.close() +``` + +Tables and columns are created automatically on first write, so in many +projects the QuestDB DDL above is optional and the Drizzle schema simply +describes what ILP created. + +## Limitations + +QuestDB is a time-series database, not a general-purpose relational one. The +parts of Drizzle that assume PostgreSQL semantics do not carry over: + +- **No `drizzle-kit` migrations.** `generate`, `push` and `migrate` emit + PostgreSQL DDL — primary keys, sequences, and `ALTER TABLE` forms QuestDB + does not implement. Manage schema with + [QuestDB DDL](/docs/query/sql/create-table/) instead. +- **No `DELETE`.** QuestDB has no `DELETE` statement, so `db.delete()` fails. + Drop whole partitions or set a [TTL](/docs/concepts/ttl/) for retention. +- **No primary or foreign keys.** The + [designated timestamp](/docs/concepts/designated-timestamp/) is not a primary + key and does not enforce uniqueness — use + [deduplication](/docs/concepts/deduplication/) for that. Because Drizzle's + relational query API (`db.query..findMany`) builds on declared + relations, use the core select API and explicit joins. +- **Limited transaction semantics.** Do not rely on `db.transaction()` for + rollback; see the + [PGWire limitations](/docs/connect/compatibility/pgwire/overview/). +- **`UPDATE` is supported** but is not the intended write path. Prefer + append-only ingestion. + +## See also + +- [Drizzle ORM documentation](https://orm.drizzle.team/docs/overview) +- [QuestDB PGWire guide for JavaScript](/docs/connect/compatibility/pgwire/nodejs/) +- [QuestDB JavaScript client](/docs/connect/clients/nodejs/) diff --git a/documentation/integrations/overview.md b/documentation/integrations/overview.md index 50f8ed528c..9142fd819f 100644 --- a/documentation/integrations/overview.md +++ b/documentation/integrations/overview.md @@ -69,6 +69,8 @@ Improve your interactions with QuestDB using these tools and interfaces: analyze monitoring metrics. - [SQLAlchemy](/docs/integrations/other/sqlalchemy/): Utilize Python's ORM capabilities for database interactions. +- [Drizzle ORM](/docs/integrations/other/drizzle/): Query QuestDB from + TypeScript with a type-safe SQL query builder. - [MindsDB](/docs/integrations/other/mindsdb/): Build machine learning models for predictive analytics on [time-series data](/blog/what-is-time-series-data/). - [Databento](/docs/integrations/other/databento/): Ingest a normalized live diff --git a/documentation/sidebars.js b/documentation/sidebars.js index f0b4f587fd..2babadb59a 100644 --- a/documentation/sidebars.js +++ b/documentation/sidebars.js @@ -935,6 +935,7 @@ module.exports = { items: [ "integrations/other/prometheus", "integrations/other/sqlalchemy", + "integrations/other/drizzle", "integrations/other/mindsdb", "integrations/other/databento", "integrations/other/cube",