mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-30 06:24:21 +01:00
272 lines
8.6 KiB
TypeScript
272 lines
8.6 KiB
TypeScript
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<any> {
|
|
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<any> {
|
|
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<any[]> {
|
|
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<any> {
|
|
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<string, any> {
|
|
// 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];
|
|
}
|
|
}
|