import { decodeValue, type CursorEntry, type Column } from './protocol.js'; import { Session, type SessionConfig } from './session.js'; import { DatabaseError } from './error.js'; /** * A prepared SQL statement that can be executed in multiple ways. * * Each statement has its own session to avoid conflicts during concurrent execution. * Provides three execution modes: * - `get(args?)`: Returns the first row or null * - `all(args?)`: Returns all rows as an array * - `iterate(args?)`: Returns an async iterator for streaming results */ export class Statement { private session: Session; private sql: string; private presentationMode: 'expanded' | 'raw' | 'pluck' = 'expanded'; private safeIntegerMode: boolean = false; private columnMetadata: Column[]; constructor(sessionConfig: SessionConfig, sql: string, columns?: Column[]) { this.session = new Session(sessionConfig); this.sql = sql; this.columnMetadata = columns || []; } /** * 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 * * @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; } /** * Enable pluck mode to return only the first column value from each row. * * @param pluck Enable or disable pluck mode. If you don't pass the parameter, pluck mode is enabled. * @returns This statement instance for chaining * * @example * ```typescript * const stmt = client.prepare("SELECT id FROM users"); * const ids = await stmt.pluck().all(); * console.log(ids); // [1, 2, 3, ...] * ``` */ pluck(pluck?: boolean): Statement { this.presentationMode = pluck === false ? 'expanded' : 'pluck'; return this; } /** * Sets safe integers mode for this statement. * * @param toggle Whether to use safe integers. If you don't pass the parameter, safe integers mode is enabled. * @returns This statement instance for chaining */ safeIntegers(toggle?: boolean): Statement { this.safeIntegerMode = toggle === false ? false : true; return this; } /** * Get column information for this statement. * * @returns Array of column metadata objects matching the native bindings format * * @example * ```typescript * const stmt = await client.prepare("SELECT id, name, email FROM users"); * const columns = stmt.columns(); * console.log(columns); // [{ name: 'id', type: 'INTEGER', column: null, database: null, table: null }, ...] * ``` */ columns(): any[] { return this.columnMetadata.map(col => ({ name: col.name, type: col.decltype })); } /** * Executes the prepared statement. * * @param args - Optional array of parameter values or object with named parameters * @returns Promise resolving to the result of the statement * * @example * ```typescript * const stmt = client.prepare("INSERT INTO users (name, email) VALUES (?, ?)"); * const result = await stmt.run(['John Doe', 'john.doe@example.com']); * console.log(`Inserted user with ID ${result.lastInsertRowid}`); * ``` */ async run(args?: any): Promise { const normalizedArgs = this.normalizeArgs(args); const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode); return { changes: result.rowsAffected, lastInsertRowid: result.lastInsertRowid }; } /** * 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 undefined if no results * * @example * ```typescript * const stmt = client.prepare("SELECT * FROM users WHERE id = ?"); * const user = await stmt.get([123]); * if (user) { * console.log(user.name); * } * ``` */ async get(args?: any): Promise { const normalizedArgs = this.normalizeArgs(args); const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode); const row = result.rows[0]; if (!row) { return undefined; } if (this.presentationMode === 'pluck') { // In pluck mode, return only the first column value return row[0]; } 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]; } // In expanded mode, convert to plain object with named properties const obj: any = {}; result.columns.forEach((col: string, i: number) => { obj[col] = row[i]; }); return obj; } /** * Execute the statement and return all rows. * * @param args - Optional array of parameter values or object with named parameters * @returns Promise resolving to an array of all result rows * * @example * ```typescript * const stmt = client.prepare("SELECT * FROM users WHERE active = ?"); * const activeUsers = await stmt.all([true]); * console.log(`Found ${activeUsers.length} active users`); * ``` */ async all(args?: any): Promise { const normalizedArgs = this.normalizeArgs(args); const result = await this.session.execute(this.sql, normalizedArgs, this.safeIntegerMode); if (this.presentationMode === 'pluck') { // In pluck mode, return only the first column value from each row return result.rows.map((row: any) => row[0]); } if (this.presentationMode === 'raw') { return result.rows.map((row: any) => [...row]); } // In expanded mode, convert rows to plain objects with named properties return result.rows.map((row: any) => { const obj: any = {}; result.columns.forEach((col: string, i: number) => { obj[col] = row[i]; }); return obj; }); } /** * Execute the statement and return an async iterator for streaming results. * * This method provides memory-efficient processing of large result sets * by streaming rows one at a time instead of loading everything into memory. * * @param args - Optional array of parameter values or object with named parameters * @returns AsyncGenerator that yields individual rows * * @example * ```typescript * const stmt = client.prepare("SELECT * FROM large_table WHERE category = ?"); * for await (const row of stmt.iterate(['electronics'])) { * // Process each row individually * console.log(row.id, row.name); * } * ``` */ async *iterate(args?: any): AsyncGenerator { const normalizedArgs = this.normalizeArgs(args); const { response, entries } = await this.session.executeRaw(this.sql, normalizedArgs); let columns: string[] = []; for await (const entry of entries) { switch (entry.type) { case 'step_begin': if (entry.cols) { columns = entry.cols.map(col => col.name); } break; case 'row': if (entry.row) { const decodedRow = entry.row.map(value => decodeValue(value, this.safeIntegerMode)); if (this.presentationMode === 'pluck') { // In pluck mode, yield only the first column value yield decodedRow[0]; } else 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': case 'error': throw new DatabaseError(entry.error?.message || 'SQL execution failed'); } } } /** * 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]; } }