/** * Query Execution Module for postgres.do * * This module handles: * - Default type parsers * - Parameterized SQL building * - Pending query creation * - Row parsing with type coercion */ import type { Row, PendingQuery, TypeParser, QueryResult, Transport, } from './types' import type { QueryMiddleware, QueryRequest } from './middleware' import { generateQueryId, executeWithMiddleware } from './middleware' // ============================================================================ // Default Type Parsers // ============================================================================ /** * Default type parsers for common PostgreSQL types. * Maps PostgreSQL OID to a parser function that converts string values * to their appropriate JavaScript types. */ export const DEFAULT_PARSERS: Record = { // Boolean 16: (value: string) => value === 't' || value === 'true', // Integer types 20: (value: string) => BigInt(value), // int8/bigint 21: (value: string) => parseInt(value, 10), // int2 23: (value: string) => parseInt(value, 10), // int4 26: (value: string) => parseInt(value, 10), // oid // Float types 700: (value: string) => parseFloat(value), // float4 701: (value: string) => parseFloat(value), // float8 1700: (value: string) => parseFloat(value), // numeric // Date/time types 1082: (value: string) => value, // date (keep as string for flexibility) 1083: (value: string) => value, // time 1114: (value: string) => new Date(value), // timestamp 1184: (value: string) => new Date(value), // timestamptz // JSON types 114: (value: string) => JSON.parse(value), // json 3802: (value: string) => JSON.parse(value), // jsonb // Array types are handled by the server } // ============================================================================ // SQL Building // ============================================================================ /** * Build parameterized SQL from template strings. * Converts template literals to PostgreSQL's $1, $2, etc. placeholder syntax. * * @param strings - The template literal strings array * @param values - The interpolated values * @returns The parameterized SQL string * * @example * ```typescript * const sql = buildParameterizedSql`SELECT * FROM users WHERE id = ${1} AND name = ${'Alice'}` * // Returns: "SELECT * FROM users WHERE id = $1 AND name = $2" * ``` */ export function buildParameterizedSql( strings: TemplateStringsArray, values: unknown[] ): string { const parts: string[] = [] for (let i = 0; i < strings.length; i++) { parts.push(strings[i]!) if (i < values.length) { parts.push(`$${i + 1}`) } } return parts.join('') } // ============================================================================ // Row Parsing // ============================================================================ /** * Parse rows using type parsers. * Applies the appropriate parser to each column based on its PostgreSQL OID. * * @param rows - The raw rows from the database * @param fields - Column metadata including data type OIDs * @param parsers - Map of OID to parser function * @returns The parsed rows with properly typed values */ export function parseRows( rows: T[], fields: QueryResult['fields'], parsers: Record ): T[] { if (!fields || fields.length === 0) { return rows } // Create a map of column name to parser const columnParsers = new Map() for (const field of fields) { const parser = parsers[field.dataTypeID] if (parser) { columnParsers.set(field.name, parser) } } // If no parsers apply, return rows as-is if (columnParsers.size === 0) { return rows } // Parse each row return rows.map((row) => { const parsed: Record = {} for (const [key, value] of Object.entries(row)) { const parser = columnParsers.get(key) if (parser && typeof value === 'string') { try { parsed[key] = parser(value) } catch { parsed[key] = value // Keep original if parsing fails } } else { parsed[key] = value } } return parsed as T }) } // ============================================================================ // Pending Query // ============================================================================ /** * Create a pending query that can be executed. * The pending query is a Promise-like object that also exposes query metadata * (strings, values, toString) for inspection and debugging. * * @param transport - The transport to execute the query on * @param strings - Template literal strings * @param values - Interpolated values * @param parsers - Type parsers for result rows * @param middlewares - Optional middleware chain * @returns A PendingQuery that is both thenable and inspectable */ export function createPendingQuery( transport: Transport, strings: TemplateStringsArray, values: unknown[], parsers: Record, middlewares: QueryMiddleware[] = [] ): PendingQuery { const sql = buildParameterizedSql(strings, values) // Create the promise that executes the query const execute = async (): Promise => { if (middlewares.length === 0) { // No middlewares - execute directly const result = await transport.query(sql, values) return parseRows(result.rows, result.fields, parsers) } // Execute through middleware chain const request: QueryRequest = { sql, params: values, timestamp: Date.now(), queryId: generateQueryId(), } const response = await executeWithMiddleware( middlewares as QueryMiddleware[], request, async () => transport.query(sql, values) ) if (!response.success && response.error) { throw response.error } return parseRows(response.result.rows, response.result.fields, parsers) } // Create the pending query object const promise = execute() as PendingQuery // Add metadata Object.defineProperty(promise, 'strings', { value: strings }) Object.defineProperty(promise, 'values', { value: values }) Object.defineProperty(promise, 'execute', { value: execute }) Object.defineProperty(promise, 'toString', { value: () => sql, }) return promise }