@tursodatabase/serverless 1.1.0-pre.1 → 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();
@@ -65,7 +65,7 @@ export declare class Connection {
65
65
  /**
66
66
  * Prepare a SQL statement for execution.
67
67
  *
68
- * 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.
69
69
  * This method fetches column metadata using the describe functionality.
70
70
  *
71
71
  * @param sql - The SQL statement to prepare
@@ -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
  }
@@ -1,9 +1,10 @@
1
1
  import { type Column, type QueryOptions } from './protocol.js';
2
- import { type SessionConfig } from './session.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
  *
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
  *
@@ -113,9 +140,11 @@ export class Statement {
113
140
  * ```
114
141
  */
115
142
  async run(args, queryOptions) {
116
- const normalizedArgs = this.normalizeArgs(args);
117
- const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode, queryOptions);
118
- return { changes: result.rowsAffected, lastInsertRowid: result.lastInsertRowid };
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.
@@ -133,27 +162,29 @@ export class Statement {
133
162
  * ```
134
163
  */
135
164
  async get(args, queryOptions) {
136
- const normalizedArgs = this.normalizeArgs(args);
137
- const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode, queryOptions);
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];
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.
@@ -169,22 +200,24 @@ export class Statement {
169
200
  * ```
170
201
  */
171
202
  async all(args, queryOptions) {
172
- const normalizedArgs = this.normalizeArgs(args);
173
- const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode, queryOptions);
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];
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
  /**
@@ -206,8 +239,17 @@ export class Statement {
206
239
  * ```
207
240
  */
208
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, queryOptions);
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.1.0-pre.1",
3
+ "version": "1.1.0-pre.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",