Drizzle ORM & Generator
@kalamdb/orm is the Drizzle package for KalamDB. Use it when you want generated tables, typed queries, or table-level live helpers. It runs on top of @kalamdb/client.
Install it with the client and Drizzle:
Driver quick start
Copy this after you have a generated schema.ts file.
The driver uses KalamDB SQL over HTTP and normalizes temporal values for Drizzle columns. timestamp(..., { mode: 'date' }), date(..., { mode: 'date' }), and time(...) can read KalamDB’s numeric wire values without app-side parsing.
Pass a full KalamDBClient to kalamDriver() so FILE uploads can route to queryWithFiles() when needed.
Table helpers
Use kTable instead of raw pgTable so the schema retains KalamDB table type metadata.
Available table helpers:
| Helper | Use for |
|---|---|
kTable.shared(name, columns, options?) | WITH (TYPE = 'SHARED') tables |
kTable.user(name, columns, options?) | per-user isolated tables |
kTable.stream(name, columns, options?) | stream tables |
kTable.system(name, columns, options?) | system metadata tables |
kSystemColumns([...]) | _seq, _deleted, and _commit_seq typed hidden columns |
KalamDB-specific columns:
| Helper | Runtime value |
|---|---|
file(name) | `FileRef |
bytes(name) | `Uint8Array |
embedding(name, dimensions) | `number[] |
Generate schema.ts
Generate a Drizzle schema from a running KalamDB server:
The generator reads SHOW TABLES, calls DESCRIBE <namespace.table> when column metadata is incomplete, and emits only the imports needed by the generated tables. It preserves primary keys, not-null constraints, system-column options, comments, table config constants such as chat_messagesConfig, and $inferSelect / $inferInsert aliases.
Options:
| Option | Description |
|---|---|
--namespace <name> | Limit output to one or more namespaces. Repeat it or pass comma-separated names. |
--include-system | Include system and dba tables. |
--include-system-columns <mode> | Add typed hidden columns to generated table definitions. Use all, _seq, or _deleted. |
--bigint-mode <mode> | Choose how generated BIGINT columns are emitted. Accepted values are string, bigint, or number; default is string. |
--no-type-aliases | Skip generated $inferSelect and $inferInsert aliases. |
BIGINT defaults to text() because KalamDB transports Int64 values as strings to preserve precision. Choose --bigint-mode bigint when you want Drizzle to coerce values to native bigint, or --bigint-mode number only when values fit safely in JavaScript numbers.
Keep schema.ts fresh during local development
Add a generator script to your app so the command stays short:
In a kalam.toml project, kalam dev watches schema.sql and reruns kalam schema gen when [dev].generate_types = true:
See Local Development. The legacy kalam --watch-schema server-polling flag is deprecated.
PostgreSQL types and Drizzle mapping
KalamDB schemas use PostgreSQL-oriented SQL types. That is why standard typed columns come from drizzle-orm/pg-core: text, integer, boolean, numeric, timestamp, and related builders describe the server columns. kTable adds KalamDB table metadata. Dart and Rust use the same server type model, with their own language representations.
The table below separates recommended SQL declarations from the current generator’s output. A generated JavaScript value does not change the server column’s SQL type. See PostgreSQL data types and compatibility limits.
| KalamDB type | Generated Drizzle helper | Read behavior |
|---|---|---|
BOOLEAN | boolean() | boolean |
INT | integer() | number |
SMALLINT | smallint() | number |
BIGINT | text() by default | string for Int64 precision |
DOUBLE PRECISION | doublePrecision() | number |
REAL | real() | number |
TEXT | text() | string |
TIMESTAMP | timestamp(..., { mode: 'date' }) | Date when using date mode |
DATE | date(..., { mode: 'date' }) | UTC-date Date |
TIME | time() | HH:mm:ss[.fraction] string |
JSON | jsonb() | JSON value |
BYTEA | bytes() | Uint8Array |
EMBEDDING(n) | embedding(name, n) | number[] |
UUID | uuid() | UUID string |
NUMERIC(p,s) / DECIMAL(p,s) | numeric() | exact decimal string |
FILE | file() | `FileRef |
Generator distinctions to review
The server distinguishes JSON from JSONB, and TIMESTAMP from TIMESTAMPTZ. The current ORM generator does not preserve all of those distinctions: its Json descriptor emits jsonb(), and the type-name parser has no TIMESTAMPTZ case, so that spelling falls back to Text. Do not use the generated file as proof of a lossless PostgreSQL schema round trip.
For a hand-authored schema, Drizzle exposes json() versus jsonb() and timestamp(name, { withTimezone: true, mode: 'date' }). Check the server declaration and generated output before using those columns. DATETIME may still appear in legacy descriptors, but new SQL should use the appropriate PostgreSQL timestamp declaration. The bytes() helper represents SQL BYTEA; it does not remove the server’s current PGWire binary-result limitation.
Upsert and returning
Drizzle returning() and onConflictDoUpdate() compile to KalamDB SQL and run through the same driver:
FILE uploads through Drizzle
You do not wrap every query. Normal select, insert, update, and delete calls go through db directly. Only FILE bytes need multipart SQL, and the driver handles that when upload values appear in .values() or .set():
You can also pass a File or Blob directly to a generated file() column; the driver uses the filename (or upload for plain blobs) as the multipart field name.
kalamDriver() compiles the Drizzle builder, normalizes quoted identifiers and DEFAULT columns for KalamDB, replaces upload params with FILE("name"), and calls client.queryWithFiles() when bytes are present.
For raw SQL strings (no Drizzle builder), use queryWithFiles() from @kalamdb/orm or @kalamdb/client. Use compileQuery() when you need normalized SQL and params without executing.
See FILE Columns & Uploads for download URLs, FileRef, and progress callbacks.
Execute as a user
Agents and service workers can compile a Drizzle builder and run it through KalamDB’s EXECUTE AS USER path:
Only pass a user id that your service account is authorized to impersonate.
Live table helpers
Use ORM liveTable() for a typed live list outside React. In React, prefer useLiveQuery, which handles the component lifecycle. A normal db.select() runs once; it does not open a subscription.
Create the inbox table and copy or generate the typed messages schema as schema.ts. Then add:
Call await watchRoom(client, room, render, reportError) from your view/service startup and keep the returned stop function. Catch startup rejection at that boundary. Show loading before startup, replace the list in render, and show an empty state only after receiving an empty list. Keep an error visible if reportError runs; cached rows may be stale.
When the room changes, await stop() before opening the next room. Await it again on view disposal as appropriate; only the application owner disconnects the shared client. To verify the example, run the inbox write sequence with the same authenticated user.
where: eq(messages.room, room) compiles values through Drizzle; do not build predicates by interpolating user input into SQL. The ORM maps database column names to schema property names and decodes timestamp/date/time fields. Rows are inferred from messages.$inferSelect, so render row.body directly instead of calling a cell accessor.
orderBy and limit are local presentation controls. Unlike the direct client’s materializer limit, the ORM keeps the matching set behind the visible slice. Use a selective where for large tables; limit: 100 does not bound subscription memory. The underlying live SQL remains SELECT ... FROM ... WHERE ....
Use liveEvents() for protocol events. Use the recovery guidance before adding durable checkpoints or manual retry loops.
UI and example fit
Use this package in UI code or service code that wants typed tables. Worker code should import @kalamdb/consumer only when it needs topic consumption.
Copy This Project Shape
Live query projection and generated row types
liveTable(client, table, callback, { orderBy, limit }) and
compileLiveTableDescriptor(table, options) use the same projection behavior:
Drizzle columns are mapped to their public property names, then ordered, then
limited. Date columns sort chronologically. The transport retains the full
matching set so deleting a visible row can reveal the next row. Narrow the SQL
filter to bound memory; a presentation limit is not a retention bound.
A disposed LiveQueryController is terminal. Create another controller to open
a new view. Callbacks from obsolete subscriptions cannot update its state or
forward stale errors/checkpoints. Custom-key live subscriptions accumulate initial
batches rather than replacing earlier batches.
The SQL-first generator preserves declared primary keys and emits array column
builders. Its built-in File shape aliases the SDK’s PgCatalogFile, and generated
FILE columns narrow their read type to that reference shape. For uploads through
these narrowed generated columns, use a Drizzle SQL FILE("placeholder") expression
with queryWithFiles; hand-authored file() builders retain their convenience
upload-input union. This keeps generated read models free of Blob/upload markers.
Raw live SQL normalization preserves literal whitespace. Its v1 clause recognition
still accepts a restricted SELECT grammar; for expressions outside that subset,
provide explicit subscription SQL and projection via createLiveQueryDescriptor
or use a Drizzle descriptor. This is not a general SQL parser.