Skip to content
Open
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
112 changes: 112 additions & 0 deletions docs/1.guide/2.capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
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);
// { supportsJSON: true, supportsBooleans: false, supportsArrays: false, ... }

if (db.capabilities.supportsArrays) {
// Use PostgreSQL array syntax
} else {
// Use JSON or comma-separated values
}
```

## Available Flags

| Flag | Description |
| ----------------------- | ------------------------------------------------------------- |
| `supportsJSON` | The database supports native JSON column types and functions. |
| `supportsBooleans` | The database supports native boolean types (not 0/1). |
| `supportsArrays` | The database supports native array column types. |
| `supportsDates` | The database supports native date/timestamp types. |
| `supportsUUIDs` | The database supports native UUID column types. |
| `supportsTransactions` | The database supports transactions. |
| `supportsBatch` | The database supports batch execution of statements. |

## 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 | Batch |
| :--------:|:---:|:---:|:----:|:---:|:---:|:-:|:----: |
| better-sqlite3 | ✓ | — | — | — | — | ✓ | ✓ |
| sqlite3 | ✓ | — | — | — | — | ✓ | ✓ |
| bun-sqlite | ✓ | — | — | — | — | ✓ | ✓ |
| node-sqlite | ✓ | — | — | — | — | ✓ | ✓ |
| libsql | ✓ | — | — | — | — | ✓ | ✓ |
| cloudflare-d1 | ✓ | — | — | — | — | ✓ | ✓ |
| postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| pglite | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| mysql2 | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
| planetscale | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |

<!-- /automd -->

> [!NOTE]
> Cloudflare Hyperdrive connectors inherit the capabilities of their underlying database (PostgreSQL or MySQL).

## Use Cases

### Conditional Feature Usage

You can adapt your queries based on database support:

```ts
function storeList(db: Database, items: string[]) {
if (db.capabilities.supportsArrays) {
return db.sql`INSERT INTO data (items) VALUES (${items})`;
} else {
return db.sql`INSERT INTO data (items) VALUES (${JSON.stringify(items)})`;
}
}
```

### Runtime Validation

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

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

### Feature Detection in Libraries

You can build database-agnostic libraries that adapt automatically:

```ts
export function createRepository(db: Database) {
return {
saveTags(id: string, tags: string[]) {
const value = db.capabilities.supportsArrays
? tags
: tags.join(",");
return db.sql`UPDATE items SET tags = ${value} WHERE id = ${id}`;
},
};
}
```
15 changes: 15 additions & 0 deletions docs/1.guide/_capabilities-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!-- Auto-generated by scripts/gen-capabilities-docs.ts from src/capabilities.ts. Do not edit manually. -->
| Connector | JSON | Bool | Array | Date | UUID | Tx | Batch |
| :--------:|:---:|:---:|:----:|:---:|:---:|:-:|:----: |
| better-sqlite3 | ✓ | — | — | — | — | ✓ | ✓ |
| sqlite3 | ✓ | — | — | — | — | ✓ | ✓ |
| bun-sqlite | ✓ | — | — | — | — | ✓ | ✓ |
| node-sqlite | ✓ | — | — | — | — | ✓ | ✓ |
| libsql | ✓ | — | — | — | — | ✓ | ✓ |
| cloudflare-d1 | ✓ | — | — | — | — | ✓ | ✓ |
| postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| pglite | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| cloudflare-hyperdrive-postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| mysql2 | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
| planetscale | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
| cloudflare-hyperdrive-mysql | ✓ | ✓ | — | ✓ | — | ✓ | ✓ |
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@
"./connectors/libsql/*": {
"types": "./dist/connectors/libsql/*.d.ts",
"default": "./dist/connectors/libsql/*.mjs"
},
"./capabilities": {
"types": "./dist/capabilities.d.ts",
"default": "./dist/capabilities.mjs"
}
},
"types": "./dist/index.d.mts",
"files": [
"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",
"db0": "pnpm jiti src/cli",
"dev": "vitest",
"lint": "eslint . && prettier -c src test",
Expand Down
57 changes: 57 additions & 0 deletions scripts/gen-capabilities-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { connectorCapabilities } from "../src/connectors/_internal/capabilities.ts";
import type { DatabaseCapabilities } from "../src/types.ts";

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

const db0Connectors = [
"better-sqlite3", "sqlite3", "bun-sqlite", "node-sqlite", "libsql", "cloudflare-d1",
"postgresql", "pglite", "cloudflare-hyperdrive-postgresql",
"mysql2", "planetscale", "cloudflare-hyperdrive-mysql",
];

const capabilityLabels: Record<keyof DatabaseCapabilities, string> = {
supportsJSON: "JSON",
supportsBooleans: "Bool",
supportsArrays: "Array",
supportsDates: "Date",
supportsUUIDs: "UUID",
supportsTransactions: "Tx",
supportsBatch: "Batch",
};

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

function generateTable(connectorCapabilities: Record<string, DatabaseCapabilities>): string {
const headers = ["Connector", ...Object.values(capabilityLabels)];
const separator = headers.map((h) => ":".padEnd(h.length, "-") + ":");

const rows = Object.entries(connectorCapabilities).map(([name, caps]) => {
const cells = Object.keys(capabilityLabels).map((key) =>
caps[key as keyof DatabaseCapabilities] ? check : cross,
);
return [name, ...cells];
});

const lines = [headers.join(" | "), separator.join("|"), ...rows.map((row) => row.join(" | "))];
return lines.map((line) => `| ${line} |`).join("\n");
}

const orderedCapabilities: Record<string, DatabaseCapabilities> = Object.fromEntries(
db0Connectors.map((id) => {
const caps = connectorCapabilities[id];
if (!caps) {
throw new Error(`No capabilities mapping found for connector: ${id}`);
}
return [id, caps] as const;
}),
);

const content = `<!-- Auto-generated by scripts/gen-capabilities-docs.ts from src/capabilities.ts. Do not edit manually. -->
${generateTable(orderedCapabilities)}
`;

await writeFile(outputFile, content, "utf8");
console.log("Generated capabilities table to", outputFile);
49 changes: 49 additions & 0 deletions src/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { DatabaseCapabilities, SQLDialect } from "./types.ts";

const sqlite: DatabaseCapabilities = {
supportsJSON: true,
supportsBooleans: false,
supportsArrays: false,
supportsDates: false,
supportsUUIDs: false,
supportsTransactions: true,
supportsBatch: true,
};

const postgresql: DatabaseCapabilities = {
supportsJSON: true,
supportsBooleans: true,
supportsArrays: true,
supportsDates: true,
supportsUUIDs: true,
supportsTransactions: true,
supportsBatch: true,
};

const mysql: DatabaseCapabilities = {
supportsJSON: true,
supportsBooleans: true,
supportsArrays: false,
supportsDates: true,
supportsUUIDs: false,
supportsTransactions: true,
supportsBatch: true,
};

export const dialectCapabilities: Record<SQLDialect, DatabaseCapabilities> = {
sqlite,
libsql: sqlite,
postgresql,
mysql,
};

export function getCapabilities(
dialect: SQLDialect,
overrides?: Partial<DatabaseCapabilities>,
): DatabaseCapabilities {
return overrides
? { ...dialectCapabilities[dialect], ...overrides }
: dialectCapabilities[dialect];
}

export type { DatabaseCapabilities } from "./types.ts";
18 changes: 18 additions & 0 deletions src/connectors/_internal/capabilities.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, it looks like a dialect-mapped table. I suggest exposing it as a subpath like db0/capabilities with a map from dialects to capabilities users can use.

Later we might only expose capabilityOverrides from connectors that have exception (for example have one less or extra feature)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i amended pr with this comments

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { dialectCapabilities } from "../../capabilities.ts";
import type { DatabaseCapabilities } from "../../types.ts";

// Connector-to-capabilities mapping for documentation generation
export const connectorCapabilities: Record<string, DatabaseCapabilities> = {
"better-sqlite3": dialectCapabilities.sqlite,
sqlite3: dialectCapabilities.sqlite,
"bun-sqlite": dialectCapabilities.sqlite,
"node-sqlite": dialectCapabilities.sqlite,
libsql: dialectCapabilities.libsql,
"cloudflare-d1": dialectCapabilities.sqlite,
postgresql: dialectCapabilities.postgresql,
pglite: dialectCapabilities.postgresql,
"cloudflare-hyperdrive-postgresql": dialectCapabilities.postgresql,
mysql2: dialectCapabilities.mysql,
planetscale: dialectCapabilities.mysql,
"cloudflare-hyperdrive-mysql": dialectCapabilities.mysql,
};
5 changes: 5 additions & 0 deletions src/database.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getCapabilities } from "./capabilities.ts";
import { sqlTemplate } from "./template.ts";
import type { Connector, Database, SQLDialect } from "./types.ts";
import type { Primitive } from "./types.ts";
Expand Down Expand Up @@ -34,6 +35,10 @@ export function createDatabase<TConnector extends Connector = Connector>(
return connector.dialect;
},

get capabilities() {
return getCapabilities(connector.dialect, connector.capabilityOverrides);
},

get disposed() {
return _disposed;
},
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
export { createDatabase } from "./database.ts";
export { dialectCapabilities, getCapabilities } from "./capabilities.ts";

export { connectors } from "./_connectors.ts";

export type {
Connector,
Database,
DatabaseCapabilities,
ExecResult,
Primitive,
SQLDialect,
Expand Down
5 changes: 4 additions & 1 deletion src/integrations/drizzle/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export function mapResultRow<TResult>(
} else if (is(field, SQL)) {
decoder = "decoder" in field && (field.decoder as any);
} else {
decoder = "decoder" in field.sql && (field.sql.decoder as any);
decoder =
"sql" in field &&
"decoder" in field.sql &&
(field.sql.decoder as any);
}
let node = result;
for (const [pathChunkIndex, pathChunk] of path.entries()) {
Expand Down
20 changes: 20 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ export type Primitive = string | number | boolean | undefined | null;

export type SQLDialect = "mysql" | "postgresql" | "sqlite" | "libsql";

export interface DatabaseCapabilities {
readonly supportsJSON: boolean;
readonly supportsBooleans: boolean;
readonly supportsArrays: boolean;
readonly supportsDates: boolean;
readonly supportsUUIDs: boolean;
readonly supportsTransactions: boolean;
readonly supportsBatch: boolean;
}

export type Statement = {
/**
* Binds parameters to the statement.
Expand Down Expand Up @@ -81,6 +91,11 @@ export type Connector<TInstance = unknown> = {
*/
dialect: SQLDialect;

/**
* Override specific database capabilities for this connector.
*/
capabilityOverrides?: Partial<DatabaseCapabilities>;

/**
* The client instance used internally.
*/
Expand Down Expand Up @@ -123,6 +138,11 @@ export interface Database<
> extends AsyncDisposable {
readonly dialect: SQLDialect;

/**
* Database capabilities supported by this connector.
*/
readonly capabilities: DatabaseCapabilities;

/**
* Indicates whether the database instance has been disposed/closed.
* @returns {boolean} True if the database has been disposed, false otherwise.
Expand Down
18 changes: 18 additions & 0 deletions test/connectors/_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { beforeAll, expect, it } from "vitest";
import {
Connector,
Database,
DatabaseCapabilities,
createDatabase,
type SQLDialect,
} from "../../src";

export function testConnector<TConnector extends Connector = Connector>(opts: {
connector: TConnector;
dialect: SQLDialect;
capabilities?: Partial<DatabaseCapabilities>;
}) {
let db: Database<TConnector>;
beforeAll(() => {
Expand Down Expand Up @@ -37,6 +39,22 @@ export function testConnector<TConnector extends Connector = Connector>(opts: {
expect(db.dialect).toBe(opts.dialect);
});

it("capabilities match", () => {
expect(db.capabilities).toBeDefined();
expect(typeof db.capabilities.supportsJSON).toBe("boolean");
expect(typeof db.capabilities.supportsBooleans).toBe("boolean");
expect(typeof db.capabilities.supportsArrays).toBe("boolean");
expect(typeof db.capabilities.supportsDates).toBe("boolean");
expect(typeof db.capabilities.supportsUUIDs).toBe("boolean");
expect(typeof db.capabilities.supportsTransactions).toBe("boolean");
expect(typeof db.capabilities.supportsBatch).toBe("boolean");
if (opts.capabilities) {
for (const [key, value] of Object.entries(opts.capabilities)) {
expect(db.capabilities[key as keyof DatabaseCapabilities]).toBe(value);
}
}
});

it("drop and create table", async () => {
await db.sql`DROP TABLE IF EXISTS users`;
switch (opts.dialect) {
Expand Down
Loading