Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ Each connector lazily initializes its underlying client (via `lazyInstance()`) a

## Build & Dev

| Command | Purpose |
|---------|---------|
| `pnpm build` | Generate connector registry + bundled build via `obuild` (index, every connector, and every integration are separate bundle entries with shared chunks; `build.config.ts` also validates `package.json` exports stay in sync) |
| `pnpm dev` | Vitest watch mode |
| `pnpm test` | Lint + typecheck (`tsgo`) + vitest with coverage + bun tests |
| `pnpm vitest run <path>` | Run a specific test |
| `pnpm lint` / `pnpm fmt` | ESLint + Prettier |
| Command | Purpose |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pnpm build` | Generate connector registry + bundled build via `obuild` (index, every connector, and every integration are separate bundle entries with shared chunks; `build.config.ts` also validates `package.json` exports stay in sync) |
| `pnpm dev` | Vitest watch mode |
| `pnpm test` | Lint + typecheck (`tsgo`) + vitest with coverage + bun tests |
| `pnpm vitest run <path>` | Run a specific test |
| `pnpm lint` / `pnpm fmt` | ESLint + Prettier |

Package manager: **pnpm**. Build tool: **obuild**. Typecheck: **tsgo**.

Expand All @@ -51,11 +51,14 @@ Tests live in `test/connectors/`. A shared `testConnector()` helper (`test/conne

`test/connector-dependencies.test.ts` asserts the generated `connectorDependencies` map matches the `importLib()` calls in each connector source, so declared metadata cannot drift.

`test/connector-capabilities.test.ts` does the same for capabilities: it builds each connector and asserts the docs table in `scripts/_capabilities-data.ts` matches the connector's real `dialect` + `capabilityOverrides`.

## Key Patterns

- **Zero deps** — no runtime deps and no peer deps; backend drivers are imported lazily via dynamic `import()` (see `_internal/utils.ts` `importLib()`), with a `lib` option escape hatch on every connector that needs one
- **Modular exports** — `db0/connectors/*`, `db0/integrations/drizzle`, `db0/integrations/kysely`
- **All bare imports stay external** — only relative/internal code is bundled, so backend drivers are never inlined
- **Dialect-aware** — adjusts SQL behavior (e.g., `RETURNING` support) per `SQLDialect`
- **Capabilities** — `db.capabilities` is a frozen snapshot from `src/capabilities.ts`, derived from the connector's `dialect` and refined by its optional `capabilityOverrides`. Note that `transactions` tracks the driver's _session model_, not the engine: connectors that open a new session per query (D1, PlanetScale, libsql over HTTP) set it to `false`
- **`BoundableStatement`** base class — shared bind/execute logic in `_internal/statement.ts`
- **AsyncDisposable** — `await using db = createDatabase(...)` is supported
144 changes: 144 additions & 0 deletions docs/1.guide/2.capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
---
icon: ph:check-circle-duotone
---

# Database Capabilities

> The `db.capabilities` property exposes database feature support at runtime.

Use capabilities to write portable code that adapts to the underlying database.

## Usage

You can access capabilities directly on the database instance:

```ts
import { createDatabase } from "db0";
import sqlite from "db0/connectors/better-sqlite3";

const db = createDatabase(sqlite({}));

console.log(db.capabilities);
// { json: true, booleans: false, arrays: false, ... }

if (db.capabilities.booleans) {
// Use a native boolean column
} else {
// Fall back to an integer 0/1 column
}
```

Capabilities are derived from the connector's SQL dialect, with per-connector
overrides where a specific backend differs from its dialect (see the table below).

## Available Flags

| Flag | Description |
| -------------- | -------------------------------------------------------------------------------- |
| `json` | The database has JSON support (a native type and/or JSON functions). |
| `booleans` | The database has a boolean column type (MySQL maps it to `TINYINT(1)`). |
| `arrays` | The database supports native array column types. |
| `dates` | The database supports native date/timestamp types. |
| `uuids` | The database supports native UUID column types. |
| `transactions` | Explicit `BEGIN`/`COMMIT` through `db.sql` opens a transaction (see note below). |

## Capabilities by Connector

