From a1161db9bf960e3056de907772be38b82f4fa13c Mon Sep 17 00:00:00 2001 From: onmax Date: Sun, 1 Feb 2026 17:12:48 +0100 Subject: [PATCH 1/2] feat: fetch capabilities from db-compat --- docs/1.guide/2.capabilities.md | 112 +++++++++++++++++++++++ docs/1.guide/_capabilities-table.md | 14 +++ package.json | 7 +- scripts/gen-capabilities-docs.ts | 88 ++++++++++++++++++ src/capabilities.ts | 49 ++++++++++ src/connectors/_internal/capabilities.ts | 18 ++++ src/database.ts | 5 + src/index.ts | 2 + src/integrations/drizzle/_utils.ts | 5 +- src/types.ts | 20 ++++ test/connectors/_tests.ts | 18 ++++ tsconfig.json | 5 +- 12 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 docs/1.guide/2.capabilities.md create mode 100644 docs/1.guide/_capabilities-table.md create mode 100644 scripts/gen-capabilities-docs.ts create mode 100644 src/capabilities.ts create mode 100644 src/connectors/_internal/capabilities.ts diff --git a/docs/1.guide/2.capabilities.md b/docs/1.guide/2.capabilities.md new file mode 100644 index 00000000..d6d96634 --- /dev/null +++ b/docs/1.guide/2.capabilities.md @@ -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. + + + + +| Connector | JSON | Bool | Array | Date | UUID | Tx | Batch | +| :--------:|:---:|:---:|:----:|:---:|:---:|:-:|:----: | +| better-sqlite3 | ✓ | — | — | — | — | ✓ | ✓ | +| sqlite3 | ✓ | — | — | — | — | ✓ | ✓ | +| bun-sqlite | ✓ | — | — | — | — | ✓ | ✓ | +| node-sqlite | ✓ | — | — | — | — | ✓ | ✓ | +| libsql | ✓ | — | — | — | — | ✓ | ✓ | +| cloudflare-d1 | ✓ | — | — | — | — | ✓ | ✓ | +| postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| pglite | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| mysql2 | ✓ | ✓ | — | ✓ | — | ✓ | ✓ | +| planetscale | ✓ | ✓ | — | ✓ | — | ✓ | ✓ | + + + +> [!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}`; + }, + }; +} +``` diff --git a/docs/1.guide/_capabilities-table.md b/docs/1.guide/_capabilities-table.md new file mode 100644 index 00000000..543e8fb3 --- /dev/null +++ b/docs/1.guide/_capabilities-table.md @@ -0,0 +1,14 @@ + +| Connector | JSON | Bool | Array | Date | UUID | Tx | Batch | +| :--------:|:---:|:---:|:----:|:---:|:---:|:-:|:----: | +| better-sqlite3 | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | +| sqlite3 | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | +| bun-sqlite | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | +| node-sqlite | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | +| libsql | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | +| cloudflare-d1 | ✓ | — | ✓ | ✓ | ✓ | — | ✓ | +| postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| pglite | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| cloudflare-hyperdrive-postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| mysql2 | ✓ | ✓ | — | ✓ | — | — | ✓ | +| cloudflare-hyperdrive-mysql | ✓ | ✓ | — | ✓ | — | — | ✓ | diff --git a/package.json b/package.json index 9c3cb54e..769bbeb3 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,10 @@ "./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", @@ -29,8 +33,9 @@ "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", diff --git a/scripts/gen-capabilities-docs.ts b/scripts/gen-capabilities-docs.ts new file mode 100644 index 00000000..cd12e047 --- /dev/null +++ b/scripts/gen-capabilities-docs.ts @@ -0,0 +1,88 @@ +import { writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import type { DatabaseCapabilities } from "../src/types.ts"; + +const DB_COMPAT_URL = "https://raw.githubusercontent.com/onmax/db-compat/main/packages/data/data.json"; + +const outputFile = fileURLToPath(new URL("../docs/1.guide/_capabilities-table.md", import.meta.url)); + +interface CompatTarget { dialect: string; driver: string; version: string } +interface CompatEntry { support: Record } +interface CompatData { + __meta: { targets: Record }; + sql: { + types: Record; + transactions: Record; + }; +} + +// Map db0 connector names to db-compat target IDs +const targetMapping: Record = { + "cloudflare-hyperdrive-postgresql": "hyperdrive-postgresql", + "cloudflare-hyperdrive-mysql": "hyperdrive-mysql", +}; + +const db0Connectors = [ + "better-sqlite3", "sqlite3", "bun-sqlite", "node-sqlite", "libsql", "cloudflare-d1", + "postgresql", "pglite", "cloudflare-hyperdrive-postgresql", + "mysql2", "cloudflare-hyperdrive-mysql", +]; + +function toDb0Capabilities(data: CompatData, targetId: string): DatabaseCapabilities { + const id = targetMapping[targetId] || targetId; + const target = data.__meta.targets[id]; + const types = data.sql.types; + const tx = data.sql.transactions; + + return { + supportsJSON: types.type_json.support[id].supported, + supportsBooleans: target.dialect !== "sqlite", + supportsArrays: types.type_array.support[id].supported, + supportsDates: types.type_date.support[id].supported, + supportsUUIDs: types.type_uuid.support[id].supported, + supportsTransactions: tx.BEGIN.support[id].supported, + supportsBatch: tx.batch_atomicity.support[id].supported, + }; +} + +const capabilityLabels: Record = { + supportsJSON: "JSON", + supportsBooleans: "Bool", + supportsArrays: "Array", + supportsDates: "Date", + supportsUUIDs: "UUID", + supportsTransactions: "Tx", + supportsBatch: "Batch", +}; + +const check = "✓"; +const cross = "—"; + +function generateTable(connectorCapabilities: Record): 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"); +} + +console.log("Fetching db-compat data..."); +const data: CompatData = await fetch(DB_COMPAT_URL).then((r) => r.json()); + +const connectorCapabilities: Record = Object.fromEntries( + db0Connectors.map((id) => [id, toDb0Capabilities(data, id)]), +); + +const content = ` +${generateTable(connectorCapabilities)} +`; + +await writeFile(outputFile, content, "utf8"); +console.log("Generated capabilities table to", outputFile); diff --git a/src/capabilities.ts b/src/capabilities.ts new file mode 100644 index 00000000..0a446dae --- /dev/null +++ b/src/capabilities.ts @@ -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 = { + sqlite, + libsql: sqlite, + postgresql, + mysql, +}; + +export function getCapabilities( + dialect: SQLDialect, + overrides?: Partial, +): DatabaseCapabilities { + return overrides + ? { ...dialectCapabilities[dialect], ...overrides } + : dialectCapabilities[dialect]; +} + +export type { DatabaseCapabilities } from "./types.ts"; diff --git a/src/connectors/_internal/capabilities.ts b/src/connectors/_internal/capabilities.ts new file mode 100644 index 00000000..92e0c1d3 --- /dev/null +++ b/src/connectors/_internal/capabilities.ts @@ -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 = { + "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, +}; diff --git a/src/database.ts b/src/database.ts index f94b69f9..b33abdab 100644 --- a/src/database.ts +++ b/src/database.ts @@ -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"; @@ -34,6 +35,10 @@ export function createDatabase( return connector.dialect; }, + get capabilities() { + return getCapabilities(connector.dialect, connector.capabilityOverrides); + }, + get disposed() { return _disposed; }, diff --git a/src/index.ts b/src/index.ts index 2ce54c19..7fd4bb1c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/integrations/drizzle/_utils.ts b/src/integrations/drizzle/_utils.ts index 33141d3b..762f01fd 100644 --- a/src/integrations/drizzle/_utils.ts +++ b/src/integrations/drizzle/_utils.ts @@ -28,7 +28,10 @@ export function mapResultRow( } 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()) { diff --git a/src/types.ts b/src/types.ts index 4897e0f0..3a2601e9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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. @@ -81,6 +91,11 @@ export type Connector = { */ dialect: SQLDialect; + /** + * Override specific database capabilities for this connector. + */ + capabilityOverrides?: Partial; + /** * The client instance used internally. */ @@ -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. diff --git a/test/connectors/_tests.ts b/test/connectors/_tests.ts index 7f42d784..f87e5a2c 100644 --- a/test/connectors/_tests.ts +++ b/test/connectors/_tests.ts @@ -2,6 +2,7 @@ import { beforeAll, expect, it } from "vitest"; import { Connector, Database, + DatabaseCapabilities, createDatabase, type SQLDialect, } from "../../src"; @@ -9,6 +10,7 @@ import { export function testConnector(opts: { connector: TConnector; dialect: SQLDialect; + capabilities?: Partial; }) { let db: Database; beforeAll(() => { @@ -37,6 +39,22 @@ export function testConnector(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) { diff --git a/tsconfig.json b/tsconfig.json index 880cb46b..61277639 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,9 +17,8 @@ "noImplicitOverride": true, "noEmit": true, "paths": { - "db0/connectors/*": [ - "./src/connectors/*" - ] + "db0": ["./src/index.ts"], + "db0/connectors/*": ["./src/connectors/*"] } }, "include": [ From bdbb5a2de22cb442c66e7a12da60c2e0eafaa627 Mon Sep 17 00:00:00 2001 From: onmax Date: Sat, 7 Feb 2026 12:56:16 +0100 Subject: [PATCH 2/2] docs: generate capabilities table locally --- docs/1.guide/_capabilities-table.md | 19 +++++----- scripts/gen-capabilities-docs.ts | 55 +++++++---------------------- 2 files changed, 22 insertions(+), 52 deletions(-) diff --git a/docs/1.guide/_capabilities-table.md b/docs/1.guide/_capabilities-table.md index 543e8fb3..195eaeb3 100644 --- a/docs/1.guide/_capabilities-table.md +++ b/docs/1.guide/_capabilities-table.md @@ -1,14 +1,15 @@ - + | Connector | JSON | Bool | Array | Date | UUID | Tx | Batch | | :--------:|:---:|:---:|:----:|:---:|:---:|:-:|:----: | -| better-sqlite3 | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | -| sqlite3 | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | -| bun-sqlite | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | -| node-sqlite | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | -| libsql | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | -| cloudflare-d1 | ✓ | — | ✓ | ✓ | ✓ | — | ✓ | +| better-sqlite3 | ✓ | — | — | — | — | ✓ | ✓ | +| sqlite3 | ✓ | — | — | — | — | ✓ | ✓ | +| bun-sqlite | ✓ | — | — | — | — | ✓ | ✓ | +| node-sqlite | ✓ | — | — | — | — | ✓ | ✓ | +| libsql | ✓ | — | — | — | — | ✓ | ✓ | +| cloudflare-d1 | ✓ | — | — | — | — | ✓ | ✓ | | postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | pglite | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | cloudflare-hyperdrive-postgresql | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| mysql2 | ✓ | ✓ | — | ✓ | — | — | ✓ | -| cloudflare-hyperdrive-mysql | ✓ | ✓ | — | ✓ | — | — | ✓ | +| mysql2 | ✓ | ✓ | — | ✓ | — | ✓ | ✓ | +| planetscale | ✓ | ✓ | — | ✓ | — | ✓ | ✓ | +| cloudflare-hyperdrive-mysql | ✓ | ✓ | — | ✓ | — | ✓ | ✓ | diff --git a/scripts/gen-capabilities-docs.ts b/scripts/gen-capabilities-docs.ts index cd12e047..dd82aaa7 100644 --- a/scripts/gen-capabilities-docs.ts +++ b/scripts/gen-capabilities-docs.ts @@ -1,50 +1,16 @@ 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 DB_COMPAT_URL = "https://raw.githubusercontent.com/onmax/db-compat/main/packages/data/data.json"; - const outputFile = fileURLToPath(new URL("../docs/1.guide/_capabilities-table.md", import.meta.url)); -interface CompatTarget { dialect: string; driver: string; version: string } -interface CompatEntry { support: Record } -interface CompatData { - __meta: { targets: Record }; - sql: { - types: Record; - transactions: Record; - }; -} - -// Map db0 connector names to db-compat target IDs -const targetMapping: Record = { - "cloudflare-hyperdrive-postgresql": "hyperdrive-postgresql", - "cloudflare-hyperdrive-mysql": "hyperdrive-mysql", -}; - const db0Connectors = [ "better-sqlite3", "sqlite3", "bun-sqlite", "node-sqlite", "libsql", "cloudflare-d1", "postgresql", "pglite", "cloudflare-hyperdrive-postgresql", - "mysql2", "cloudflare-hyperdrive-mysql", + "mysql2", "planetscale", "cloudflare-hyperdrive-mysql", ]; -function toDb0Capabilities(data: CompatData, targetId: string): DatabaseCapabilities { - const id = targetMapping[targetId] || targetId; - const target = data.__meta.targets[id]; - const types = data.sql.types; - const tx = data.sql.transactions; - - return { - supportsJSON: types.type_json.support[id].supported, - supportsBooleans: target.dialect !== "sqlite", - supportsArrays: types.type_array.support[id].supported, - supportsDates: types.type_date.support[id].supported, - supportsUUIDs: types.type_uuid.support[id].supported, - supportsTransactions: tx.BEGIN.support[id].supported, - supportsBatch: tx.batch_atomicity.support[id].supported, - }; -} - const capabilityLabels: Record = { supportsJSON: "JSON", supportsBooleans: "Bool", @@ -73,15 +39,18 @@ function generateTable(connectorCapabilities: Record `| ${line} |`).join("\n"); } -console.log("Fetching db-compat data..."); -const data: CompatData = await fetch(DB_COMPAT_URL).then((r) => r.json()); - -const connectorCapabilities: Record = Object.fromEntries( - db0Connectors.map((id) => [id, toDb0Capabilities(data, id)]), +const orderedCapabilities: Record = 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 = ` -${generateTable(connectorCapabilities)} +const content = ` +${generateTable(orderedCapabilities)} `; await writeFile(outputFile, content, "utf8");