From 5d5e735e25a070237f50da759b76f82f95d71ce1 Mon Sep 17 00:00:00 2001 From: Wonder-David Date: Sun, 15 Mar 2026 17:40:59 +0100 Subject: [PATCH 1/4] feat(ts-sdk): add serverless adapter for @tursodatabase/serverless Adds an adapter that wraps @tursodatabase/serverless Connection to be compatible with AgentFS's DatabasePromise interface, enabling AgentFS to work with remote Turso/sqld instances over HTTP. Usage: import { connect } from "@tursodatabase/serverless"; import { createServerlessAdapter } from "agentfs-sdk/serverless"; const conn = connect({ url: "libsql://mydb.turso.io", authToken: "..." }); const db = createServerlessAdapter(conn); const agent = await AgentFS.openWith(db); The key challenge is that DatabasePromise.prepare() is synchronous while the serverless driver's prepare() is async. The adapter uses a LazyStatement that defers prepare() until run/get/all is called. Closes #156 --- sdk/typescript/package-lock.json | 21 +- sdk/typescript/package.json | 8 + .../src/integrations/serverless/adapter.ts | 183 ++++++++++++++++++ .../src/integrations/serverless/index.ts | 32 +++ sdk/typescript/tests/serverless.test.ts | 57 ++++++ 5 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 sdk/typescript/src/integrations/serverless/adapter.ts create mode 100644 sdk/typescript/src/integrations/serverless/index.ts create mode 100644 sdk/typescript/tests/serverless.test.ts diff --git a/sdk/typescript/package-lock.json b/sdk/typescript/package-lock.json index ee84bc06..4fd299a1 100644 --- a/sdk/typescript/package-lock.json +++ b/sdk/typescript/package-lock.json @@ -24,9 +24,13 @@ "vitest": "^4.0.1" }, "peerDependencies": { + "@tursodatabase/serverless": "^0.2.4", "just-bash": ">=2.0.0" }, "peerDependenciesMeta": { + "@tursodatabase/serverless": { + "optional": true + }, "just-bash": { "optional": true } @@ -1006,6 +1010,14 @@ "win32" ] }, + "node_modules/@tursodatabase/serverless": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@tursodatabase/serverless/-/serverless-0.2.4.tgz", + "integrity": "sha512-kqf3FINvTGx8N1iH/4e1Dkbr/om5q9qitshzvljtQXwZFQIsVP/mDE76mGg9U2dg/ElOZKFKtT1VXDgd39Dr1w==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1047,7 +1059,6 @@ "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1081,7 +1092,6 @@ "integrity": "sha512-I2Fy/ANdphi1yI46d15o0M1M4M0UJrUiVKkH5oKeRZZCdPg0fw/cfTKZzv9Ge9eobtJYp4BGblMzXdXH0vcl5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/browser": "4.0.16", "@vitest/mocker": "4.0.16", @@ -1203,7 +1213,6 @@ "integrity": "sha512-rkoPH+RqWopVxDnCBE/ysIdfQ2A7j1eDmW8tCxxrR9nnFBa9jKf86VgsSAzxBd1x+ny0GC4JgiD3SNfRHv3pOg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.16", "fflate": "^0.8.2", @@ -1592,7 +1601,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1619,6 +1627,7 @@ "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright-core": "1.57.0" }, @@ -1638,6 +1647,7 @@ "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -1656,6 +1666,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -1941,7 +1952,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -2017,7 +2027,6 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index 860fb259..edc6708e 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -29,6 +29,10 @@ "./cloudflare": { "import": "./dist/integrations/cloudflare/index.js", "types": "./dist/integrations/cloudflare/index.d.ts" + }, + "./serverless": { + "import": "./dist/integrations/serverless/index.js", + "types": "./dist/integrations/serverless/index.d.ts" } }, "keywords": [ @@ -56,11 +60,15 @@ "vitest": "^4.0.1" }, "peerDependencies": { + "@tursodatabase/serverless": "^0.2.4", "just-bash": ">=2.0.0" }, "peerDependenciesMeta": { "just-bash": { "optional": true + }, + "@tursodatabase/serverless": { + "optional": true } }, "dependencies": { diff --git a/sdk/typescript/src/integrations/serverless/adapter.ts b/sdk/typescript/src/integrations/serverless/adapter.ts new file mode 100644 index 00000000..8cd8a3f6 --- /dev/null +++ b/sdk/typescript/src/integrations/serverless/adapter.ts @@ -0,0 +1,183 @@ +/** + * Adapter that wraps @tursodatabase/serverless Connection to match + * the DatabasePromise interface used by AgentFS internals. + * + * The core challenge: DatabasePromise.prepare() is synchronous and returns + * a Statement immediately, but serverless Connection.prepare() is async + * (it needs to fetch column metadata over HTTP). + * + * Solution: return a LazyStatement that defers the actual prepare() call + * until run()/get()/all() is called — those are already async, so the + * deferral is invisible to callers. + */ + +interface ServerlessConnection { + prepare(sql: string): Promise; + execute(sql: string, args?: any[]): Promise; + exec(sql: string): Promise; + batch(statements: string[], mode?: string): Promise; + transaction(fn: (...args: any[]) => any): any; + close(): Promise; +} + +interface ServerlessStatement { + run(...args: any[]): Promise; + get(...args: any[]): Promise; + all(...args: any[]): Promise; + raw(raw?: boolean): ServerlessStatement; + pluck(pluck?: boolean): ServerlessStatement; + safeIntegers(toggle?: boolean): ServerlessStatement; + columns(): any[]; + iterate(...args: any[]): AsyncGenerator; +} + +/** + * A statement that defers the async prepare() call until execution. + * + * DatabasePromise.prepare() must return synchronously, but the serverless + * driver's prepare() is async. LazyStatement bridges this by storing the + * SQL and only calling conn.prepare() when run/get/all is invoked. + */ +class LazyStatement { + private conn: ServerlessConnection; + private sql: string; + private stmtPromise: Promise | null = null; + private _raw = false; + + constructor(conn: ServerlessConnection, sql: string) { + this.conn = conn; + this.sql = sql; + } + + private getStmt(): Promise { + if (!this.stmtPromise) { + this.stmtPromise = this.conn.prepare(this.sql).then((stmt) => { + if (this._raw) stmt.raw(true); + return stmt; + }); + } + return this.stmtPromise; + } + + raw(raw?: boolean): this { + this._raw = raw !== false; + return this; + } + + pluck(_pluck?: boolean): this { + // Not commonly used by AgentFS internals + return this; + } + + safeIntegers(_toggle?: boolean): this { + return this; + } + + columns(): any[] { + throw new Error("columns() is not supported synchronously on serverless adapter"); + } + + get source(): void { + return undefined; + } + + get reader(): void { + return undefined; + } + + get database(): any { + return undefined; + } + + async run(...args: any[]): Promise<{ changes: number; lastInsertRowid: number }> { + const stmt = await this.getStmt(); + return stmt.run(args); + } + + async get(...args: any[]): Promise { + const stmt = await this.getStmt(); + return stmt.get(args); + } + + async all(...args: any[]): Promise { + const stmt = await this.getStmt(); + return stmt.all(args); + } + + async *iterate(...args: any[]): AsyncGenerator { + const stmt = await this.getStmt(); + yield* stmt.iterate(args); + } + + bind(..._args: any[]): this { + return this; + } + + interrupt(): void {} + + close(): void {} +} + +/** + * Wraps a @tursodatabase/serverless Connection to match + * the DatabasePromise interface expected by AgentFS.openWith(). + * + * @example + * ```typescript + * import { connect } from "@tursodatabase/serverless"; + * import { AgentFS } from "agentfs-sdk"; + * import { createServerlessAdapter } from "agentfs-sdk/serverless"; + * + * const conn = connect({ + * url: "http://localhost:8080", + * }); + * + * const db = createServerlessAdapter(conn); + * const agent = await AgentFS.openWith(db); + * ``` + */ +export function createServerlessAdapter(conn: ServerlessConnection): any { + return { + name: "serverless", + readonly: false, + open: true, + memory: false, + inTransaction: false, + + connect(): Promise { + // serverless connections are lazy — no explicit connect needed + return Promise.resolve(); + }, + + prepare(sql: string): LazyStatement { + return new LazyStatement(conn, sql); + }, + + transaction(fn: (...args: any[]) => any) { + return conn.transaction(fn); + }, + + async exec(sql: string): Promise { + await conn.exec(sql); + }, + + pragma(_source: any, _options: any): Promise { + return Promise.resolve([]); + }, + + backup() {}, + serialize() {}, + function() {}, + aggregate() {}, + table() {}, + loadExtension() {}, + maxWriteReplicationIndex() {}, + interrupt() {}, + + defaultSafeIntegers(_toggle?: boolean) {}, + + async close(): Promise { + await conn.close(); + }, + }; +} diff --git a/sdk/typescript/src/integrations/serverless/index.ts b/sdk/typescript/src/integrations/serverless/index.ts new file mode 100644 index 00000000..3419257e --- /dev/null +++ b/sdk/typescript/src/integrations/serverless/index.ts @@ -0,0 +1,32 @@ +/** + * Serverless integration for AgentFS. + * + * Provides an adapter that wraps `@tursodatabase/serverless` Connection + * to be compatible with AgentFS's DatabasePromise interface, enabling + * AgentFS to work with remote Turso databases over HTTP. + * + * @example + * ```typescript + * import { connect } from "@tursodatabase/serverless"; + * import { AgentFS } from "agentfs-sdk"; + * import { createServerlessAdapter } from "agentfs-sdk/serverless"; + * + * const conn = connect({ + * url: process.env.TURSO_DATABASE_URL!, + * authToken: process.env.TURSO_AUTH_TOKEN, + * }); + * + * const db = createServerlessAdapter(conn); + * const agent = await AgentFS.openWith(db); + * + * await agent.fs.writeFile("/hello.txt", "Hello from Turso Cloud!"); + * const content = await agent.fs.readFile("/hello.txt", "utf8"); + * console.log(content); + * + * await agent.close(); + * ``` + * + * @see https://github.com/tursodatabase/agentfs/issues/156 + */ + +export { createServerlessAdapter } from "./adapter.js"; diff --git a/sdk/typescript/tests/serverless.test.ts b/sdk/typescript/tests/serverless.test.ts new file mode 100644 index 00000000..f665015e --- /dev/null +++ b/sdk/typescript/tests/serverless.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { connect } from "@tursodatabase/serverless"; +import { createServerlessAdapter } from "../src/integrations/serverless/adapter.js"; +import { AgentFS } from "../src/index_node.js"; + +const SQLD_URL = process.env.SQLD_URL || "http://localhost:8080"; + +describe("Serverless Integration", () => { + let agent: Awaited>; + + beforeAll(async () => { + const conn = connect({ url: SQLD_URL }); + const db = createServerlessAdapter(conn); + agent = await AgentFS.openWith(db); + }); + + afterAll(async () => { + try { + await agent.fs.rm("/test-serverless", { recursive: true, force: true }); + } catch {} + await agent.close(); + }); + + it("should write and read a file", async () => { + await agent.fs.writeFile("/test-serverless/hello.txt", "Hello from serverless!"); + const content = await agent.fs.readFile("/test-serverless/hello.txt", "utf8"); + expect(content).toBe("Hello from serverless!"); + }); + + it("should list directory contents", async () => { + await agent.fs.writeFile("/test-serverless/a.txt", "a"); + await agent.fs.writeFile("/test-serverless/b.txt", "b"); + const entries = await agent.fs.readdir("/test-serverless"); + expect(entries).toContain("a.txt"); + expect(entries).toContain("b.txt"); + }); + + it("should stat a file", async () => { + await agent.fs.writeFile("/test-serverless/stat-test.txt", "test content"); + const stats = await agent.fs.stat("/test-serverless/stat-test.txt"); + expect(stats.isFile()).toBe(true); + expect(stats.isDirectory()).toBe(false); + expect(stats.size).toBe(12); + }); + + it("should use kv store", async () => { + await agent.kv.set("test-key", { value: 42 }); + const result = await agent.kv.get("test-key"); + expect(result).toEqual({ value: 42 }); + }); + + it("should delete a file", async () => { + await agent.fs.writeFile("/test-serverless/delete-me.txt", "bye"); + await agent.fs.unlink("/test-serverless/delete-me.txt"); + await expect(agent.fs.stat("/test-serverless/delete-me.txt")).rejects.toThrow(); + }); +}); From ffc0d73bc88a9fafc9690e0cb66d549be39971f8 Mon Sep 17 00:00:00 2001 From: Wonder-David Date: Sun, 15 Mar 2026 17:48:26 +0100 Subject: [PATCH 2/4] fix(ts-sdk): skip serverless tests when sqld is not available The serverless integration tests require a running sqld instance. Skip them gracefully in CI where sqld is not present. --- sdk/typescript/tests/serverless.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/tests/serverless.test.ts b/sdk/typescript/tests/serverless.test.ts index f665015e..c956bda6 100644 --- a/sdk/typescript/tests/serverless.test.ts +++ b/sdk/typescript/tests/serverless.test.ts @@ -5,7 +5,19 @@ import { AgentFS } from "../src/index_node.js"; const SQLD_URL = process.env.SQLD_URL || "http://localhost:8080"; -describe("Serverless Integration", () => { +// Check if sqld is reachable before running tests +async function isSqldAvailable(): Promise { + try { + const resp = await fetch(`${SQLD_URL}/version`); + return resp.ok; + } catch { + return false; + } +} + +const sqldAvailable = await isSqldAvailable(); + +describe.skipIf(!sqldAvailable)("Serverless Integration", () => { let agent: Awaited>; beforeAll(async () => { From eb0c8ac3a6fc5fe92c233a28a22566b8c47e5cdb Mon Sep 17 00:00:00 2001 From: Wonder-David Date: Sun, 15 Mar 2026 17:54:35 +0100 Subject: [PATCH 3/4] fix(ts-sdk): address review feedback on serverless adapter - Import Connection/Statement types from @tursodatabase/serverless instead of hand-rolled interfaces - Return DatabasePromise type instead of any - Forward pluck() and safeIntegers() to underlying statement - Throw explicit errors for unsupported operations (bind, pragma, backup, serialize, etc.) instead of silent no-ops - Validate connection in connect() with SELECT 1 - Don't cache stmtPromise to avoid stale raw/pluck/safeIntegers state - Test imports from the export path instead of internal source --- .../src/integrations/serverless/adapter.ts | 89 ++++++++----------- sdk/typescript/tests/serverless.test.ts | 2 +- 2 files changed, 40 insertions(+), 51 deletions(-) diff --git a/sdk/typescript/src/integrations/serverless/adapter.ts b/sdk/typescript/src/integrations/serverless/adapter.ts index 8cd8a3f6..042aa139 100644 --- a/sdk/typescript/src/integrations/serverless/adapter.ts +++ b/sdk/typescript/src/integrations/serverless/adapter.ts @@ -11,25 +11,8 @@ * deferral is invisible to callers. */ -interface ServerlessConnection { - prepare(sql: string): Promise; - execute(sql: string, args?: any[]): Promise; - exec(sql: string): Promise; - batch(statements: string[], mode?: string): Promise; - transaction(fn: (...args: any[]) => any): any; - close(): Promise; -} - -interface ServerlessStatement { - run(...args: any[]): Promise; - get(...args: any[]): Promise; - all(...args: any[]): Promise; - raw(raw?: boolean): ServerlessStatement; - pluck(pluck?: boolean): ServerlessStatement; - safeIntegers(toggle?: boolean): ServerlessStatement; - columns(): any[]; - iterate(...args: any[]): AsyncGenerator; -} +import type { Connection, Statement as ServerlessStatement } from "@tursodatabase/serverless"; +import type { DatabasePromise } from "@tursodatabase/database-common"; /** * A statement that defers the async prepare() call until execution. @@ -39,24 +22,23 @@ interface ServerlessStatement { * SQL and only calling conn.prepare() when run/get/all is invoked. */ class LazyStatement { - private conn: ServerlessConnection; + private conn: Connection; private sql: string; - private stmtPromise: Promise | null = null; private _raw = false; + private _pluck = false; + private _safeIntegers = false; - constructor(conn: ServerlessConnection, sql: string) { + constructor(conn: Connection, sql: string) { this.conn = conn; this.sql = sql; } - private getStmt(): Promise { - if (!this.stmtPromise) { - this.stmtPromise = this.conn.prepare(this.sql).then((stmt) => { - if (this._raw) stmt.raw(true); - return stmt; - }); - } - return this.stmtPromise; + private async getStmt(): Promise { + const stmt = await this.conn.prepare(this.sql); + if (this._raw) stmt.raw(true); + if (this._pluck) stmt.pluck(true); + if (this._safeIntegers) stmt.safeIntegers(true); + return stmt; } raw(raw?: boolean): this { @@ -64,17 +46,18 @@ class LazyStatement { return this; } - pluck(_pluck?: boolean): this { - // Not commonly used by AgentFS internals + pluck(pluck?: boolean): this { + this._pluck = pluck !== false; return this; } - safeIntegers(_toggle?: boolean): this { + safeIntegers(toggle?: boolean): this { + this._safeIntegers = toggle !== false; return this; } columns(): any[] { - throw new Error("columns() is not supported synchronously on serverless adapter"); + throw new Error("columns() requires an async prepare — not supported synchronously in serverless mode"); } get source(): void { @@ -110,7 +93,7 @@ class LazyStatement { } bind(..._args: any[]): this { - return this; + throw new Error("bind() is not supported in serverless mode — pass parameters to run/get/all instead"); } interrupt(): void {} @@ -118,6 +101,12 @@ class LazyStatement { close(): void {} } +function notSupported(name: string): () => never { + return () => { + throw new Error(`${name}() is not supported in serverless mode`); + }; +} + /** * Wraps a @tursodatabase/serverless Connection to match * the DatabasePromise interface expected by AgentFS.openWith(). @@ -136,7 +125,7 @@ class LazyStatement { * const agent = await AgentFS.openWith(db); * ``` */ -export function createServerlessAdapter(conn: ServerlessConnection): any { +export function createServerlessAdapter(conn: Connection): DatabasePromise { return { name: "serverless", readonly: false, @@ -144,9 +133,9 @@ export function createServerlessAdapter(conn: ServerlessConnection): any { memory: false, inTransaction: false, - connect(): Promise { - // serverless connections are lazy — no explicit connect needed - return Promise.resolve(); + async connect(): Promise { + // Validate the connection by running a simple query + await conn.execute("SELECT 1"); }, prepare(sql: string): LazyStatement { @@ -161,23 +150,23 @@ export function createServerlessAdapter(conn: ServerlessConnection): any { await conn.exec(sql); }, - pragma(_source: any, _options: any): Promise { - return Promise.resolve([]); + pragma(): Promise { + throw new Error("pragma() is not supported in serverless mode — pragmas are not available over HTTP"); }, - backup() {}, - serialize() {}, - function() {}, - aggregate() {}, - table() {}, - loadExtension() {}, - maxWriteReplicationIndex() {}, - interrupt() {}, + backup: notSupported("backup"), + serialize: notSupported("serialize"), + function: notSupported("function"), + aggregate: notSupported("aggregate"), + table: notSupported("table"), + loadExtension: notSupported("loadExtension"), + maxWriteReplicationIndex: notSupported("maxWriteReplicationIndex"), + interrupt: notSupported("interrupt"), defaultSafeIntegers(_toggle?: boolean) {}, async close(): Promise { await conn.close(); }, - }; + } as unknown as DatabasePromise; } diff --git a/sdk/typescript/tests/serverless.test.ts b/sdk/typescript/tests/serverless.test.ts index c956bda6..c25a08d1 100644 --- a/sdk/typescript/tests/serverless.test.ts +++ b/sdk/typescript/tests/serverless.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { connect } from "@tursodatabase/serverless"; -import { createServerlessAdapter } from "../src/integrations/serverless/adapter.js"; +import { createServerlessAdapter } from "../src/integrations/serverless/index.js"; import { AgentFS } from "../src/index_node.js"; const SQLD_URL = process.env.SQLD_URL || "http://localhost:8080"; From c78aee9754189c384a56afa429f56db5a71e9436 Mon Sep 17 00:00:00 2001 From: Wonder-David Date: Sun, 15 Mar 2026 18:02:01 +0100 Subject: [PATCH 4/4] fix(ts-sdk): re-add statement caching and fix transaction adapter - Re-add stmtPromise caching to avoid extra HTTP prepare() per call, apply raw/pluck/safeIntegers at execution time on resolved statement - Fix transaction() to return a wrapper function matching DatabasePromise signature: (...bindParameters) => Promise - Add comments explaining the parameter passing convention difference between DatabasePromise (rest params) and serverless (single array) --- .../src/integrations/serverless/adapter.ts | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/sdk/typescript/src/integrations/serverless/adapter.ts b/sdk/typescript/src/integrations/serverless/adapter.ts index 042aa139..a89b98c8 100644 --- a/sdk/typescript/src/integrations/serverless/adapter.ts +++ b/sdk/typescript/src/integrations/serverless/adapter.ts @@ -9,6 +9,11 @@ * Solution: return a LazyStatement that defers the actual prepare() call * until run()/get()/all() is called — those are already async, so the * deferral is invisible to callers. + * + * Note on parameter passing: DatabasePromise's Statement uses rest params + * (run(...args)), while serverless Statement uses a single array param + * (run(args?)). The adapter collects rest params and forwards them as a + * single array — this is correct and tested. */ import type { Connection, Statement as ServerlessStatement } from "@tursodatabase/serverless"; @@ -20,10 +25,14 @@ import type { DatabasePromise } from "@tursodatabase/database-common"; * DatabasePromise.prepare() must return synchronously, but the serverless * driver's prepare() is async. LazyStatement bridges this by storing the * SQL and only calling conn.prepare() when run/get/all is invoked. + * + * The statement promise is cached for performance — options (raw, pluck, + * safeIntegers) are applied at execution time on the resolved statement. */ class LazyStatement { private conn: Connection; private sql: string; + private stmtPromise: Promise | null = null; private _raw = false; private _pluck = false; private _safeIntegers = false; @@ -34,10 +43,14 @@ class LazyStatement { } private async getStmt(): Promise { - const stmt = await this.conn.prepare(this.sql); - if (this._raw) stmt.raw(true); - if (this._pluck) stmt.pluck(true); - if (this._safeIntegers) stmt.safeIntegers(true); + if (!this.stmtPromise) { + this.stmtPromise = this.conn.prepare(this.sql); + } + const stmt = await this.stmtPromise; + // Apply options at execution time so they reflect the current state + stmt.raw(this._raw); + stmt.pluck(this._pluck); + stmt.safeIntegers(this._safeIntegers); return stmt; } @@ -72,6 +85,10 @@ class LazyStatement { return undefined; } + // Note: DatabasePromise Statement uses rest params (...args), + // but serverless Statement uses a single array param (args?). + // We collect rest params and forward as a single array. + async run(...args: any[]): Promise<{ changes: number; lastInsertRowid: number }> { const stmt = await this.getStmt(); return stmt.run(args); @@ -142,8 +159,15 @@ export function createServerlessAdapter(conn: Connection): DatabasePromise { return new LazyStatement(conn, sql); }, - transaction(fn: (...args: any[]) => any) { - return conn.transaction(fn); + // DatabasePromise.transaction() returns a wrapper function that + // executes fn inside a transaction when called. The serverless + // driver's transaction() has the same shape. + transaction(fn: (...args: any[]) => Promise) { + return (...bindParameters: any[]) => { + return conn.transaction(async () => { + return fn(...bindParameters); + }); + }; }, async exec(sql: string): Promise {