@tursodatabase/serverless 1.0.0 → 1.1.0-pre.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/compat.d.ts CHANGED
@@ -87,11 +87,10 @@ export declare class LibsqlError extends Error {
87
87
  constructor(message: string, code: string, extendedCode?: string, rawCode?: number, cause?: Error);
88
88
  }
89
89
  /**
90
- * Interactive transaction interface (not implemented in serverless mode).
90
+ * Interactive transaction interface.
91
91
  *
92
92
  * @remarks
93
- * Transactions are not supported in the serverless compatibility layer.
94
- * Calling transaction() will throw a LibsqlError.
93
+ * A transaction keeps a dedicated session open until commit/rollback/close.
95
94
  */
96
95
  export interface Transaction {
97
96
  execute(stmt: InStatement): Promise<ResultSet>;
package/dist/compat.js CHANGED
@@ -25,6 +25,7 @@ class LibSQLClient {
25
25
  authToken: config.authToken || '',
26
26
  remoteEncryptionKey: config.remoteEncryptionKey
27
27
  };
28
+ this.sessionConfig = sessionConfig;
28
29
  this.session = new Session(sessionConfig);
29
30
  }
30
31
  validateConfig(config) {
@@ -157,8 +158,124 @@ class LibSQLClient {
157
158
  // For now, just call batch - in a real implementation this would disable foreign keys
158
159
  return this.batch(stmts, "write");
159
160
  }
161
+ modeToBeginSql(mode) {
162
+ switch (mode) {
163
+ case "write":
164
+ return "BEGIN IMMEDIATE";
165
+ case "deferred":
166
+ return "BEGIN DEFERRED";
167
+ case "read":
168
+ default:
169
+ return "BEGIN";
170
+ }
171
+ }
160
172
  async transaction(mode) {
161
- throw new LibsqlError("Transactions not implemented", "NOT_IMPLEMENTED");
173
+ await this.execLock.acquire();
174
+ if (this._closed) {
175
+ this.execLock.release();
176
+ throw new LibsqlError("Client is closed", "CLIENT_CLOSED");
177
+ }
178
+ const txSession = new Session(this.sessionConfig);
179
+ let txClosed = false;
180
+ let cleanupStarted = false;
181
+ const ensureOpen = () => {
182
+ if (txClosed) {
183
+ throw new LibsqlError("Transaction is closed", "TRANSACTION_CLOSED");
184
+ }
185
+ };
186
+ const closeTx = async () => {
187
+ if (cleanupStarted)
188
+ return;
189
+ cleanupStarted = true;
190
+ txClosed = true;
191
+ try {
192
+ await txSession.close();
193
+ }
194
+ finally {
195
+ this.execLock.release();
196
+ }
197
+ };
198
+ const executeInTx = async (stmt) => {
199
+ ensureOpen();
200
+ const normalized = this.normalizeStatement(stmt);
201
+ try {
202
+ const result = await txSession.execute(normalized.sql, normalized.args, this._defaultSafeIntegers);
203
+ return this.convertResult(result);
204
+ }
205
+ catch (error) {
206
+ throw mapDatabaseError(error, "EXECUTE_ERROR");
207
+ }
208
+ };
209
+ try {
210
+ await txSession.sequence(this.modeToBeginSql(mode));
211
+ }
212
+ catch (error) {
213
+ await closeTx();
214
+ throw mapDatabaseError(error, "BEGIN_ERROR");
215
+ }
216
+ return {
217
+ execute: async (stmtOrSql, args) => {
218
+ if (typeof stmtOrSql === "string") {
219
+ const normalizedArgs = args ? (Array.isArray(args) ? args : Object.values(args)) : [];
220
+ return executeInTx({ sql: stmtOrSql, args: normalizedArgs });
221
+ }
222
+ return executeInTx(stmtOrSql);
223
+ },
224
+ batch: async (stmts) => {
225
+ ensureOpen();
226
+ const results = [];
227
+ for (const stmt of stmts) {
228
+ results.push(await executeInTx(stmt));
229
+ }
230
+ return results;
231
+ },
232
+ executeMultiple: async (sql) => {
233
+ ensureOpen();
234
+ try {
235
+ await txSession.sequence(sql);
236
+ }
237
+ catch (error) {
238
+ throw mapDatabaseError(error, "EXECUTE_MULTIPLE_ERROR");
239
+ }
240
+ },
241
+ commit: async () => {
242
+ ensureOpen();
243
+ try {
244
+ await txSession.sequence("COMMIT");
245
+ }
246
+ catch (error) {
247
+ throw mapDatabaseError(error, "COMMIT_ERROR");
248
+ }
249
+ finally {
250
+ await closeTx();
251
+ }
252
+ },
253
+ rollback: async () => {
254
+ ensureOpen();
255
+ try {
256
+ await txSession.sequence("ROLLBACK");
257
+ }
258
+ catch (error) {
259
+ throw mapDatabaseError(error, "ROLLBACK_ERROR");
260
+ }
261
+ finally {
262
+ await closeTx();
263
+ }
264
+ },
265
+ close: () => {
266
+ if (txClosed)
267
+ return;
268
+ txClosed = true;
269
+ void txSession.sequence("ROLLBACK")
270
+ .catch(() => undefined)
271
+ .finally(() => {
272
+ void closeTx();
273
+ });
274
+ },
275
+ get closed() {
276
+ return txClosed;
277
+ },
278
+ };
162
279
  }
163
280
  async executeMultiple(sql) {
164
281
  await this.execLock.acquire();
@@ -1,5 +1,6 @@
1
1
  import { type SessionConfig } from './session.js';
2
2
  import { Statement } from './statement.js';
3
+ import { type QueryOptions } from './protocol.js';
3
4
  /**
4
5
  * Configuration options for connecting to a Turso database.
5
6
  */
@@ -64,7 +65,7 @@ export declare class Connection {
64
65
  /**
65
66
  * Prepare a SQL statement for execution.
66
67
  *
67
- * Each prepared statement gets its own session to avoid conflicts during concurrent execution.
68
+ * Prepared statements created from a Connection use the same underlying session so transaction boundaries are preserved.
68
69
  * This method fetches column metadata using the describe functionality.
69
70
  *
70
71
  * @param sql - The SQL statement to prepare
@@ -91,7 +92,7 @@ export declare class Connection {
91
92
  * console.log(result.rows);
92
93
  * ```
93
94
  */
94
- execute(sql: string, args?: any[]): Promise<any>;
95
+ execute(sql: string, args?: any[], queryOptions?: QueryOptions): Promise<any>;
95
96
  /**
96
97
  * Execute a SQL statement and return all results.
97
98
  *
@@ -104,7 +105,7 @@ export declare class Connection {
104
105
  * console.log(result.rows);
105
106
  * ```
106
107
  */
107
- exec(sql: string): Promise<any>;
108
+ exec(sql: string, queryOptions?: QueryOptions): Promise<any>;
108
109
  /**
109
110
  * Execute multiple SQL statements in a batch.
110
111
  *
@@ -121,14 +122,14 @@ export declare class Connection {
121
122
  * ]);
122
123
  * ```
123
124
  */
124
- batch(statements: string[], mode?: string): Promise<any>;
125
+ batch(statements: string[], mode?: string, queryOptions?: QueryOptions): Promise<any>;
125
126
  /**
126
127
  * Execute a pragma.
127
128
  *
128
129
  * @param pragma - The pragma to execute
129
130
  * @returns Promise resolving to the result of the pragma
130
131
  */
131
- pragma(pragma: string): Promise<any>;
132
+ pragma(pragma: string, queryOptions?: QueryOptions): Promise<any>;
132
133
  /**
133
134
  * Sets the default safe integers mode for all statements from this connection.
134
135
  *
@@ -71,7 +71,7 @@ export class Connection {
71
71
  /**
72
72
  * Prepare a SQL statement for execution.
73
73
  *
74
- * Each prepared statement gets its own session to avoid conflicts during concurrent execution.
74
+ * Prepared statements created from a Connection use the same underlying session so transaction boundaries are preserved.
75
75
  * This method fetches column metadata using the describe functionality.
76
76
  *
77
77
  * @param sql - The SQL statement to prepare
@@ -92,7 +92,7 @@ export class Connection {
92
92
  const session = new Session(this.config);
93
93
  const description = await session.describe(sql);
94
94
  await session.close();
95
- const stmt = new Statement(this.config, sql, description.cols);
95
+ const stmt = Statement.fromSession(this.session, sql, description.cols, this.execLock);
96
96
  if (this.defaultSafeIntegerMode) {
97
97
  stmt.safeIntegers(true);
98
98
  }
@@ -111,13 +111,13 @@ export class Connection {
111
111
  * console.log(result.rows);
112
112
  * ```
113
113
  */
114
- async execute(sql, args) {
114
+ async execute(sql, args, queryOptions) {
115
115
  if (!this.isOpen) {
116
116
  throw new TypeError("The database connection is not open");
117
117
  }
118
118
  await this.execLock.acquire();
119
119
  try {
120
- return await this.session.execute(sql, args || [], this.defaultSafeIntegerMode);
120
+ return await this.session.execute(sql, args || [], this.defaultSafeIntegerMode, queryOptions);
121
121
  }
122
122
  finally {
123
123
  this.execLock.release();
@@ -135,13 +135,13 @@ export class Connection {
135
135
  * console.log(result.rows);
136
136
  * ```
137
137
  */
138
- async exec(sql) {
138
+ async exec(sql, queryOptions) {
139
139
  if (!this.isOpen) {
140
140
  throw new TypeError("The database connection is not open");
141
141
  }
142
142
  await this.execLock.acquire();
143
143
  try {
144
- return await this.session.sequence(sql);
144
+ return await this.session.sequence(sql, queryOptions);
145
145
  }
146
146
  finally {
147
147
  this.execLock.release();
@@ -163,13 +163,13 @@ export class Connection {
163
163
  * ]);
164
164
  * ```
165
165
  */
166
- async batch(statements, mode) {
166
+ async batch(statements, mode, queryOptions) {
167
167
  if (!this.isOpen) {
168
168
  throw new TypeError("The database connection is not open");
169
169
  }
170
170
  await this.execLock.acquire();
171
171
  try {
172
- return await this.session.batch(statements);
172
+ return await this.session.batch(statements, queryOptions);
173
173
  }
174
174
  finally {
175
175
  this.execLock.release();
@@ -181,14 +181,14 @@ export class Connection {
181
181
  * @param pragma - The pragma to execute
182
182
  * @returns Promise resolving to the result of the pragma
183
183
  */
184
- async pragma(pragma) {
184
+ async pragma(pragma, queryOptions) {
185
185
  if (!this.isOpen) {
186
186
  throw new TypeError("The database connection is not open");
187
187
  }
188
188
  await this.execLock.acquire();
189
189
  try {
190
190
  const sql = `PRAGMA ${pragma}`;
191
- return await this.session.execute(sql);
191
+ return await this.session.execute(sql, [], false, queryOptions);
192
192
  }
193
193
  finally {
194
194
  this.execLock.release();
package/dist/error.d.ts CHANGED
@@ -7,3 +7,13 @@ export declare class DatabaseError extends Error {
7
7
  cause?: Error;
8
8
  constructor(message: string, code?: string, rawCode?: number, cause?: Error);
9
9
  }
10
+ /**
11
+ * Error thrown when a query exceeds the configured timeout.
12
+ *
13
+ * This is a subclass of `DatabaseError` with `code` set to `"TIMEOUT"`.
14
+ * Catch this type to distinguish timeouts from other database errors
15
+ * and decide whether to retry or fail gracefully.
16
+ */
17
+ export declare class TimeoutError extends DatabaseError {
18
+ constructor(message?: string, cause?: Error);
19
+ }
package/dist/error.js CHANGED
@@ -8,3 +8,17 @@ export class DatabaseError extends Error {
8
8
  Object.setPrototypeOf(this, DatabaseError.prototype);
9
9
  }
10
10
  }
11
+ /**
12
+ * Error thrown when a query exceeds the configured timeout.
13
+ *
14
+ * This is a subclass of `DatabaseError` with `code` set to `"TIMEOUT"`.
15
+ * Catch this type to distinguish timeouts from other database errors
16
+ * and decide whether to retry or fail gracefully.
17
+ */
18
+ export class TimeoutError extends DatabaseError {
19
+ constructor(message = 'Query timed out', cause) {
20
+ super(message, 'TIMEOUT', undefined, cause);
21
+ this.name = 'TimeoutError';
22
+ Object.setPrototypeOf(this, TimeoutError.prototype);
23
+ }
24
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Connection, connect, type Config } from './connection.js';
2
2
  export { Statement } from './statement.js';
3
3
  export { Session, type SessionConfig } from './session.js';
4
- export { DatabaseError } from './error.js';
5
- export { type Column, ENCRYPTION_KEY_HEADER } from './protocol.js';
4
+ export { DatabaseError, TimeoutError } from './error.js';
5
+ export { type Column, type QueryOptions, ENCRYPTION_KEY_HEADER } from './protocol.js';
package/dist/index.js CHANGED
@@ -2,5 +2,5 @@
2
2
  export { Connection, connect } from './connection.js';
3
3
  export { Statement } from './statement.js';
4
4
  export { Session } from './session.js';
5
- export { DatabaseError } from './error.js';
5
+ export { DatabaseError, TimeoutError } from './error.js';
6
6
  export { ENCRYPTION_KEY_HEADER } from './protocol.js';
@@ -108,8 +108,13 @@ export interface CursorEntry {
108
108
  }
109
109
  /** HTTP header key for the encryption key */
110
110
  export declare const ENCRYPTION_KEY_HEADER = "x-turso-encryption-key";
111
- export declare function executeCursor(url: string, authToken: string | undefined, request: CursorRequest, remoteEncryptionKey?: string): Promise<{
111
+ /** Per-query timeout options. Overrides defaultQueryTimeout for this call. */
112
+ export interface QueryOptions {
113
+ /** Per-query timeout in milliseconds. Overrides defaultQueryTimeout for this call. */
114
+ queryTimeout?: number;
115
+ }
116
+ export declare function executeCursor(url: string, authToken: string | undefined, request: CursorRequest, remoteEncryptionKey?: string, signal?: AbortSignal): Promise<{
112
117
  response: CursorResponse;
113
118
  entries: AsyncGenerator<CursorEntry>;
114
119
  }>;
115
- export declare function executePipeline(url: string, authToken: string | undefined, request: PipelineRequest, remoteEncryptionKey?: string): Promise<PipelineResponse>;
120
+ export declare function executePipeline(url: string, authToken: string | undefined, request: PipelineRequest, remoteEncryptionKey?: string, signal?: AbortSignal): Promise<PipelineResponse>;
package/dist/protocol.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DatabaseError } from './error.js';
1
+ import { DatabaseError, TimeoutError } from './error.js';
2
2
  export function encodeValue(value) {
3
3
  if (value === null || value === undefined) {
4
4
  return { type: 'null' };
@@ -53,7 +53,13 @@ export function decodeValue(value, safeIntegers = false) {
53
53
  }
54
54
  /** HTTP header key for the encryption key */
55
55
  export const ENCRYPTION_KEY_HEADER = 'x-turso-encryption-key';
56
- export async function executeCursor(url, authToken, request, remoteEncryptionKey) {
56
+ function wrapAbortError(error) {
57
+ if (error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')) {
58
+ throw new TimeoutError('Query timed out');
59
+ }
60
+ throw error;
61
+ }
62
+ export async function executeCursor(url, authToken, request, remoteEncryptionKey, signal) {
57
63
  const headers = {
58
64
  'Content-Type': 'application/json',
59
65
  };
@@ -63,11 +69,18 @@ export async function executeCursor(url, authToken, request, remoteEncryptionKey
63
69
  if (remoteEncryptionKey) {
64
70
  headers[ENCRYPTION_KEY_HEADER] = remoteEncryptionKey;
65
71
  }
66
- const response = await fetch(`${url}/v3/cursor`, {
67
- method: 'POST',
68
- headers,
69
- body: JSON.stringify(request),
70
- });
72
+ let response;
73
+ try {
74
+ response = await fetch(`${url}/v3/cursor`, {
75
+ method: 'POST',
76
+ headers,
77
+ body: JSON.stringify(request),
78
+ signal,
79
+ });
80
+ }
81
+ catch (error) {
82
+ wrapAbortError(error);
83
+ }
71
84
  if (!response.ok) {
72
85
  let errorMessage = `HTTP error! status: ${response.status}`;
73
86
  try {
@@ -90,22 +103,29 @@ export async function executeCursor(url, authToken, request, remoteEncryptionKey
90
103
  let buffer = '';
91
104
  let cursorResponse;
92
105
  // First, read until we get the cursor response (first line)
93
- while (!cursorResponse) {
94
- const { done, value } = await reader.read();
95
- if (done)
96
- break;
97
- buffer += decoder.decode(value, { stream: true });
98
- const newlineIndex = buffer.indexOf('\n');
99
- if (newlineIndex !== -1) {
100
- const line = buffer.slice(0, newlineIndex).trim();
101
- buffer = buffer.slice(newlineIndex + 1);
102
- if (line) {
103
- cursorResponse = JSON.parse(line);
106
+ try {
107
+ while (!cursorResponse) {
108
+ const { done, value } = await reader.read();
109
+ if (done)
104
110
  break;
111
+ buffer += decoder.decode(value, { stream: true });
112
+ const newlineIndex = buffer.indexOf('\n');
113
+ if (newlineIndex !== -1) {
114
+ const line = buffer.slice(0, newlineIndex).trim();
115
+ buffer = buffer.slice(newlineIndex + 1);
116
+ if (line) {
117
+ cursorResponse = JSON.parse(line);
118
+ break;
119
+ }
105
120
  }
106
121
  }
107
122
  }
123
+ catch (error) {
124
+ reader.releaseLock();
125
+ wrapAbortError(error);
126
+ }
108
127
  if (!cursorResponse) {
128
+ reader.releaseLock();
109
129
  throw new DatabaseError('No cursor response received');
110
130
  }
111
131
  async function* parseEntries() {
@@ -121,10 +141,16 @@ export async function executeCursor(url, authToken, request, remoteEncryptionKey
121
141
  }
122
142
  // Continue reading from the stream
123
143
  while (true) {
124
- const { done, value } = await reader.read();
125
- if (done)
144
+ let readResult;
145
+ try {
146
+ readResult = await reader.read();
147
+ }
148
+ catch (error) {
149
+ wrapAbortError(error);
150
+ }
151
+ if (readResult.done)
126
152
  break;
127
- buffer += decoder.decode(value, { stream: true });
153
+ buffer += decoder.decode(readResult.value, { stream: true });
128
154
  while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
129
155
  const line = buffer.slice(0, newlineIndex).trim();
130
156
  buffer = buffer.slice(newlineIndex + 1);
@@ -144,7 +170,7 @@ export async function executeCursor(url, authToken, request, remoteEncryptionKey
144
170
  }
145
171
  return { response: cursorResponse, entries: parseEntries() };
146
172
  }
147
- export async function executePipeline(url, authToken, request, remoteEncryptionKey) {
173
+ export async function executePipeline(url, authToken, request, remoteEncryptionKey, signal) {
148
174
  const headers = {
149
175
  'Content-Type': 'application/json',
150
176
  };
@@ -154,11 +180,18 @@ export async function executePipeline(url, authToken, request, remoteEncryptionK
154
180
  if (remoteEncryptionKey) {
155
181
  headers[ENCRYPTION_KEY_HEADER] = remoteEncryptionKey;
156
182
  }
157
- const response = await fetch(`${url}/v3/pipeline`, {
158
- method: 'POST',
159
- headers,
160
- body: JSON.stringify(request),
161
- });
183
+ let response;
184
+ try {
185
+ response = await fetch(`${url}/v3/pipeline`, {
186
+ method: 'POST',
187
+ headers,
188
+ body: JSON.stringify(request),
189
+ signal,
190
+ });
191
+ }
192
+ catch (error) {
193
+ wrapAbortError(error);
194
+ }
162
195
  if (!response.ok) {
163
196
  throw new DatabaseError(`HTTP error! status: ${response.status}`);
164
197
  }
package/dist/session.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type CursorResponse, type CursorEntry, type DescribeResult } from './protocol.js';
1
+ import { type CursorResponse, type CursorEntry, type DescribeResult, type QueryOptions } from './protocol.js';
2
2
  /**
3
3
  * Configuration options for a session.
4
4
  */
@@ -12,6 +12,8 @@ export interface SessionConfig {
12
12
  * to enable access to encrypted Turso Cloud databases.
13
13
  */
14
14
  remoteEncryptionKey?: string;
15
+ /** Default maximum query execution time in milliseconds before interruption. */
16
+ defaultQueryTimeout?: number;
15
17
  }
16
18
  /**
17
19
  * A database session that manages the connection state and baton.
@@ -24,13 +26,14 @@ export declare class Session {
24
26
  private baton;
25
27
  private baseUrl;
26
28
  constructor(config: SessionConfig);
29
+ private createAbortSignal;
27
30
  /**
28
31
  * Describe a SQL statement to get its column metadata.
29
32
  *
30
33
  * @param sql - The SQL statement to describe
31
34
  * @returns Promise resolving to the statement description
32
35
  */
33
- describe(sql: string): Promise<DescribeResult>;
36
+ describe(sql: string, queryOptions?: QueryOptions): Promise<DescribeResult>;
34
37
  /**
35
38
  * Execute a SQL statement and return all results.
36
39
  *
@@ -39,7 +42,7 @@ export declare class Session {
39
42
  * @param safeIntegers - Whether to return integers as BigInt
40
43
  * @returns Promise resolving to the complete result set
41
44
  */
42
- execute(sql: string, args?: any[] | Record<string, any>, safeIntegers?: boolean): Promise<any>;
45
+ execute(sql: string, args?: any[] | Record<string, any>, safeIntegers?: boolean, queryOptions?: QueryOptions): Promise<any>;
43
46
  /**
44
47
  * Execute a SQL statement and return the raw response and entries.
45
48
  *
@@ -47,7 +50,7 @@ export declare class Session {
47
50
  * @param args - Optional array of parameter values or object with named parameters
48
51
  * @returns Promise resolving to the raw response and cursor entries
49
52
  */
50
- executeRaw(sql: string, args?: any[] | Record<string, any>): Promise<{
53
+ executeRaw(sql: string, args?: any[] | Record<string, any>, queryOptions?: QueryOptions): Promise<{
51
54
  response: CursorResponse;
52
55
  entries: AsyncGenerator<CursorEntry>;
53
56
  }>;
@@ -72,14 +75,14 @@ export declare class Session {
72
75
  * @param statements - Array of SQL statements to execute
73
76
  * @returns Promise resolving to batch execution results
74
77
  */
75
- batch(statements: string[]): Promise<any>;
78
+ batch(statements: string[], queryOptions?: QueryOptions): Promise<any>;
76
79
  /**
77
80
  * Execute a sequence of SQL statements separated by semicolons.
78
81
  *
79
82
  * @param sql - SQL string containing multiple statements separated by semicolons
80
83
  * @returns Promise resolving when all statements are executed
81
84
  */
82
- sequence(sql: string): Promise<void>;
85
+ sequence(sql: string, queryOptions?: QueryOptions): Promise<void>;
83
86
  /**
84
87
  * Close the session.
85
88
  *
package/dist/session.js CHANGED
@@ -18,13 +18,20 @@ export class Session {
18
18
  this.config = config;
19
19
  this.baseUrl = normalizeUrl(config.url);
20
20
  }
21
+ createAbortSignal(queryOptions) {
22
+ const timeout = queryOptions?.queryTimeout ?? this.config.defaultQueryTimeout;
23
+ if (timeout != null && timeout > 0) {
24
+ return AbortSignal.timeout(timeout);
25
+ }
26
+ return undefined;
27
+ }
21
28
  /**
22
29
  * Describe a SQL statement to get its column metadata.
23
30
  *
24
31
  * @param sql - The SQL statement to describe
25
32
  * @returns Promise resolving to the statement description
26
33
  */
27
- async describe(sql) {
34
+ async describe(sql, queryOptions) {
28
35
  const request = {
29
36
  baton: this.baton,
30
37
  requests: [{
@@ -32,7 +39,7 @@ export class Session {
32
39
  sql: sql
33
40
  }]
34
41
  };
35
- const response = await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
42
+ const response = await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey, this.createAbortSignal(queryOptions));
36
43
  this.baton = response.baton;
37
44
  if (response.base_url) {
38
45
  this.baseUrl = response.base_url;
@@ -57,8 +64,8 @@ export class Session {
57
64
  * @param safeIntegers - Whether to return integers as BigInt
58
65
  * @returns Promise resolving to the complete result set
59
66
  */
60
- async execute(sql, args = [], safeIntegers = false) {
61
- const { response, entries } = await this.executeRaw(sql, args);
67
+ async execute(sql, args = [], safeIntegers = false, queryOptions) {
68
+ const { response, entries } = await this.executeRaw(sql, args, queryOptions);
62
69
  const result = await this.processCursorEntries(entries, safeIntegers);
63
70
  return result;
64
71
  }
@@ -69,7 +76,7 @@ export class Session {
69
76
  * @param args - Optional array of parameter values or object with named parameters
70
77
  * @returns Promise resolving to the raw response and cursor entries
71
78
  */
72
- async executeRaw(sql, args = []) {
79
+ async executeRaw(sql, args = [], queryOptions) {
73
80
  let positionalArgs = [];
74
81
  let namedArgs = [];
75
82
  if (Array.isArray(args)) {
@@ -118,7 +125,7 @@ export class Session {
118
125
  }]
119
126
  }
120
127
  };
121
- const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
128
+ const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey, this.createAbortSignal(queryOptions));
122
129
  this.baton = response.baton;
123
130
  if (response.base_url) {
124
131
  this.baseUrl = response.base_url;
@@ -202,7 +209,7 @@ export class Session {
202
209
  * @param statements - Array of SQL statements to execute
203
210
  * @returns Promise resolving to batch execution results
204
211
  */
205
- async batch(statements) {
212
+ async batch(statements, queryOptions) {
206
213
  const request = {
207
214
  baton: this.baton,
208
215
  batch: {
@@ -216,7 +223,7 @@ export class Session {
216
223
  }))
217
224
  }
218
225
  };
219
- const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
226
+ const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey, this.createAbortSignal(queryOptions));
220
227
  this.baton = response.baton;
221
228
  if (response.base_url) {
222
229
  this.baseUrl = response.base_url;
@@ -249,7 +256,7 @@ export class Session {
249
256
  * @param sql - SQL string containing multiple statements separated by semicolons
250
257
  * @returns Promise resolving when all statements are executed
251
258
  */
252
- async sequence(sql) {
259
+ async sequence(sql, queryOptions) {
253
260
  const request = {
254
261
  baton: this.baton,
255
262
  requests: [{
@@ -257,7 +264,7 @@ export class Session {
257
264
  sql: sql
258
265
  }]
259
266
  };
260
- const response = await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
267
+ const response = await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey, this.createAbortSignal(queryOptions));
261
268
  this.baton = response.baton;
262
269
  if (response.base_url) {
263
270
  this.baseUrl = response.base_url;
@@ -288,9 +295,9 @@ export class Session {
288
295
  };
289
296
  await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
290
297
  }
291
- catch (error) {
292
- // Ignore errors during close, as the connection might already be closed
293
- console.error('Error closing session:', error);
298
+ catch {
299
+ // Ignore errors during close the connection might already be closed
300
+ // or the baton may be stale after a timeout.
294
301
  }
295
302
  }
296
303
  // Reset local state
@@ -1,9 +1,10 @@
1
- import { type Column } from './protocol.js';
2
- import { type SessionConfig } from './session.js';
1
+ import { type Column, type QueryOptions } from './protocol.js';
2
+ import { Session, type SessionConfig } from './session.js';
3
+ import { type AsyncLock } from './async-lock.js';
3
4
  /**
4
5
  * A prepared SQL statement that can be executed in multiple ways.
5
6
  *
6
- * Each statement has its own session to avoid conflicts during concurrent execution.
7
+ * Statements may either own a dedicated session or share a connection session to preserve transaction boundaries.
7
8
  * Provides three execution modes:
8
9
  * - `get(args?)`: Returns the first row or null
9
10
  * - `all(args?)`: Returns all rows as an array
@@ -15,7 +16,14 @@ export declare class Statement {
15
16
  private presentationMode;
16
17
  private safeIntegerMode;
17
18
  private columnMetadata;
19
+ private execLock?;
18
20
  constructor(sessionConfig: SessionConfig, sql: string, columns?: Column[]);
21
+ /**
22
+ * Create a Statement that shares an existing session and serializes execution
23
+ * through the given lock. Used by Connection.prepare() so prepared statements
24
+ * participate in the connection's transaction scope.
25
+ */
26
+ static fromSession(session: Session, sql: string, columns: Column[] | undefined, execLock: AsyncLock): Statement;
19
27
  /**
20
28
  * Whether the prepared statement returns data.
21
29
  *
@@ -81,6 +89,7 @@ export declare class Statement {
81
89
  * ```
82
90
  */
83
91
  columns(): any[];
92
+ private withLock;
84
93
  /**
85
94
  * Executes the prepared statement.
86
95
  *
@@ -94,7 +103,7 @@ export declare class Statement {
94
103
  * console.log(`Inserted user with ID ${result.lastInsertRowid}`);
95
104
  * ```
96
105
  */
97
- run(args?: any): Promise<any>;
106
+ run(args?: any, queryOptions?: QueryOptions): Promise<any>;
98
107
  /**
99
108
  * Execute the statement and return the first row.
100
109
  *
@@ -110,7 +119,7 @@ export declare class Statement {
110
119
  * }
111
120
  * ```
112
121
  */
113
- get(args?: any): Promise<any>;
122
+ get(args?: any, queryOptions?: QueryOptions): Promise<any>;
114
123
  /**
115
124
  * Execute the statement and return all rows.
116
125
  *
@@ -124,7 +133,7 @@ export declare class Statement {
124
133
  * console.log(`Found ${activeUsers.length} active users`);
125
134
  * ```
126
135
  */
127
- all(args?: any): Promise<any[]>;
136
+ all(args?: any, queryOptions?: QueryOptions): Promise<any[]>;
128
137
  /**
129
138
  * Execute the statement and return an async iterator for streaming results.
130
139
  *
@@ -143,7 +152,7 @@ export declare class Statement {
143
152
  * }
144
153
  * ```
145
154
  */
146
- iterate(args?: any): AsyncGenerator<any>;
155
+ iterate(args?: any, queryOptions?: QueryOptions): AsyncGenerator<any>;
147
156
  /**
148
157
  * Normalize arguments to handle both single values and arrays.
149
158
  * Matches the behavior of the native bindings.
package/dist/statement.js CHANGED
@@ -4,7 +4,7 @@ import { DatabaseError } from './error.js';
4
4
  /**
5
5
  * A prepared SQL statement that can be executed in multiple ways.
6
6
  *
7
- * Each statement has its own session to avoid conflicts during concurrent execution.
7
+ * Statements may either own a dedicated session or share a connection session to preserve transaction boundaries.
8
8
  * Provides three execution modes:
9
9
  * - `get(args?)`: Returns the first row or null
10
10
  * - `all(args?)`: Returns all rows as an array
@@ -18,6 +18,21 @@ export class Statement {
18
18
  this.sql = sql;
19
19
  this.columnMetadata = columns || [];
20
20
  }
21
+ /**
22
+ * Create a Statement that shares an existing session and serializes execution
23
+ * through the given lock. Used by Connection.prepare() so prepared statements
24
+ * participate in the connection's transaction scope.
25
+ */
26
+ static fromSession(session, sql, columns, execLock) {
27
+ const stmt = Object.create(Statement.prototype);
28
+ stmt.session = session;
29
+ stmt.sql = sql;
30
+ stmt.columnMetadata = columns || [];
31
+ stmt.presentationMode = 'expanded';
32
+ stmt.safeIntegerMode = false;
33
+ stmt.execLock = execLock;
34
+ return stmt;
35
+ }
21
36
  /**
22
37
  * Whether the prepared statement returns data.
23
38
  *
@@ -99,6 +114,18 @@ export class Statement {
99
114
  type: col.decltype
100
115
  }));
101
116
  }
117
+ async withLock(fn) {
118
+ if (!this.execLock) {
119
+ return await fn();
120
+ }
121
+ await this.execLock.acquire();
122
+ try {
123
+ return await fn();
124
+ }
125
+ finally {
126
+ this.execLock.release();
127
+ }
128
+ }
102
129
  /**
103
130
  * Executes the prepared statement.
104
131
  *
@@ -112,10 +139,12 @@ export class Statement {
112
139
  * console.log(`Inserted user with ID ${result.lastInsertRowid}`);
113
140
  * ```
114
141
  */
115
- async run(args) {
116
- const normalizedArgs = this.normalizeArgs(args);
117
- const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode);
118
- return { changes: result.rowsAffected, lastInsertRowid: result.lastInsertRowid };
142
+ async run(args, queryOptions) {
143
+ return await this.withLock(async () => {
144
+ const normalizedArgs = this.normalizeArgs(args);
145
+ const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode, queryOptions);
146
+ return { changes: result.rowsAffected, lastInsertRowid: result.lastInsertRowid };
147
+ });
119
148
  }
120
149
  /**
121
150
  * Execute the statement and return the first row.
@@ -132,28 +161,30 @@ export class Statement {
132
161
  * }
133
162
  * ```
134
163
  */
135
- async get(args) {
136
- const normalizedArgs = this.normalizeArgs(args);
137
- const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode);
138
- const row = result.rows[0];
139
- if (!row) {
140
- return undefined;
141
- }
142
- if (this.presentationMode === 'pluck') {
143
- // In pluck mode, return only the first column value
144
- return row[0];
145
- }
146
- if (this.presentationMode === 'raw') {
147
- // In raw mode, return the row as a plain array (it already is one)
148
- // The row object is already an array with column properties added
149
- return [...row];
150
- }
151
- // In expanded mode, convert to plain object with named properties
152
- const obj = {};
153
- result.columns.forEach((col, i) => {
154
- obj[col] = row[i];
164
+ async get(args, queryOptions) {
165
+ return await this.withLock(async () => {
166
+ const normalizedArgs = this.normalizeArgs(args);
167
+ const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode, queryOptions);
168
+ const row = result.rows[0];
169
+ if (!row) {
170
+ return undefined;
171
+ }
172
+ if (this.presentationMode === 'pluck') {
173
+ // In pluck mode, return only the first column value
174
+ return row[0];
175
+ }
176
+ if (this.presentationMode === 'raw') {
177
+ // In raw mode, return the row as a plain array (it already is one)
178
+ // The row object is already an array with column properties added
179
+ return [...row];
180
+ }
181
+ // In expanded mode, convert to plain object with named properties
182
+ const obj = {};
183
+ result.columns.forEach((col, i) => {
184
+ obj[col] = row[i];
185
+ });
186
+ return obj;
155
187
  });
156
- return obj;
157
188
  }
158
189
  /**
159
190
  * Execute the statement and return all rows.
@@ -168,23 +199,25 @@ export class Statement {
168
199
  * console.log(`Found ${activeUsers.length} active users`);
169
200
  * ```
170
201
  */
171
- async all(args) {
172
- const normalizedArgs = this.normalizeArgs(args);
173
- const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode);
174
- if (this.presentationMode === 'pluck') {
175
- // In pluck mode, return only the first column value from each row
176
- return result.rows.map((row) => row[0]);
177
- }
178
- if (this.presentationMode === 'raw') {
179
- return result.rows.map((row) => [...row]);
180
- }
181
- // In expanded mode, convert rows to plain objects with named properties
182
- return result.rows.map((row) => {
183
- const obj = {};
184
- result.columns.forEach((col, i) => {
185
- obj[col] = row[i];
202
+ async all(args, queryOptions) {
203
+ return await this.withLock(async () => {
204
+ const normalizedArgs = this.normalizeArgs(args);
205
+ const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode, queryOptions);
206
+ if (this.presentationMode === 'pluck') {
207
+ // In pluck mode, return only the first column value from each row
208
+ return result.rows.map((row) => row[0]);
209
+ }
210
+ if (this.presentationMode === 'raw') {
211
+ return result.rows.map((row) => [...row]);
212
+ }
213
+ // In expanded mode, convert rows to plain objects with named properties
214
+ return result.rows.map((row) => {
215
+ const obj = {};
216
+ result.columns.forEach((col, i) => {
217
+ obj[col] = row[i];
218
+ });
219
+ return obj;
186
220
  });
187
- return obj;
188
221
  });
189
222
  }
190
223
  /**
@@ -205,9 +238,18 @@ export class Statement {
205
238
  * }
206
239
  * ```
207
240
  */
208
- async *iterate(args) {
241
+ async *iterate(args, queryOptions) {
242
+ // Shared-connection statements must not hold the connection lock across
243
+ // `yield` points, or nested queries in the loop body can deadlock.
244
+ if (this.execLock) {
245
+ const rows = await this.all(args, queryOptions);
246
+ for (const row of rows) {
247
+ yield row;
248
+ }
249
+ return;
250
+ }
209
251
  const normalizedArgs = this.normalizeArgs(args);
210
- const { response, entries } = await this.session.executeRaw(this.sql, normalizedArgs);
252
+ const { entries } = await this.session.executeRaw(this.sql, normalizedArgs, queryOptions);
211
253
  let columns = [];
212
254
  for await (const entry of entries) {
213
255
  switch (entry.type) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tursodatabase/serverless",
3
- "version": "1.0.0",
3
+ "version": "1.1.0-pre.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",