UNPKG

expo-sqlite

Version:

Provides access to a database using SQLite (https://www.sqlite.org/). The database is persisted across restarts of your app.

180 lines (167 loc) 6.21 kB
import type { SQLiteBindValue, SQLiteRunResult } from './NativeStatement'; import type { SQLiteDatabase } from './SQLiteDatabase'; import { parseSQLQuery, type SQLParsedInfo } from './queryUtils'; /** * Conditional type that returns `T[]` when type parameter is explicitly provided, * or union type when using the default `unknown` type. */ type SQLiteTaggedQueryResult<T> = [unknown] extends [T] ? unknown[] | SQLiteRunResult : T[]; /** * A SQL query with tagged template literals API that can be awaited directly (returns array of objects by default), * or transformed using .values() or .first() methods. * * This API is inspired by Bun's SQL interface: * * @example * ```ts * // Default: returns array of objects * const users = await sql`SELECT * FROM users WHERE age > ${21}`; * * // Get values as arrays * const values = await sql`SELECT name, age FROM users`.values(); * // Returns: [["Alice", 30], ["Bob", 25]] * * // Get first row only * const user = await sql`SELECT * FROM users WHERE id = ${1}`.first(); * * // With type parameter * const users = await sql<User>`SELECT * FROM users`; * * // Mutable queries return SQLiteRunResult * const result = await sql`INSERT INTO users (name) VALUES (${"Alice"})` as SQLiteRunResult; * console.log(result.lastInsertRowId, result.changes); * * // Synchronous API * const users = sql<User>`SELECT * FROM users WHERE age > ${21}`.allSync(); * const user = sql<User>`SELECT * FROM users WHERE id = ${userId}`.firstSync(); * ``` */ export class SQLiteTaggedQuery<T = unknown> implements PromiseLike<SQLiteTaggedQueryResult<T>> { private readonly source: string; private readonly params: SQLiteBindValue[]; private readonly parsedInfo: SQLParsedInfo; constructor( private readonly database: SQLiteDatabase, strings: TemplateStringsArray, values: unknown[] ) { const sql = strings.join('?'); this.source = sql; this.params = values as SQLiteBindValue[]; this.parsedInfo = parseSQLQuery(sql); } /** * Make a query awaitable that automatically returns rows or metadata based on query type. * This is called automatically when you await the query. * * @example * ```ts * // SELECT returns array of objects * const users = await sql`SELECT * FROM users`; * * // INSERT returns metadata * const result = await sql`INSERT INTO users (name) VALUES (${"Alice"})`; * console.log(result.lastInsertRowId, result.changes); * * // With type parameter (no assertion needed) * const users = await sql<User>`SELECT * FROM users`; // Type: User[] * ``` */ then<TResult1 = SQLiteTaggedQueryResult<T>, TResult2 = never>( onfulfilled?: ((value: SQLiteTaggedQueryResult<T>) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null ): PromiseLike<TResult1 | TResult2> { if (this.parsedInfo.canReturnRows) { // SELECT, PRAGMA, WITH, EXPLAIN, or has RETURNING clause return this.database.getAllAsync<T>(this.source, this.params).then(onfulfilled, onrejected); } else { // INSERT, UPDATE, DELETE without RETURNING return this.database.runAsync(this.source, this.params).then(onfulfilled as any, onrejected); } } /** * Execute the query and return rows as arrays of values (Bun-style). * Each row is an array where values are in column order. * * @example * ```ts * const rows = await sql`SELECT name, age FROM users`.values(); * // Returns: [["Alice", 30], ["Bob", 25]] * ``` */ async values(): Promise<any[][]> { const statement = await this.database.prepareAsync(this.source); try { const result = await statement.executeForRawResultAsync(this.params); return await result.getAllAsync(); } finally { await statement.finalizeAsync(); } } /** * Execute the query and return the first row only. * Returns null if no rows match. * * @example * ```ts * const user = await sql`SELECT * FROM users WHERE id = ${1}`.first(); * ``` */ async first(): Promise<T | null> { return this.database.getFirstAsync<T>(this.source, this.params); } /** * Execute the query and return an async iterator over the rows. * * @example * ```ts * for await (const user of sql`SELECT * FROM users`.each()) { * console.log(user.name); * } * ``` */ each(): AsyncIterableIterator<T> { return this.database.getEachAsync<T>(this.source, this.params); } // Synchronous variants /** * Execute a query synchronously that returns rows or metadata based on query type. * > **Note:** Running heavy tasks with this function can block the JavaScript thread and affect performance. */ allSync(): SQLiteTaggedQueryResult<T> { if (this.parsedInfo.canReturnRows) { // SELECT, PRAGMA, WITH, EXPLAIN, or has RETURNING clause return this.database.getAllSync<T>(this.source, this.params) as SQLiteTaggedQueryResult<T>; } else { // INSERT, UPDATE, DELETE without RETURNING return this.database.runSync(this.source, this.params) as SQLiteTaggedQueryResult<T>; } } /** * Execute the query synchronously and return rows as arrays of values. * > **Note:** Running heavy tasks with this function can block the JavaScript thread and affect performance. */ valuesSync(): any[][] { const statement = this.database.prepareSync(this.source); try { const result = statement.executeForRawResultSync(this.params); return result.getAllSync(); } finally { statement.finalizeSync(); } } /** * Execute the query synchronously and return the first row. * > **Note:** Running heavy tasks with this function can block the JavaScript thread and affect performance. */ firstSync(): T | null { return this.database.getFirstSync<T>(this.source, this.params); } /** * Execute the query synchronously and return an iterator. * > **Note:** Running heavy tasks with this function can block the JavaScript thread and affect performance. */ eachSync(): IterableIterator<T> { return this.database.getEachSync<T>(this.source, this.params); } }