@tursodatabase/serverless 1.2.0-pre.2 → 1.2.0

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.
@@ -0,0 +1,307 @@
1
+ import { executeCursor, executePipeline, encodeValue, decodeValue } from './protocol.js';
2
+ import { DatabaseError } from './error.js';
3
+ function normalizeUrl(url) {
4
+ return url.replace(/^libsql:\/\//, 'https://');
5
+ }
6
+ function isValidIdentifier(str) {
7
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
8
+ }
9
+ /**
10
+ * A database session that manages the connection state and baton.
11
+ *
12
+ * Each session maintains its own connection state and can execute SQL statements
13
+ * independently without interfering with other sessions.
14
+ */
15
+ export class Session {
16
+ constructor(config) {
17
+ this.baton = null;
18
+ this.config = config;
19
+ this.baseUrl = normalizeUrl(config.url);
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
+ }
28
+ /**
29
+ * Describe a SQL statement to get its column metadata.
30
+ *
31
+ * @param sql - The SQL statement to describe
32
+ * @returns Promise resolving to the statement description
33
+ */
34
+ async describe(sql, queryOptions) {
35
+ const request = {
36
+ baton: this.baton,
37
+ requests: [{
38
+ type: "describe",
39
+ sql: sql
40
+ }]
41
+ };
42
+ const response = await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey, this.createAbortSignal(queryOptions));
43
+ this.baton = response.baton;
44
+ if (response.base_url) {
45
+ this.baseUrl = response.base_url;
46
+ }
47
+ // Check for errors in the response
48
+ if (response.results && response.results[0]) {
49
+ const result = response.results[0];
50
+ if (result.type === "error") {
51
+ throw new DatabaseError(result.error?.message || 'Describe execution failed', result.error?.code);
52
+ }
53
+ if (result.response?.type === "describe" && result.response.result) {
54
+ return result.response.result;
55
+ }
56
+ }
57
+ throw new DatabaseError('Unexpected describe response');
58
+ }
59
+ /**
60
+ * Execute a SQL statement and return all results.
61
+ *
62
+ * @param sql - The SQL statement to execute
63
+ * @param args - Optional array of parameter values or object with named parameters
64
+ * @param safeIntegers - Whether to return integers as BigInt
65
+ * @returns Promise resolving to the complete result set
66
+ */
67
+ async execute(sql, args = [], safeIntegers = false, queryOptions) {
68
+ const { response, entries } = await this.executeRaw(sql, args, queryOptions);
69
+ const result = await this.processCursorEntries(entries, safeIntegers);
70
+ return result;
71
+ }
72
+ /**
73
+ * Execute a SQL statement and return the raw response and entries.
74
+ *
75
+ * @param sql - The SQL statement to execute
76
+ * @param args - Optional array of parameter values or object with named parameters
77
+ * @returns Promise resolving to the raw response and cursor entries
78
+ */
79
+ async executeRaw(sql, args = [], queryOptions) {
80
+ let positionalArgs = [];
81
+ let namedArgs = [];
82
+ if (Array.isArray(args)) {
83
+ positionalArgs = args.map(encodeValue);
84
+ }
85
+ else {
86
+ // Check if this is an object with numeric keys (for ?1, ?2 style parameters)
87
+ const keys = Object.keys(args);
88
+ const isNumericKeys = keys.length > 0 && keys.every(key => /^\d+$/.test(key));
89
+ if (isNumericKeys) {
90
+ // Convert numeric-keyed object to positional args
91
+ // Sort keys numerically to ensure correct order
92
+ const sortedKeys = keys.sort((a, b) => parseInt(a) - parseInt(b));
93
+ const maxIndex = parseInt(sortedKeys[sortedKeys.length - 1]);
94
+ // Create array with undefined for missing indices
95
+ positionalArgs = new Array(maxIndex);
96
+ for (const key of sortedKeys) {
97
+ const index = parseInt(key) - 1; // Convert to 0-based index
98
+ positionalArgs[index] = encodeValue(args[key]);
99
+ }
100
+ // Fill any undefined values with null
101
+ for (let i = 0; i < positionalArgs.length; i++) {
102
+ if (positionalArgs[i] === undefined) {
103
+ positionalArgs[i] = { type: 'null' };
104
+ }
105
+ }
106
+ }
107
+ else {
108
+ // Convert object with named parameters to NamedArg array
109
+ namedArgs = Object.entries(args).map(([name, value]) => ({
110
+ name,
111
+ value: encodeValue(value)
112
+ }));
113
+ }
114
+ }
115
+ const request = {
116
+ baton: this.baton,
117
+ batch: {
118
+ steps: [{
119
+ stmt: {
120
+ sql,
121
+ args: positionalArgs,
122
+ named_args: namedArgs,
123
+ want_rows: true
124
+ }
125
+ }]
126
+ }
127
+ };
128
+ const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey, this.createAbortSignal(queryOptions));
129
+ this.baton = response.baton;
130
+ if (response.base_url) {
131
+ this.baseUrl = response.base_url;
132
+ }
133
+ return { response, entries };
134
+ }
135
+ /**
136
+ * Process cursor entries into a structured result.
137
+ *
138
+ * @param entries - Async generator of cursor entries
139
+ * @returns Promise resolving to the processed result
140
+ */
141
+ async processCursorEntries(entries, safeIntegers = false) {
142
+ let columns = [];
143
+ let columnTypes = [];
144
+ let rows = [];
145
+ let rowsAffected = 0;
146
+ let lastInsertRowid;
147
+ for await (const entry of entries) {
148
+ switch (entry.type) {
149
+ case 'step_begin':
150
+ if (entry.cols) {
151
+ columns = entry.cols.map(col => col.name);
152
+ columnTypes = entry.cols.map(col => col.decltype || '');
153
+ }
154
+ break;
155
+ case 'row':
156
+ if (entry.row) {
157
+ const decodedRow = entry.row.map(value => decodeValue(value, safeIntegers));
158
+ const rowObject = this.createRowObject(decodedRow, columns);
159
+ rows.push(rowObject);
160
+ }
161
+ break;
162
+ case 'step_end':
163
+ if (entry.affected_row_count !== undefined) {
164
+ rowsAffected = entry.affected_row_count;
165
+ }
166
+ if (entry.last_insert_rowid) {
167
+ lastInsertRowid = parseInt(entry.last_insert_rowid, 10);
168
+ }
169
+ break;
170
+ case 'step_error':
171
+ case 'error':
172
+ throw new DatabaseError(entry.error?.message || 'SQL execution failed', entry.error?.code);
173
+ }
174
+ }
175
+ return {
176
+ columns,
177
+ columnTypes,
178
+ rows,
179
+ rowsAffected,
180
+ lastInsertRowid
181
+ };
182
+ }
183
+ /**
184
+ * Create a row object with both array and named property access.
185
+ *
186
+ * @param values - Array of column values
187
+ * @param columns - Array of column names
188
+ * @returns Row object with dual access patterns
189
+ */
190
+ createRowObject(values, columns) {
191
+ const row = [...values];
192
+ // Add column name properties to the array as non-enumerable
193
+ // Only add valid identifier names to avoid conflicts
194
+ columns.forEach((column, index) => {
195
+ if (column && isValidIdentifier(column)) {
196
+ Object.defineProperty(row, column, {
197
+ value: values[index],
198
+ enumerable: false,
199
+ writable: false,
200
+ configurable: true
201
+ });
202
+ }
203
+ });
204
+ return row;
205
+ }
206
+ /**
207
+ * Execute multiple SQL statements in a batch.
208
+ *
209
+ * @param statements - Array of SQL statements to execute
210
+ * @returns Promise resolving to batch execution results
211
+ */
212
+ async batch(statements, queryOptions) {
213
+ const request = {
214
+ baton: this.baton,
215
+ batch: {
216
+ steps: statements.map(sql => ({
217
+ stmt: {
218
+ sql,
219
+ args: [],
220
+ named_args: [],
221
+ want_rows: false
222
+ }
223
+ }))
224
+ }
225
+ };
226
+ const { response, entries } = await executeCursor(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey, this.createAbortSignal(queryOptions));
227
+ this.baton = response.baton;
228
+ if (response.base_url) {
229
+ this.baseUrl = response.base_url;
230
+ }
231
+ let totalRowsAffected = 0;
232
+ let lastInsertRowid;
233
+ for await (const entry of entries) {
234
+ switch (entry.type) {
235
+ case 'step_end':
236
+ if (entry.affected_row_count !== undefined) {
237
+ totalRowsAffected += entry.affected_row_count;
238
+ }
239
+ if (entry.last_insert_rowid) {
240
+ lastInsertRowid = parseInt(entry.last_insert_rowid, 10);
241
+ }
242
+ break;
243
+ case 'step_error':
244
+ case 'error':
245
+ throw new DatabaseError(entry.error?.message || 'Batch execution failed', entry.error?.code);
246
+ }
247
+ }
248
+ return {
249
+ rowsAffected: totalRowsAffected,
250
+ lastInsertRowid
251
+ };
252
+ }
253
+ /**
254
+ * Execute a sequence of SQL statements separated by semicolons.
255
+ *
256
+ * @param sql - SQL string containing multiple statements separated by semicolons
257
+ * @returns Promise resolving when all statements are executed
258
+ */
259
+ async sequence(sql, queryOptions) {
260
+ const request = {
261
+ baton: this.baton,
262
+ requests: [{
263
+ type: "sequence",
264
+ sql: sql
265
+ }]
266
+ };
267
+ const response = await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey, this.createAbortSignal(queryOptions));
268
+ this.baton = response.baton;
269
+ if (response.base_url) {
270
+ this.baseUrl = response.base_url;
271
+ }
272
+ // Check for errors in the response
273
+ if (response.results && response.results[0]) {
274
+ const result = response.results[0];
275
+ if (result.type === "error") {
276
+ throw new DatabaseError(result.error?.message || 'Sequence execution failed', result.error?.code);
277
+ }
278
+ }
279
+ }
280
+ /**
281
+ * Close the session.
282
+ *
283
+ * This sends a close request to the server to properly clean up the stream
284
+ * before resetting the local state.
285
+ */
286
+ async close() {
287
+ // Only send close request if we have an active baton
288
+ if (this.baton) {
289
+ try {
290
+ const request = {
291
+ baton: this.baton,
292
+ requests: [{
293
+ type: "close"
294
+ }]
295
+ };
296
+ await executePipeline(this.baseUrl, this.config.authToken, request, this.config.remoteEncryptionKey);
297
+ }
298
+ catch {
299
+ // Ignore errors during close — the connection might already be closed
300
+ // or the baton may be stale after a timeout.
301
+ }
302
+ }
303
+ // Reset local state
304
+ this.baton = null;
305
+ this.baseUrl = '';
306
+ }
307
+ }
@@ -0,0 +1,161 @@
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';
4
+ /**
5
+ * A prepared SQL statement that can be executed in multiple ways.
6
+ *
7
+ * Statements may either own a dedicated session or share a connection session to preserve transaction boundaries.
8
+ * Provides three execution modes:
9
+ * - `get(args?)`: Returns the first row or null
10
+ * - `all(args?)`: Returns all rows as an array
11
+ * - `iterate(args?)`: Returns an async iterator for streaming results
12
+ */
13
+ export declare class Statement {
14
+ private session;
15
+ private sql;
16
+ private presentationMode;
17
+ private safeIntegerMode;
18
+ private columnMetadata;
19
+ private execLock?;
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;
27
+ /**
28
+ * Whether the prepared statement returns data.
29
+ *
30
+ * This is `true` for SELECT queries and statements with RETURNING clause,
31
+ * and `false` for INSERT, UPDATE, DELETE statements without RETURNING.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const stmt = await conn.prepare(sql);
36
+ * if (stmt.reader) {
37
+ * return stmt.all(args); // SELECT-like query
38
+ * } else {
39
+ * return stmt.run(args); // INSERT/UPDATE/DELETE
40
+ * }
41
+ * ```
42
+ */
43
+ get reader(): boolean;
44
+ /**
45
+ * Enable raw mode to return arrays instead of objects.
46
+ *
47
+ * @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled.
48
+ * @returns This statement instance for chaining
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const stmt = client.prepare("SELECT * FROM users WHERE id = ?");
53
+ * const row = await stmt.raw().get([1]);
54
+ * console.log(row); // [1, "Alice", "alice@example.org"]
55
+ * ```
56
+ */
57
+ raw(raw?: boolean): Statement;
58
+ /**
59
+ * Enable pluck mode to return only the first column value from each row.
60
+ *
61
+ * @param pluck Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled.
62
+ * @returns This statement instance for chaining
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * const stmt = client.prepare("SELECT id FROM users");
67
+ * const ids = await stmt.pluck().all();
68
+ * console.log(ids); // [1, 2, 3, ...]
69
+ * ```
70
+ */
71
+ pluck(pluck?: boolean): Statement;
72
+ /**
73
+ * Sets safe integers mode for this statement.
74
+ *
75
+ * @param toggle Whether to use safe integers. If you don't pass the parameter, safe integers mode is enabled.
76
+ * @returns This statement instance for chaining
77
+ */
78
+ safeIntegers(toggle?: boolean): Statement;
79
+ /**
80
+ * Get column information for this statement.
81
+ *
82
+ * @returns Array of column metadata objects matching the native bindings format
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * const stmt = await client.prepare("SELECT id, name, email FROM users");
87
+ * const columns = stmt.columns();
88
+ * console.log(columns); // [{ name: 'id', type: 'INTEGER', column: null, database: null, table: null }, ...]
89
+ * ```
90
+ */
91
+ columns(): any[];
92
+ private withLock;
93
+ /**
94
+ * Executes the prepared statement.
95
+ *
96
+ * @param args - Optional array of parameter values or object with named parameters
97
+ * @returns Promise resolving to the result of the statement
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const stmt = client.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
102
+ * const result = await stmt.run(['John Doe', 'john.doe@example.com']);
103
+ * console.log(`Inserted user with ID ${result.lastInsertRowid}`);
104
+ * ```
105
+ */
106
+ run(args?: any, queryOptions?: QueryOptions): Promise<any>;
107
+ /**
108
+ * Execute the statement and return the first row.
109
+ *
110
+ * @param args - Optional array of parameter values or object with named parameters
111
+ * @returns Promise resolving to the first row or undefined if no results
112
+ *
113
+ * @example
114
+ * ```typescript
115
+ * const stmt = client.prepare("SELECT * FROM users WHERE id = ?");
116
+ * const user = await stmt.get([123]);
117
+ * if (user) {
118
+ * console.log(user.name);
119
+ * }
120
+ * ```
121
+ */
122
+ get(args?: any, queryOptions?: QueryOptions): Promise<any>;
123
+ /**
124
+ * Execute the statement and return all rows.
125
+ *
126
+ * @param args - Optional array of parameter values or object with named parameters
127
+ * @returns Promise resolving to an array of all result rows
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * const stmt = client.prepare("SELECT * FROM users WHERE active = ?");
132
+ * const activeUsers = await stmt.all([true]);
133
+ * console.log(`Found ${activeUsers.length} active users`);
134
+ * ```
135
+ */
136
+ all(args?: any, queryOptions?: QueryOptions): Promise<any[]>;
137
+ /**
138
+ * Execute the statement and return an async iterator for streaming results.
139
+ *
140
+ * This method provides memory-efficient processing of large result sets
141
+ * by streaming rows one at a time instead of loading everything into memory.
142
+ *
143
+ * @param args - Optional array of parameter values or object with named parameters
144
+ * @returns AsyncGenerator that yields individual rows
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * const stmt = client.prepare("SELECT * FROM large_table WHERE category = ?");
149
+ * for await (const row of stmt.iterate(['electronics'])) {
150
+ * // Process each row individually
151
+ * console.log(row.id, row.name);
152
+ * }
153
+ * ```
154
+ */
155
+ iterate(args?: any, queryOptions?: QueryOptions): AsyncGenerator<any>;
156
+ /**
157
+ * Normalize arguments to handle both single values and arrays.
158
+ * Matches the behavior of the native bindings.
159
+ */
160
+ private normalizeArgs;
161
+ }