> [!TIP]
> See [db-compat.onmax.me](https://db-compat.onmax.me) for a comprehensive database feature comparison.

<!-- automd:file src="./_capabilities-table.md" -->

<!-- Auto-generated by scripts/gen-capabilities-docs.ts. Do not edit manually. -->

| Connector | JSON | Bool | Array | Date | UUID | Tx |
| :------------------------------- | :--: | :--: | :---: | :--: | :--: | :-: |
| better-sqlite3 | ✓ | — | — | — | — | ✓ |
| bun-sqlite | ✓ | — | — | — | — | ✓ |
| cloudflare-d1 | ✓ | — | — | — | — | — |
| cloudflare-hyperdrive-mysql | ✓ | ✓ | — | ✓ | — | ✓ |
| cloudflare-hyperdrive-postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| libsql-core | ✓ | — | — | — | — | ✓ |
| libsql-http | ✓ | — | — | — | — | — |
| libsql-node | ✓ | — | — | — | — | ✓ |
| libsql-web | ✓ | — | — | — | — | — |
| mysql2 | ✓ | ✓ | — | ✓ | — | ✓ |
| neon | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| node-sqlite | ✓ | — | — | — | — | ✓ |
| pglite | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| planetscale | ✓ | ✓ | — | ✓ | — | — |
| postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| sqlite3 | ✓ | — | — | — | — | ✓ |

<!-- /automd -->

> [!NOTE]
> `transactions` describes whether `BEGIN`/`COMMIT` sent through `db.sql` actually
> open a transaction. It is a property of the driver's session model rather than of the SQL
> dialect, so several connectors report `false` even though their database engine supports
> transactions:
>
> - [Cloudflare D1](/connectors/cloudflare) rejects `BEGIN`/`COMMIT` outright; only implicit
> transactions via `D1Database.batch()` are available.
> - [PlanetScale](/connectors/planetscale) opens a new HTTP session per query, so consecutive
> statements never share a transaction. Use `Client.transaction()` on the underlying client.
> - [libsql-http and libsql-web](/connectors/libsql) open and close a Hrana stream within a
> single request per query. Use `client.transaction()` on the underlying client.
>
> `libsql-node` and `libsql-core` report `true` because they are commonly used against a local
> file, where a single connection is held open. Pointed at a remote `libsql:`/`http:` URL they
> behave like `libsql-http`, and this flag cannot detect that statically — reach for
> `client.transaction()` if you are unsure.

## Use Cases

### Conditional Feature Usage

You can adapt your queries to what the database supports:

```ts
import type { Database } from "db0";

function storeList(db: Database, items: string[]) {
const value = db.capabilities.arrays
? `{${items.join(",")}}` // PostgreSQL array literal
: JSON.stringify(items);
return db.sql`INSERT INTO data (items) VALUES (${value})`;
}
```

> [!NOTE]
> Values interpolated into `db.sql` must be primitives (`string`,
> `number`, `boolean`, `null` or `undefined`), so arrays and objects have to be serialized
> to a value the target database understands.

### Runtime Validation

You can validate that the database meets your application's requirements:

```ts
import type { Database } from "db0";

function initDatabase(db: Database) {
if (!db.capabilities.transactions) {
throw new Error("This application requires transaction support");
}
}
```

### Feature Detection in Libraries

You can build database-agnostic libraries that adapt automatically:

```ts
import type { Database } from "db0";

export function createRepository(db: Database) {
return {
saveFlag(id: string, enabled: boolean) {
const value = db.capabilities.booleans ? enabled : Number(enabled);
return db.sql`UPDATE items SET enabled = ${value} WHERE id = ${id}`;
},
};
}
```
20 changes: 20 additions & 0 deletions docs/1.guide/_capabilities-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- Auto-generated by scripts/gen-capabilities-docs.ts. Do not edit manually. -->

| Connector | JSON | Bool | Array | Date | UUID | Tx |
| :------------------------------- | :--: | :--: | :---: | :--: | :--: | :-: |
| better-sqlite3 | ✓ | — | — | — | — | ✓ |
| bun-sqlite | ✓ | — | — | — | — | ✓ |
| cloudflare-d1 | ✓ | — | — | — | — | — |
| cloudflare-hyperdrive-mysql | ✓ | ✓ | — | ✓ | — | ✓ |
| cloudflare-hyperdrive-postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| libsql-core | ✓ | — | — | — | — | ✓ |
| libsql-http | ✓ | — | — | — | — | — |
| libsql-node | ✓ | — | — | — | — | ✓ |
| libsql-web | ✓ | — | — | — | — | — |
| mysql2 | ✓ | ✓ | — | ✓ | — | ✓ |
| neon | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| node-sqlite | ✓ | — | — | — | — | ✓ |
| pglite | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| planetscale | ✓ | ✓ | — | ✓ | — | — |
| postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| sqlite3 | ✓ | — | — | — | — | ✓ |
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@
"dist"
],
"scripts": {
"build": "pnpm gen-connectors && obuild",
"build": "pnpm gen-connectors && pnpm gen-capabilities && obuild",
"gen-connectors": "jiti scripts/gen-connectors.ts",
"gen-capabilities": "jiti scripts/gen-capabilities-docs.ts && automd --dir docs --input 1.guide/2.capabilities.md",
"db0": "pnpm jiti src/cli",
"dev": "vitest",
"lint": "prettier -c src test",
"fmt": "prettier -w src test",
"lint": "prettier -c src test scripts",
"fmt": "prettier -w src test scripts",
"prepack": "pnpm build",
"release": "pnpm test && changelogen --release --push && pnpm publish",
"test": "pnpm lint && pnpm test:types && vitest run --coverage && pnpm test:bun",
Expand Down
45 changes: 45 additions & 0 deletions scripts/_capabilities-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { dialectCapabilities, getCapabilities } from "../src/capabilities.ts";
import type { ConnectorName } from "../src/_connectors.ts";
import type { DatabaseCapabilities } from "../src/types.ts";

/**
* Capabilities of every connector, mirroring its `dialect` and `capabilityOverrides`.
*
* Typed as an exhaustive `Record<ConnectorName, ...>` so that `pnpm test:types`
* fails when a connector is added without a row here. The *values* are checked
* against the real connectors by `test/connector-capabilities.test.ts`, which
* imports each one and compares — keep both in mind when editing.
*/
export const connectorCapabilities: Record<
ConnectorName,
DatabaseCapabilities
> = {
"better-sqlite3": dialectCapabilities.sqlite,
sqlite3: dialectCapabilities.sqlite,
"bun-sqlite": dialectCapabilities.sqlite,
bun: dialectCapabilities.sqlite,
"node-sqlite": dialectCapabilities.sqlite,
sqlite: dialectCapabilities.sqlite,
"libsql-core": dialectCapabilities.libsql,
"libsql-node": dialectCapabilities.libsql,
libsql: dialectCapabilities.libsql,
"libsql-http": getCapabilities("libsql", { transactions: false }),
"libsql-web": getCapabilities("libsql", { transactions: false }),
"cloudflare-d1": getCapabilities("sqlite", { transactions: false }),
postgresql: dialectCapabilities.postgresql,
pglite: dialectCapabilities.postgresql,
neon: dialectCapabilities.postgresql,
"cloudflare-hyperdrive-postgresql": dialectCapabilities.postgresql,
mysql2: dialectCapabilities.mysql,
planetscale: getCapabilities("mysql", { transactions: false }),
"cloudflare-hyperdrive-mysql": dialectCapabilities.mysql,
};

export const capabilityLabels: Record<keyof DatabaseCapabilities, string> = {
json: "JSON",
booleans: "Bool",
arrays: "Array",
dates: "Date",
uuids: "UUID",
transactions: "Tx",
};
87 changes: 87 additions & 0 deletions scripts/gen-capabilities-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { connectors, type ConnectorName } from "../src/_connectors.ts";
import {
capabilityLabels,
connectorCapabilities,
} from "./_capabilities-data.ts";
import type { DatabaseCapabilities } from "../src/types.ts";

const outputFile = fileURLToPath(
new URL("../docs/1.guide/_capabilities-table.md", import.meta.url),
);

const check = "✓";
const cross = "—";

/** Canonical connector names, in registry order, with aliases removed. */
function canonicalConnectorNames(): ConnectorName[] {
const seen = new Set<string>();
return (Object.keys(connectors) as ConnectorName[]).filter((name) => {
const subpath = connectors[name];
if (seen.has(subpath)) {
return false;
}
seen.add(subpath);
return true;
});
}

/** Pads `value` to `width`, matching how Prettier lays out markdown tables. */
function pad(value: string, width: number, align: "left" | "center"): string {
const total = width - [...value].length;
if (align === "left") {
return value + " ".repeat(total);
}
const left = Math.floor(total / 2);
return " ".repeat(left) + value + " ".repeat(total - left);
}

function generateTable(names: ConnectorName[]): string {
const keys = Object.keys(capabilityLabels) as (keyof DatabaseCapabilities)[];
const headers = ["Connector", ...Object.values(capabilityLabels)];

// The connector column reads as a label, the flag columns as marks.
const aligns = headers.map((_, index) =>
index === 0 ? ("left" as const) : ("center" as const),
);

const body = names.map((name) => [
name,
...keys.map((key) => (connectorCapabilities[name][key] ? check : cross)),
]);

// `:-:` is the narrowest legal separator, so it sets the minimum column width.
const widths = headers.map((header, index) =>
Math.max(
3,
...[header, ...body.map((row) => row[index]!)].map(
(cell) => [...cell].length,
),
),
);

const separator = widths.map((width, index) =>
aligns[index] === "left"
? ":" + "-".repeat(width - 1)
: ":" + "-".repeat(width - 2) + ":",
);

const rows = [headers, separator, ...body].map((row, index) =>
index === 1
? `| ${row.join(" | ")} |`
: `| ${row
.map((cell, column) => pad(cell, widths[column]!, aligns[column]!))
.join(" | ")} |`,
);

return rows.join("\n");
}

const content = `<!-- Auto-generated by scripts/gen-capabilities-docs.ts. Do not edit manually. -->

${generateTable(canonicalConnectorNames())}
`;

await writeFile(outputFile, content, "utf8");
console.log("Generated capabilities table to", outputFile);
3 changes: 2 additions & 1 deletion scripts/gen-connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ for (const entry of connectorEntries) {

const safeName = camelCase(name).replace(/db/i, "DB").replace(/sql/i, "SQL");

const alternativeNames: string[] = aliases[name] || [];
const alternativeNames: readonly string[] =
aliases[name as keyof typeof aliases] || [];

const names = [...new Set([name, ...alternativeNames])];

Expand Down
Loading
Loading