From 86581197bf8c312a442b8050deed7269fd90872e Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 1 Aug 2025 14:00:52 +0300 Subject: [PATCH 1/3] serverless: Fix Statement.get() to return undefined ...aligns with the native bindings semantics. --- packages/turso-serverless/src/statement.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/turso-serverless/src/statement.ts b/packages/turso-serverless/src/statement.ts index 77e7a39e9..11dec8239 100644 --- a/packages/turso-serverless/src/statement.ts +++ b/packages/turso-serverless/src/statement.ts @@ -45,7 +45,7 @@ export class Statement { * Execute the statement and return the first row. * * @param args - Optional array of parameter values or object with named parameters - * @returns Promise resolving to the first row or null if no results + * @returns Promise resolving to the first row or undefined if no results * * @example * ```typescript @@ -58,7 +58,7 @@ export class Statement { */ async get(args: any[] | Record = []): Promise { const result = await this.session.execute(this.sql, args); - return result.rows[0] || null; + return result.rows[0] || undefined; } /** From 335d4a19c87b65379315ffab6073dfd226d102c8 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 1 Aug 2025 14:07:05 +0300 Subject: [PATCH 2/3] serverless: Implement Statement.raw() --- packages/turso-serverless/src/statement.ts | 48 ++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/turso-serverless/src/statement.ts b/packages/turso-serverless/src/statement.ts index 11dec8239..cc44ae939 100644 --- a/packages/turso-serverless/src/statement.ts +++ b/packages/turso-serverless/src/statement.ts @@ -17,12 +17,31 @@ import { DatabaseError } from './error.js'; export class Statement { private session: Session; private sql: string; + private presentationMode: 'expanded' | 'raw' | 'pluck' = 'expanded'; constructor(sessionConfig: SessionConfig, sql: string) { this.session = new Session(sessionConfig); this.sql = sql; } + /** + * Toggle raw mode. + * + * @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. + * @returns This statement instance for chaining + * + * @example + * ```typescript + * const stmt = client.prepare("SELECT * FROM users WHERE id = ?"); + * const row = await stmt.raw().get([1]); + * console.log(row); // [1, "Alice", "alice@example.org"] + * ``` + */ + raw(raw?: boolean): Statement { + this.presentationMode = raw === false ? 'expanded' : 'raw'; + return this; + } + /** * Executes the prepared statement. * @@ -58,7 +77,18 @@ export class Statement { */ async get(args: any[] | Record = []): Promise { const result = await this.session.execute(this.sql, args); - return result.rows[0] || undefined; + const row = result.rows[0]; + if (!row) { + return undefined; + } + + if (this.presentationMode === 'raw') { + // In raw mode, return the row as a plain array (it already is one) + // The row object is already an array with column properties added + return [...row]; + } + + return row; } /** @@ -76,6 +106,13 @@ export class Statement { */ async all(args: any[] | Record = []): Promise { const result = await this.session.execute(this.sql, args); + + if (this.presentationMode === 'raw') { + // In raw mode, return arrays of values + // Each row is already an array with column properties added + return result.rows.map((row: any) => [...row]); + } + return result.rows; } @@ -112,8 +149,13 @@ export class Statement { case 'row': if (entry.row) { const decodedRow = entry.row.map(decodeValue); - const rowObject = this.session.createRowObject(decodedRow, columns); - yield rowObject; + if (this.presentationMode === 'raw') { + // In raw mode, yield arrays of values + yield decodedRow; + } else { + const rowObject = this.session.createRowObject(decodedRow, columns); + yield rowObject; + } } break; case 'step_error': From 47860b6df58acbc2862e47105b0e6b38e48234c9 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Fri, 1 Aug 2025 14:15:34 +0300 Subject: [PATCH 3/3] serverless: Fix bind parameters --- packages/turso-serverless/src/session.ts | 35 +++++++++++++--- packages/turso-serverless/src/statement.ts | 46 +++++++++++++++++----- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/packages/turso-serverless/src/session.ts b/packages/turso-serverless/src/session.ts index 74d9e06f4..6fddbfb2c 100644 --- a/packages/turso-serverless/src/session.ts +++ b/packages/turso-serverless/src/session.ts @@ -75,11 +75,36 @@ export class Session { if (Array.isArray(args)) { positionalArgs = args.map(encodeValue); } else { - // Convert object with named parameters to NamedArg array - namedArgs = Object.entries(args).map(([name, value]) => ({ - name, - value: encodeValue(value) - })); + // Check if this is an object with numeric keys (for ?1, ?2 style parameters) + const keys = Object.keys(args); + const isNumericKeys = keys.length > 0 && keys.every(key => /^\d+$/.test(key)); + + if (isNumericKeys) { + // Convert numeric-keyed object to positional args + // Sort keys numerically to ensure correct order + const sortedKeys = keys.sort((a, b) => parseInt(a) - parseInt(b)); + const maxIndex = parseInt(sortedKeys[sortedKeys.length - 1]); + + // Create array with undefined for missing indices + positionalArgs = new Array(maxIndex); + for (const key of sortedKeys) { + const index = parseInt(key) - 1; // Convert to 0-based index + positionalArgs[index] = encodeValue(args[key]); + } + + // Fill any undefined values with null + for (let i = 0; i < positionalArgs.length; i++) { + if (positionalArgs[i] === undefined) { + positionalArgs[i] = { type: 'null' }; + } + } + } else { + // Convert object with named parameters to NamedArg array + namedArgs = Object.entries(args).map(([name, value]) => ({ + name, + value: encodeValue(value) + })); + } } const request: CursorRequest = { diff --git a/packages/turso-serverless/src/statement.ts b/packages/turso-serverless/src/statement.ts index cc44ae939..c5dbbca99 100644 --- a/packages/turso-serverless/src/statement.ts +++ b/packages/turso-serverless/src/statement.ts @@ -24,8 +24,9 @@ export class Statement { this.sql = sql; } + /** - * Toggle raw mode. + * Enable raw mode to return arrays instead of objects. * * @param raw Enable or disable raw mode. If you don't pass the parameter, raw mode is enabled. * @returns This statement instance for chaining @@ -55,8 +56,9 @@ export class Statement { * console.log(`Inserted user with ID ${result.lastInsertRowid}`); * ``` */ - async run(args: any[] | Record = []): Promise { - const result = await this.session.execute(this.sql, args); + async run(args?: any): Promise { + const normalizedArgs = this.normalizeArgs(args); + const result = await this.session.execute(this.sql, normalizedArgs); return { changes: result.rowsAffected, lastInsertRowid: result.lastInsertRowid }; } @@ -75,8 +77,9 @@ export class Statement { * } * ``` */ - async get(args: any[] | Record = []): Promise { - const result = await this.session.execute(this.sql, args); + async get(args?: any): Promise { + const normalizedArgs = this.normalizeArgs(args); + const result = await this.session.execute(this.sql, normalizedArgs); const row = result.rows[0]; if (!row) { return undefined; @@ -104,8 +107,9 @@ export class Statement { * console.log(`Found ${activeUsers.length} active users`); * ``` */ - async all(args: any[] | Record = []): Promise { - const result = await this.session.execute(this.sql, args); + async all(args?: any): Promise { + const normalizedArgs = this.normalizeArgs(args); + const result = await this.session.execute(this.sql, normalizedArgs); if (this.presentationMode === 'raw') { // In raw mode, return arrays of values @@ -134,8 +138,9 @@ export class Statement { * } * ``` */ - async *iterate(args: any[] | Record = []): AsyncGenerator { - const { response, entries } = await this.session.executeRaw(this.sql, args); + async *iterate(args?: any): AsyncGenerator { + const normalizedArgs = this.normalizeArgs(args); + const { response, entries } = await this.session.executeRaw(this.sql, normalizedArgs); let columns: string[] = []; @@ -165,4 +170,27 @@ export class Statement { } } + /** + * Normalize arguments to handle both single values and arrays. + * Matches the behavior of the native bindings. + */ + private normalizeArgs(args: any): any[] | Record { + // No arguments provided + if (args === undefined) { + return []; + } + + // If it's an array, return as-is + if (Array.isArray(args)) { + return args; + } + + // Check if it's a plain object (for named parameters) + if (args !== null && typeof args === 'object' && args.constructor === Object) { + return args; + } + + // Single value - wrap in array + return [args]; + } }