diff --git a/AGENTS.md b/AGENTS.md index 47532b16..b9ab5774 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 ` | 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 ` | Run a specific test | +| `pnpm lint` / `pnpm fmt` | ESLint + Prettier | Package manager: **pnpm**. Build tool: **obuild**. Typecheck: **tsgo**. @@ -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 diff --git a/docs/1.guide/2.capabilities.md b/docs/1.guide/2.capabilities.md new file mode 100644 index 00000000..c4ed8a38 --- /dev/null +++ b/docs/1.guide/2.capabilities.md @@ -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. + + + + + +| 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 | ✓ | — | — | — | — | ✓ | + + + +> [!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}`; + }, + }; +} +``` diff --git a/docs/1.guide/_capabilities-table.md b/docs/1.guide/_capabilities-table.md new file mode 100644 index 00000000..83dea8c1 --- /dev/null +++ b/docs/1.guide/_capabilities-table.md @@ -0,0 +1,20 @@ + + +| 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 | ✓ | — | — | — | — | ✓ | diff --git a/package.json b/package.json index 0bc68a7a..b3a060f0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/_capabilities-data.ts b/scripts/_capabilities-data.ts new file mode 100644 index 00000000..bc18abbb --- /dev/null +++ b/scripts/_capabilities-data.ts @@ -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` 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 = { + json: "JSON", + booleans: "Bool", + arrays: "Array", + dates: "Date", + uuids: "UUID", + transactions: "Tx", +}; diff --git a/scripts/gen-capabilities-docs.ts b/scripts/gen-capabilities-docs.ts new file mode 100644 index 00000000..415da438 --- /dev/null +++ b/scripts/gen-capabilities-docs.ts @@ -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(); + 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 = ` + +${generateTable(canonicalConnectorNames())} +`; + +await writeFile(outputFile, content, "utf8"); +console.log("Generated capabilities table to", outputFile); diff --git a/scripts/gen-connectors.ts b/scripts/gen-connectors.ts index e69f34e6..59a3710c 100644 --- a/scripts/gen-connectors.ts +++ b/scripts/gen-connectors.ts @@ -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])]; diff --git a/src/capabilities.ts b/src/capabilities.ts new file mode 100644 index 00000000..1282bd1e --- /dev/null +++ b/src/capabilities.ts @@ -0,0 +1,45 @@ +import type { DatabaseCapabilities, SQLDialect } from "./types.ts"; + +const sqlite: DatabaseCapabilities = Object.freeze({ + json: true, + booleans: false, + arrays: false, + dates: false, + uuids: false, + transactions: true, +}); + +const postgresql: DatabaseCapabilities = Object.freeze({ + json: true, + booleans: true, + arrays: true, + dates: true, + uuids: true, + transactions: true, +}); + +const mysql: DatabaseCapabilities = Object.freeze({ + json: true, + booleans: true, + arrays: false, + dates: true, + uuids: false, + transactions: true, +}); + +export const dialectCapabilities: Record = + Object.freeze({ + sqlite, + libsql: sqlite, + postgresql, + mysql, + }); + +export function getCapabilities( + dialect: SQLDialect, + overrides?: Partial, +): DatabaseCapabilities { + return overrides + ? Object.freeze({ ...dialectCapabilities[dialect], ...overrides }) + : dialectCapabilities[dialect]; +} diff --git a/src/connectors/cloudflare-d1.ts b/src/connectors/cloudflare-d1.ts index f4a36999..caf7d1e8 100644 --- a/src/connectors/cloudflare-d1.ts +++ b/src/connectors/cloudflare-d1.ts @@ -27,6 +27,10 @@ export default function cloudflareD1Connector( return { name: "cloudflare-d1", dialect: "sqlite", + // D1 has no explicit transactions (`BEGIN`/`COMMIT` are rejected); + // only implicit ones via `D1Database.batch()`. + // https://developers.cloudflare.com/d1/worker-api/d1-database/#batch + capabilityOverrides: { transactions: false }, getInstance: () => getDB(), exec: (sql) => getDB().exec(sql), prepare: (sql) => new StatementWrapper(getDB().prepare(sql)), diff --git a/src/connectors/libsql/core.ts b/src/connectors/libsql/core.ts index 9099ca8a..67a7ca00 100644 --- a/src/connectors/libsql/core.ts +++ b/src/connectors/libsql/core.ts @@ -1,10 +1,11 @@ import type { Client, InStatement } from "@libsql/client"; -import type { Connector, Primitive } from "db0"; +import type { Connector, DatabaseCapabilities, Primitive } from "db0"; import { BoundableStatement } from "../_internal/statement.ts"; export type ConnectorOptions = { getClient: () => Client | Promise; name?: string; + capabilityOverrides?: Partial; dispose?: () => void | Promise; }; @@ -19,6 +20,7 @@ export default function libSqlCoreConnector( return { name: opts.name || "libsql-core", dialect: "libsql", + capabilityOverrides: opts.capabilityOverrides, getInstance: async () => opts.getClient(), exec: (sql) => query(sql), prepare: (sql) => new StatementWrapper(sql, query), diff --git a/src/connectors/libsql/http.ts b/src/connectors/libsql/http.ts index 5a903546..6a2c18e7 100644 --- a/src/connectors/libsql/http.ts +++ b/src/connectors/libsql/http.ts @@ -39,6 +39,10 @@ export default function libSqlConnector( return libSqlCore({ name: CONNECTOR_NAME, + // Every `client.execute()` opens a fresh Hrana stream and closes it in the + // same request, so `BEGIN`/`COMMIT` sent as separate statements never share + // a stream. Transactions require `client.transaction()`. + capabilityOverrides: { transactions: false }, getClient, dispose: async () => { const client = await getClient.current; diff --git a/src/connectors/libsql/web.ts b/src/connectors/libsql/web.ts index f32ff31b..8b81b90a 100644 --- a/src/connectors/libsql/web.ts +++ b/src/connectors/libsql/web.ts @@ -39,6 +39,10 @@ export default function libSqlConnector( return libSqlCore({ name: CONNECTOR_NAME, + // Every `client.execute()` opens a fresh Hrana stream and closes it in the + // same request, so `BEGIN`/`COMMIT` sent as separate statements never share + // a stream. Transactions require `client.transaction()`. + capabilityOverrides: { transactions: false }, getClient, dispose: async () => { const client = await getClient.current; diff --git a/src/connectors/planetscale.ts b/src/connectors/planetscale.ts index 24a6b2fb..6c4106d2 100644 --- a/src/connectors/planetscale.ts +++ b/src/connectors/planetscale.ts @@ -52,6 +52,10 @@ export default function planetscaleConnector( return { name: "planetscale", dialect: "mysql", + // `Client.execute()` opens a new `Connection` (and a new session) per query, + // so `BEGIN`/`COMMIT` issued as separate statements never share a session. + // Transactions require `Client.transaction()`. + capabilityOverrides: { transactions: false }, getInstance: () => getClient(), exec: (sql) => query(sql), prepare: (sql) => new StatementWrapper(sql, query), diff --git a/src/database.ts b/src/database.ts index e8de7a27..b5fa7f41 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"; @@ -20,6 +21,11 @@ const DISPOSED_ERR = export function createDatabase( connector: TConnector, ): Database { + const capabilities = getCapabilities( + connector.dialect, + connector.capabilityOverrides, + ); + let _disposed = false; const checkDisposed = () => { if (_disposed) { @@ -38,6 +44,10 @@ export function createDatabase( return connector.dialect; }, + get capabilities() { + return capabilities; + }, + get disposed() { return _disposed; }, diff --git a/src/index.ts b/src/index.ts index 9e112f2e..ef7850b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export { createDatabase } from "./database.ts"; +export { dialectCapabilities, getCapabilities } from "./capabilities.ts"; export { connectors, connectorDependencies } from "./_connectors.ts"; @@ -7,6 +8,7 @@ export type { ConnectorDependency, ConnectorDependencies, Database, + DatabaseCapabilities, ExecResult, LibImport, Primitive, diff --git a/src/types.ts b/src/types.ts index 1b06f4ce..20d293e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,15 @@ export type Primitive = string | number | boolean | undefined | null; export type SQLDialect = "mysql" | "postgresql" | "sqlite" | "libsql"; +export interface DatabaseCapabilities { + readonly json: boolean; + readonly booleans: boolean; + readonly arrays: boolean; + readonly dates: boolean; + readonly uuids: boolean; + readonly transactions: boolean; +} + export type Statement = { /** * Binds parameters to the statement. @@ -83,6 +92,11 @@ export type Connector = { */ dialect: SQLDialect; + /** + * Override specific database capabilities for this connector. + */ + capabilityOverrides?: Partial; + /** * The client instance used internally. */ @@ -126,6 +140,11 @@ export interface Database< readonly connector: ConnectorName; 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/connector-capabilities.test.ts b/test/connector-capabilities.test.ts new file mode 100644 index 00000000..a63d8a9b --- /dev/null +++ b/test/connector-capabilities.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from "vitest"; +import { connectors, type ConnectorName } from "../src/_connectors"; +import { getCapabilities } from "../src/capabilities"; +import { connectorCapabilities } from "../scripts/_capabilities-data"; + +/** + * `bun-sqlite` statically imports `bun:sqlite`, which cannot be resolved under + * Node — it is covered by `test/connectors/bun-test.ts` instead. + */ +const NOT_LOADABLE_IN_NODE = new Set(["bun-sqlite", "bun"]); + +describe("connector capabilities", () => { + for (const name of Object.keys(connectors) as ConnectorName[]) { + it.skipIf(NOT_LOADABLE_IN_NODE.has(name))(name, async () => { + const specifier = connectors[name].replace( + "db0/connectors/", + "../src/connectors/", + ); + const { default: createConnector } = await import(specifier); + + // Connectors are lazy: building one never touches the underlying driver, + // so `{}` is enough to read its declared `dialect`/`capabilityOverrides`. + const connector = createConnector({}); + + expect( + getCapabilities(connector.dialect, connector.capabilityOverrides), + `${name}: docs table is out of sync with the connector`, + ).toEqual(connectorCapabilities[name]); + }); + } + + it("aliases share the same capabilities", () => { + expect(connectorCapabilities["libsql"]).toEqual( + connectorCapabilities["libsql-node"], + ); + expect(connectorCapabilities["bun"]).toEqual( + connectorCapabilities["bun-sqlite"], + ); + expect(connectorCapabilities["sqlite"]).toEqual( + connectorCapabilities["node-sqlite"], + ); + }); +}); diff --git a/test/connectors/_tests.ts b/test/connectors/_tests.ts index 7f42d784..cf664737 100644 --- a/test/connectors/_tests.ts +++ b/test/connectors/_tests.ts @@ -2,13 +2,16 @@ import { beforeAll, expect, it } from "vitest"; import { Connector, Database, + DatabaseCapabilities, createDatabase, + getCapabilities, type SQLDialect, } from "../../src"; export function testConnector(opts: { connector: TConnector; dialect: SQLDialect; + capabilities?: Partial; }) { let db: Database; beforeAll(() => { @@ -37,6 +40,15 @@ export function testConnector(opts: { expect(db.dialect).toBe(opts.dialect); }); + it("capabilities match", () => { + expect(db.capabilities).toEqual( + getCapabilities(opts.dialect, opts.capabilities), + ); + // Capabilities are a stable, immutable snapshot. + expect(db.capabilities).toBe(db.capabilities); + expect(Object.isFrozen(db.capabilities)).toBe(true); + }); + it("drop and create table", async () => { await db.sql`DROP TABLE IF EXISTS users`; switch (opts.dialect) { diff --git a/test/connectors/cloudflare/cloudflare-d1.test.ts b/test/connectors/cloudflare/cloudflare-d1.test.ts index 92797505..df8ee30e 100644 --- a/test/connectors/cloudflare/cloudflare-d1.test.ts +++ b/test/connectors/cloudflare/cloudflare-d1.test.ts @@ -21,6 +21,7 @@ describe("connectors: cloudflare-d1", () => { testConnector({ dialect: "sqlite", + capabilities: { transactions: false }, connector: cloudflareD1({ bindingName: "test", }), diff --git a/test/connectors/planetscale.test.ts b/test/connectors/planetscale.test.ts index a254f9e7..cf9be0bc 100644 --- a/test/connectors/planetscale.test.ts +++ b/test/connectors/planetscale.test.ts @@ -9,6 +9,7 @@ describe.runIf( )("connectors: planetscale.test", () => { testConnector({ dialect: "mysql", + capabilities: { transactions: false }, connector: connector({ host: process.env.PLANETSCALE_HOST!, username: process.env.PLANETSCALE_USERNAME!, diff --git a/tsconfig.json b/tsconfig.json index 708a51d9..8a26f039 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,12 +16,12 @@ "noImplicitOverride": true, "noEmit": true, "paths": { - "db0/connectors/*": [ - "./src/connectors/*" - ] + "db0": ["./src/index.ts"], + "db0/connectors/*": ["./src/connectors/*"] } }, "include": [ - "src" + "src", + "scripts" ] }