Octomind
Same model as opencode, same endpoint, same prices — octomind solved 24/25 to its 19/25, at half the cost. Open-source agent runtime: one binary, any model, runs unattended in CI. Apache 2.0.
翻譯準備中
Postgres-native TypeScript ORM. Single-query nested relations via json_agg. Streaming cursors, typed errors, edge runtime support. One runtime dependency.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗A Postgres ORM written from scratch. One dependency.
Turbine compiles typed queries straight to SQL. There is no query engine, no WASM, and nothing between your code and Postgres except pg: dependencies is literally one line. Nested relations resolve in one statement. The client is small enough for a Worker or a Lambda cold start, sits next to hand-written pg in the benchmarks, ships an 11-tool read-only MCP server so coding agents can work your schema safely, and includes the operational tooling (index advice, migration guards, PII projection) that usually lives in a paid cloud tier. MIT, and the engine seam is documented if you want to fork it.
npm install turbine-orm
Turbine is pre-1.0 (0.x). Which surfaces hold steady across minors, which are Experimental, and what has to be true before 1.0: STABILITY.md.
Docs: turbineorm.dev · Quick Start · Why Turbine · API Reference · Relations · AI Agents · Benchmarks · Errors
Contents: Why Turbine · Benchmarks · Quick Start · Queries · Built for agents · Safety tooling · Postgres features · Serverless and edge · Database engines · From scratch, and forkable · Comparison · Limitations · Requirements · Contributing
Six reasons, each with the mechanism that makes it true:
dependencies is { "pg": "^8.15.0" }. No engine binary, no WASM compiler, no adapter packages in lockstep. The optional engines (SQLite, MySQL, SQL Server, PowDB, all tiered Experimental in STABILITY.md) are peer dependencies or Node builtins you install only if you use them.with clause compiles to correlated json_agg subqueries, so users with posts with comments is one round trip, typed end to end: users[0].posts[0].comments[0].author.name autocompletes with no annotation.npx turbine skill installs a query-writing skill whose every claim is executed against a live database before release; npx turbine mcp exposes eleven read-only MCP tools, including a relation graph and a join-path finder that returns the with clause to write. Every tool runs inside BEGIN READ ONLY, and PII-tagged columns are redacted before rows reach a model.update/delete with an empty where throws. Columns tagged pii: true are excluded from the emitted SQL's projections. turbine doctor reports missing FK indexes offline, no account, no telemetry.Measured 2026-10-15 against turbine-orm 0.92.0, versus Drizzle 0.45.3 (relational queries) and Prisma 7.10.0 (@prisma/adapter-pg, relationJoins on), with a hand-written pg control arm. Run by the repository's benchmarks.yml workflow on a dedicated machine: AMD Ryzen 9 7950X host, 4-vCPU 16 GB Firecracker VM, client and PostgreSQL 17 (Docker) on the same VM over a Unix socket. PostgreSQL 17.11, Node v24.18.0, pg 8.23.1 under every arm. Same schema, same data (1K users, 10K posts, 50K comments), same pool config. Each figure is the median over 200 rounds with arm order rotated per round, taken as the median of 3 full runs.
| Scenario | Turbine 0.92.0 | Drizzle 0.45.3 | Prisma 7.10.0 | raw pg |
|---|---|---|---|---|
| findMany, 100 users | 0.208 ms | 0.310 ms | 0.352 ms | 0.201 ms |
| findMany, 50 users + posts | 1.075 ms | 1.485 ms | 2.955 ms | 0.854 ms |
| findMany, 10 users + posts + comments | 0.620 ms | 0.884 ms | 2.270 ms | n/a |
| findUnique by primary key | 0.090 ms | 0.149 ms | 0.179 ms | 0.075 ms |
| findUnique, user + posts + comments | 0.246 ms | 0.460 ms | 0.599 ms | n/a |
| count, all users | 0.079 ms | 0.114 ms | 0.169 ms | 0.078 ms |
| stream 50,000 comments, per row (tie) | 55.02 ms | 55.13 ms | 82.84 ms | 57.03 ms |
| atomic increment | 0.164 ms | 0.182 ms | 0.259 ms | 0.135 ms |
| pipeline, 5-query batch | 0.343 ms | 0.701 ms | 0.784 ms | 0.360 ms |
| hot findUnique, rotating ids | 0.078 ms | 0.145 ms | 0.158 ms | 0.070 ms |
What that run says:
pg, Turbine ran at 1.09x, Drizzle at 1.59x and Prisma at 2.13x (geometric mean over the 8 scenarios with a control). Where a query returns nested rows, the control decodes them into objects with Date timestamps as the ORMs do. The nested read with posts is 1.26x the control, down from 1.86x in 0.91.0, because relation rows now decode in one pass.OFFSET pagination unless forceCustomPlan is set, and per-row streaming against the Drizzle 1.0 release candidate (findManyStreamBatches ties it).Full method, the extra scenarios, streaming, latency and the losses: benchmarks/RESULTS-0.92.0.md, or the charts at turbineorm.dev/benchmarks.
Reproduce it on a fresh machine with gh workflow run benchmarks.yml, or locally with benchmarks/run-round.sh (see benchmarks/README.md).
npm install turbine-orm
npm install --save-dev tsx # the CLI loads .ts config/schema files via tsx
npm pkg set type=module # the snippet below uses top-level await: ESM, or save it as a .mts file
npx turbine init --url postgres://user:pass@localhost:5432/mydb
npx turbine generate # introspect the DB, emit a typed client
import { turbine } from './generated/turbine/index.js';
const db = turbine({ connectionString: process.env.DATABASE_URL });
const users = await db.users.findMany({
where: { role: 'admin' },
orderBy: { createdAt: 'desc' },
limit: 10,
});
await db.disconnect();
generate writes three files to ./generated/turbine/: entity types, runtime schema metadata, and a typed client with a turbine() factory. The factory is generated (it is typed against your schema), so import it from your output directory, not from 'turbine-orm'. ESM and CommonJS both work.
The /index.js on that import is required under moduleResolution: NodeNext (a relative import needs an explicit extension, and there is no directory-index resolution). Under a bundler (moduleResolution: bundler, the Next.js and Vite default) './generated/turbine' works too. turbine generate prints the spelling that matches your tsconfig.json.
Full walkthrough, including the code-first defineSchema path for an empty database: turbineorm.dev/quickstart.
The API is Prisma-shaped: findMany, findUnique, findFirst, create, update, delete, upsert, plus count, aggregate, groupBy, and streaming. A tour of the parts worth knowing:
const users = await db.users.findMany({
where: { orgId: 1 },
with: {
posts: {
with: { comments: true },
orderBy: { createdAt: 'desc' },
limit: 5,
},
},
});
// users[0].posts[0].comments is typed, and this was one SQL statement
Per-relation where, orderBy, limit, select, and omit work at every depth. Many-to-many junction tables are auto-detected during generate, and a self-referencing FK (users.manager_id referencing users.id) gives you both directions, named user (to-one) and users (to-many) by default and renameable in turbine.config.ts. Relation filters (some / every / none) filter parents by their children.
Four load strategies produce identical rows: join (one statement), batched (one flat follow-up per relation), flatten (LEFT JOIN for eligible to-one relations), and auto (the default: the join plan, falling back to batched per relation when the correlation column has no covering index). A differential fuzz suite holds join, batched and auto to byte-identical output on every release and nightly; flatten is held to the same parity by its own deterministic suite (src/test/flatten-parity.integration.test.ts). Details: turbineorm.dev/relations.
await db.users.create({ data: { email: '[email protected]', name: 'Alice', orgId: 1 } });
await db.users.createMany({ data: [/* ... */] }); // one INSERT via UNNEST, not N inserts
await db.posts.update({
where: { id: 1 },
data: { viewCount: { increment: 1 } }, // col = col + $n, no read-modify-write race
});
Atomic operators: increment, decrement, multiply, divide, set. Nested writes (create, connect, connectOrCreate, disconnect, set, delete, update, upsert inside data) run in one transaction. update / delete with an empty where throws ValidationError rather than touching every row; see privilege options for the explicit opt-out.
Equality, not, in / notIn, gt / gte / lt / lte, contains / startsWith / endsWith (with mode: 'insensitive' for ILIKE), Postgres array operators (has / hasEvery / hasSome), JSON path filters, full-text search, pgvector distance, and AND / OR / NOT at any depth. LIKE wildcards in user input are escaped; every value is a bound parameter. The full table with examples: turbineorm.dev/queries.
await db.$transaction(async (tx) => {
const user = await tx.users.create({ data: { email: '[email protected]', name: 'New', orgId: 1 } });
await tx.posts.create({ data: { userId: user.id, orgId: 1, title: 'First' } });
});
Nested $transaction calls become SAVEPOINTs. Isolation levels, timeouts, and sessionContext (transaction-local GUCs for Postgres RLS) are options. A transaction PostgreSQL rolled back (a statement failed and the callback caught it) rejects instead of reporting a commit, and a tx used after its callback settles throws rather than running outside the transaction. tx deliberately exposes a smaller surface than db; reaching for a client-only member is a compile error, not a production surprise. Reference: turbineorm.dev/transactions.
const [user, postCount, recent] = await db.pipeline(
db.users.buildFindUnique({ where: { id: 1 } }),
db.posts.buildCount({ where: { orgId: 1 } }),
db.posts.buildFindMany({ where: { userId: 1 }, limit: 5 }),
);
// 3 queries, 1 TCP flush
db.pipeline(...) uses the Postgres extended-query protocol (Parse/Bind/Execute/Sync) to send N queries in one flush. That is wire pipelining, not a batch transaction. Every query method has a build* twin returning { sql, params, transform }, and the write builders batch too: db.$transaction([...]) takes an array of built queries and runs them atomically.
const users = await db.sql<{ id: number; name: string }>`
SELECT id, name FROM users WHERE org_id = ${orgId}
`; // every ${value} becomes $N, never concatenated
const one = await db.sql<{ id: number }>`SELECT id FROM users WHERE id = ${42}`.one();
db.sql<T> is the typed escape hatch (thenable, with .one() and .scalar()); db.raw is the untyped one. Neither has a code path that interpolates a value into SQL text.
for await (const user of db.users.findManyStream({
where: { orgId: 1 },
orderBy: { id: 'asc' },
with: { posts: true },
})) {
process.stdout.write(`${user.email}\n`);
}
Backed by DECLARE CURSOR on a dedicated connection: constant memory at any row count, safe to break early, nested with per batch.
const db = turbine({
connectionString: process.env.DATABASE_URL,
globalFilters: {
posts: { deletedAt: null }, // soft delete
orders: () => ({ tenantId: currentTenant() }), // per-request tenancy
},
});
A global filter is a WhereClause that is AND-merged into every query on a table: reads, relation subqueries targeting it, and the predicates of update / delete / upsert. Values are parameterized. Keys may be table names or client accessors and are checked when the client is built, and a function filter that returns undefined throws instead of running unfiltered. Details: turbineorm.dev/global-filters.
Three options remove a safety boundary: skipGlobalFilters, includePii, and allowFullTableScan. Each accepts exactly one value, a symbol exported from the package:
import { UNSAFE } from 'turbine-orm';
await db.sessions.deleteMany({ where: {}, allowFullTableScan: UNSAFE });
true throws. The reason is mass assignment: these options sit next to where on the same object, and a handler that spreads req.body into query args must not let a JSON payload disable tenancy or unlock PII. JSON.parse cannot produce a symbol, so there is no untrusted-data path to the privilege. Full rationale: turbineorm.dev/global-filters.
import { NotFoundError, UniqueConstraintError } from 'turbine-orm';
try {
await db.users.findUniqueOrThrow({ where: { id: 999 } });
} catch (err) {
if (err instanceof NotFoundError) { /* err.code === 'TURBINE_E001' */ }
}
Every error Turbine raises extends TurbineError with a stable code (TURBINE_E001 through E020) and a docsUrl; a driver error whose SQLSTATE Turbine does not map stays the driver's own error, carrying that SQLSTATE on .code. Error messages carry keys, never values: a NotFoundError says where: { id, email } without printing the email, so errors are safe to forward to a tracker without a scrubbing rule. Retryable failures (DeadlockError, SerializationFailureError and its subclass LockNotAvailableError) expose isRetryable: true as a typed const. err.code is the check that survives a mixed ESM/CJS module graph, where two copies of the package hold two copies of every class; since 0.78 instanceof works across copies too, through a cross-copy brand on every error. Full table: turbineorm.dev/errors.
An agent pointed at a database usually gets a connection string and guesses. Turbine gives it a skill and typed tools instead:
npx turbine skill # install the query-writing skill into the project
npx turbine mcp # read-only MCP server over JSON-RPC stdio, ships in the package
The skill covers what an agent gets wrong on a schema it has not seen: with versus Prisma's include, how relation names are derived, what select may name, relation filters, the having shape, JSON paths, and which error each mistake produces. Every factual claim in it is executed against a live database before release, so it cannot drift from the ORM the way documentation usually does. --agents prints a short block for AGENTS.md instead.
Eleven tools, every one inside BEGIN READ ONLY with a statement timeout, so an agent cannot mutate anything through this server:
| Tool | What it answers |
|---|---|
schema_overview | Tables, columns, relations, indexes, estimated row counts. |
table_detail | One table, in full. |
relation_graph | The whole relation graph, or one table's subtree, with cardinality, keys, and junction tables. |
find_join_path | "How do I get from comments to orgs": the relation chain and the with clause to write. |
table_stats | Planner row estimate, on-disk size, indexes. Reports analyzed: false instead of guessing 0. |
explain_query | EXPLAIN for a schema-validated findMany plan. No free-form SQL input exists. |
compile_query | The exact SQL a read query would send, without running it: bound params, statement count, relation strategy, and whether it is bounded by a LIMIT. |
explain_error | A Turbine error code, mapped to cause, fix, and docs link. |
sample_rows | Up to 50 rows; PII-tagged columns are never fetched, and the reply lists what was hidden. |
migrate_status | Applied vs pending migrations, without applying anything. |
doctor_report | Missing relation indexes, from the same advisor turbine doctor uses. |
The rest of the agent story is structural: query args are fully typed, so a wrong query is a compile error the agent can read; errors carry stable codes it can branch on; an unrecognized query option warns instead of being silently dropped; and llms.txt / llms-full.txt give it the docs in fetchable form. Setup for Claude Code and Cursor, plus a drop-in instructions snippet: turbineorm.dev/ai-agents.
turbine doctor: index advice, offline. Turbine loads relations as correlated subqueries, so an unindexed FK becomes a scan per parent row. doctor derives every column set the relation queries probe, reports the ones with no covering index with a cost tier, and --fix writes the migration. It also detects cached-plan divergence: columns whose value distribution makes a named prepared statement's generic plan unsafe, verified with a plan-only EXPLAIN. No cloud service, no account.
Destructive migrations need consent. migrate up, migrate down, and push scan for DROP TABLE, DROP COLUMN, TRUNCATE, unqualified DELETE / UPDATE, and ALTER COLUMN ... TYPE, print an itemized report, and refuse. Interactively you type destroy my data, then yes; in CI you pass --allow-destructive. A refused batch applies nothing. Migrations are SQL files, checksummed with SHA-256, applied under pg_try_advisory_lock().
PII stays in the database. Tag a column pii: true in the schema and it is excluded from every default projection at the SQL level: RETURNING "id", "name" instead of RETURNING *. It is also refused as a groupBy key and a _min / _max target. Reading it back takes includePii: UNSAFE, per query, and naming it in select without that throws. A schema with no tagged column emits byte-identical SQL.
Studio is read-only by default. npx turbine studio binds loopback, authenticates with a per-process token, runs every read inside BEGIN READ ONLY, and has no raw-SQL surface: queries are composed in a builder validated identifier by identifier. Without --write, the write endpoints do not exist in the router. PII cells are redacted server-side. Try it with no database: npx turbine-orm@latest studio --demo boots a seeded in-memory sample. Docs: turbineorm.dev/studio.
Observability without an agent. db.$on('query') taps every query with params redacted by default; db.$observe() flushes p50/p95/p99 aggregates per minute to a metrics table, and npx turbine observe is the dashboard. Docs: turbineorm.dev/observability.
Going deep on one database means the parts other ORMs push to raw SQL are typed surface here:
orderBy: { embedding: { distance: { to, metric: 'cosine' } } }, values bound as parameters. Docsdb.$listen(channel, handler) and db.$notify(channel, payload). Your database is the message bus. Docs$transaction(fn, { sessionContext }) sets transaction-local GUCs so Row-Level Security policies filter for you. Docswhere: { body: { search: 'postgres & orm' } } compiles to to_tsvector @@ to_tsquery, parameterized. Docs$primary() escape hatch (docs), views and generated columns (docs), optimistic locking (docs), and explain() on every table accessor (docs)The core is driver-agnostic: hand any pg-compatible pool to turbineHttp() and Turbine runs on Vercel Edge, Cloudflare Workers, Deno Deploy, or anywhere else without TCP. The main entry's import graph is held under 149 kB brotli (edge entry under 117 kB) with pg external, enforced by size-limit in CI at those exact numbers; run npm run size for the current figure.
import { Pool } from '@neondatabase/serverless';
import { turbineHttp } from 'turbine-orm/serverless';
import { SCHEMA } from './generated/turbine/metadata.js';
const db = turbineHttp(new Pool({ connectionString: process.env.DATABASE_URL }), SCHEMA);
const users = await db.table('users').findMany({ with: { posts: true }, limit: 10 });
HTTP drivers cannot hold a cursor or a LISTEN connection, so findManyStream and $listen are unavailable there; everything else works. Walkthroughs for Neon, Vercel Postgres, Supabase, and Hyperdrive: turbineorm.dev/serverless.
Postgres is the default and primary target. The same typed API also runs on SQLite (Node's built-in node:sqlite, zero extra installs, Node ≥ 22.5), MySQL 8 (mysql2), SQL Server 2016+ (mssql), and PowDB (embedded or networked), each behind a subpath export with its driver as an optional peer. All four are Experimental in the stability policy: the query API they expose is the shared one, but each engine's capability boundaries and factory options may still change in a minor release. PowDB is in maintenance mode: bug and data-safety fixes, no new features.
npm install turbine-orm # SQLite needs nothing else
npm install turbine-orm mysql2 # MySQL
npm install turbine-orm mssql # SQL Server
npm install turbine-orm @zvndev/powdb-embedded # PowDB, in-process
import { turbineSqlite } from 'turbine-orm/sqlite';
import { SCHEMA } from './generated/turbine/metadata.js';
const db = turbineSqlite(':memory:', SCHEMA);
const users = await db.users.findMany({ with: { posts: true }, limit: 10 });
That snippet assumes a Postgres database somewhere, because SCHEMA comes from
turbine generate and generate reads a live Postgres catalog. If SQLite is
your only database, describe the schema in code instead. defineSchema is the
same declaration push and migrate consume, and two pure functions turn it
into the DDL and the runtime metadata, with no database involved:
import { defineSchema, schemaDefToMetadata, schemaToSQL } from 'turbine-orm';
import { sqliteDialect, turbineSqlite } from 'turbine-orm/sqlite';
const schema = defineSchema({
users: {
id: { type: 'serial', primaryKey: true },
email: { type: 'text', notNull: true, unique: true },
},
posts: {
id: { type: 'serial', primaryKey: true },
// "table.column": this is what makes `with: { posts: true }` work below.
userId: { type: 'integer', notNull: true, references: 'users.id' },
title: { type: 'text', notNull: true },
},
});
const db = turbineSqlite(':memory:', schemaDefToMetadata(schema));
for (const stmt of schemaToSQL(schema, { dialect: sqliteDialect })) {
await db.rawUnsafe(stmt);
}
const users = await db
.table('users')
.findMany({ with: { posts: true }, orderBy: { id: 'asc' }, limit: 10 });
schemaToSQL emits SQLite DDL (including the foreign key and its index) and
schemaDefToMetadata derives the posts / users relations from the same
references:, so nested with works without a code-generation step. The
accessors are db.table('users') rather than db.users: the typed property
accessors are what turbine generate emits, and this path skips it.
Single-statement nested with works on all four SQL engines (json_agg, json_group_array, JSON_ARRAYAGG, FOR JSON PATH). Postgres-only features (pgvector, LISTEN/NOTIFY, RLS sessionContext, true cursor streaming) throw a typed UnsupportedFeatureError (TURBINE_E017) elsewhere instead of degrading silently. The turbine generate / migrate CLI is Postgres-only. Full capability matrix and per-engine notes: turbineorm.dev/engines.
Migrating from Prisma? turbine migrate-from-prisma reads your schema.prisma and emits a typed mapping; turbine-orm/prisma-compat then wraps a TurbineClient in a PrismaClient-shaped surface, so prisma.user.findMany({ include }) keeps working while you port. Guide. Coming from Drizzle: the mapping.
Turbine is MIT and has no cloud tier, no paid gateway, and no telemetry. Everything named on this page is in the box.
It is also built to be extended rather than wrapped. All SQL generation routes through a documented Dialect contract (identifier quoting, placeholders, result strategy, capability flags, JSON aggregation hooks); the SQLite, MySQL, and SQL Server engines are implementations of that seam, not forks of the core. If you need an engine Turbine does not ship, the seam is where you start: turbineorm.dev/dialects.
| Turbine | Prisma | Drizzle | Kysely | MikroORM | |
|---|---|---|---|---|---|
| Engine / runtime | No engine binary (pg only) | Client + TS/WASM query compiler | No engine | No engine | Kysely as query runner; SQL built by MikroORM (v7) |
| Runtime deps | 1 (pg) | @prisma/client + required driver adapter | 0 | 0 | @mikro-orm/core (0 deps) + a driver package |
| Main bundle (brotli) | under 149 kB import graph (CI-enforced), pg external | ~1.6 MB client (TS/WASM compiler) | ~7 KB core | small | |
| Studio | Read-only by default | Full CRUD, cloud-hosted | Full CRUD; Gateway self-hosted, free | None | AdminJS integration guide (CRUD panel) |
| Error PII safety | Keys only by default | Values in messages | Raw pg errors | Raw pg errors | |
| Migrations | SQL-first, SHA-256 checksums | DSL-generated, shadow DB | SQL or Drizzle Kit | None | Generated by diffing entities against a snapshot or the database; TS/JS classes |
| Edge runtime | One import swap, under 117 kB brotli (CI-enforced) | Driver adapter + WASM compiler | Native | Native | Cloudflare Workers and Deno (v7) |
| Pipeline batching | Parse/Bind/Execute protocol | Sequential in txn | Sequential | Manual | flush() runs queued changes in one transaction |
| Typed errors | isRetryable discriminant | Error codes only | None | None | DriverException subclasses (UniqueConstraintViolationException, ...) |
Competitor columns last checked 2026-08-22, against Prisma 7 and Drizzle 0.45 (the current latest; 1.0 is at rc.4). The MikroORM column was checked 2026-10-13 against its 7.2 documentation; a blank cell is one that documentation does not settle. Cells that make a claim about someone else's pricing or licensing carry a link to that vendor's own page, because those are the cells that go stale silently: this table said Drizzle's Gateway was paid, which was true of the old Drizzle Studio subscription and is not true of Gateway, and nothing in the repo would have caught it. Features marked Preview may change; bundle sizes move release to release. The longer version of this argument, including what is not a reason to switch: turbineorm.dev/why-turbine.
Stated so you do not find out three weeks in:
sessionContext, full-text search, array filters, and distinct are Postgres-only and throw UnsupportedFeatureError elsewhere. The CLI (generate, migrate) is Postgres-only.findManyStream materializes the result first, then yields in batches: same rows, not constant memory.limit in the with clause, or stream parents and resolve children per batch.| Postgres | TypeScript | Notes |
|---|---|---|
int2, int4, float4, float8 | number | |
int8 / bigint | number | Values above Number.MAX_SAFE_INTEGER come back as string to avoid precision loss; bigint[] reads as string[] |
numeric, money | string | Arbitrary precision, kept exact |
text, varchar, uuid, citext | string | |
timestamptz, timestamp, date | Date | Zone-less columns read and write as UTC by default (utcTimestamps); the full temporal semantics, including temporalInfinity, are at turbineorm.dev/schema |
boolean | boolean | |
json, jsonb | unknown | |
bytea | Buffer | |
interval, point, circle | object | The driver's parsed form, for example { x, y } for a point |
| Array types | T[] |
findMany, 4 levels deep, typed through the chainnode:sqlite)@types/node in devDependencies when you typecheck with skipLibCheck: false: the turbine-orm/sqlite engine and the turbine-orm/cli declarations reference Node's own types (node:sqlite, NodeJS)Contributions are welcome. See CONTRIBUTING.md for setup and the PR checklist; participants agree to the Code of Conduct. The unit suite runs without a database:
npm install
npm run test:unit
Per-release detail lives in the CHANGELOG and at turbineorm.dev/changelog.
MIT
| Nested relations | 1 query, deep type inference | 1 query per relation by default; single-query relationJoins is Preview | 1 query, relations() re-declaration | Manual (jsonArrayFrom) | populate; default balanced: joins to-one, one query per to-many relation |
| Index advice | turbine doctor, offline, --fix | Optimize retired March 2026 (cloud Query Insights) | None | None |
| MCP server for agents | 11 read-only tools, PII-redacted | Official MCP server | drizzle-kit mcp | None |
| Vector search | Built-in distance / KNN | Preview / raw | Extension API | Manual |
| LISTEN/NOTIFY | $listen / $notify | None | None | None |