From 920b3dc8de0872c92e5b93fa2edee926b3cce8f2 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Tue, 6 May 2025 17:32:10 -0700 Subject: [PATCH] @types/bun: Improvements to Bun.SQL (#19488) Co-authored-by: Ciro Spaciari --- packages/bun-types/bun.d.ts | 207 +++++++++++++----- packages/bun-types/globals.d.ts | 7 - test/integration/bun-types/fixture/spawn.ts | 5 - test/integration/bun-types/fixture/sql.ts | 192 ++++++++++++++++ .../bun-types/fixture/utilities.ts | 1 - 5 files changed, 341 insertions(+), 71 deletions(-) create mode 100644 test/integration/bun-types/fixture/sql.ts diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 5ed9e31f90..afdf569432 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -991,11 +991,9 @@ declare module "bun" { function fileURLToPath(url: URL | string): string; /** - * Fast incremental writer that becomes an `ArrayBuffer` on end(). + * Fast incremental writer that becomes an {@link ArrayBuffer} on end(). */ class ArrayBufferSink { - constructor(); - start(options?: { asUint8Array?: boolean; /** @@ -1320,7 +1318,8 @@ declare module "bun" { * } * }; */ - type SQLOptions = { + + interface SQLOptions { /** Connection URL (can be string or URL object) */ url?: URL | string; /** Database server hostname */ @@ -1369,27 +1368,33 @@ declare module "bun" { bigint?: boolean; /** Automatic creation of prepared statements, defaults to true */ prepare?: boolean; - }; + } /** * Represents a SQL query that can be executed, with additional control methods * Extends Promise to allow for async/await usage */ - interface SQLQuery extends Promise { + interface SQLQuery extends Promise { /** Indicates if the query is currently executing */ active: boolean; + /** Indicates if the query has been cancelled */ cancelled: boolean; + /** Cancels the executing query */ - cancel(): SQLQuery; + cancel(): SQLQuery; + /** Execute as a simple query, no parameters are allowed but can execute multiple commands separated by semicolons */ - simple(): SQLQuery; + simple(): SQLQuery; + /** Executes the query */ - execute(): SQLQuery; + execute(): SQLQuery; + /** Returns the raw query result */ - raw(): SQLQuery; + raw(): SQLQuery; + /** Returns only the values from the query result */ - values(): SQLQuery; + values(): SQLQuery; } /** @@ -1407,65 +1412,117 @@ declare module "bun" { * Main SQL client interface providing connection and transaction management */ interface SQL { - /** Creates a new SQL client instance - * @example - * const sql = new SQL("postgres://localhost:5432/mydb"); - * const sql = new SQL(new URL("postgres://localhost:5432/mydb")); - */ - new (connectionString: string | URL): SQL; - /** Creates a new SQL client instance with options - * @example - * const sql = new SQL("postgres://localhost:5432/mydb", { idleTimeout: 1000 }); - */ - new (connectionString: string | URL, options: SQLOptions): SQL; - /** Creates a new SQL client instance with options - * @example - * const sql = new SQL({ url: "postgres://localhost:5432/mydb", idleTimeout: 1000 }); - */ - new (options?: SQLOptions): SQL; - /** Executes a SQL query using template literals - * @example - * const [user] = await sql`select * from users where id = ${1}`; - */ - (strings: string | TemplateStringsArray, ...values: any[]): SQLQuery; /** - * Helper function to allow easy use to insert values into a query + * Executes a SQL query using template literals * @example - * const result = await sql`insert into users ${sql(users)} RETURNING *`; + * ```ts + * const [user] = await sql`select * from users where id = ${1}`; + * ``` */ - (obj: any): SQLQuery; - /** Commits a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL + (strings: string[] | TemplateStringsArray, ...values: any[]): SQLQuery; + + /** + * Helper function for inserting an object into a query + * * @example + * ```ts + * // Insert an object + * const result = await sql`insert into users ${sql(users)} RETURNING *`; + * + * // Or pick specific columns + * const result = await sql`insert into users ${sql(users, "id", "name")} RETURNING *`; + * + * // Or a single object + * const result = await sql`insert into users ${sql(user)} RETURNING *`; + * ``` + */ + (obj: T | T[] | readonly T[], ...columns: (keyof T)[]): SQLQuery; + + /** + * Helper function for inserting any serializable value into a query + * + * @example + * ```ts + * const result = await sql`SELECT * FROM users WHERE id IN ${sql([1, 2, 3])}`; + * ``` + */ + (obj: unknown): SQLQuery; + + /** + * Commits a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL + * + * @param name - The name of the distributed transaction + * + * @example + * ```ts * await sql.commitDistributed("my_distributed_transaction"); + * ``` */ commitDistributed(name: string): Promise; - /** Rolls back a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL + + /** + * Rolls back a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL + * + * @param name - The name of the distributed transaction + * * @example + * ```ts * await sql.rollbackDistributed("my_distributed_transaction"); + * ``` */ rollbackDistributed(name: string): Promise; + /** Waits for the database connection to be established + * * @example + * ```ts * await sql.connect(); + * ``` */ connect(): Promise; - /** Closes the database connection with optional timeout in seconds. If timeout is 0, it will close immediately, if is not provided it will wait for all queries to finish before closing. + + /** + * Closes the database connection with optional timeout in seconds. If timeout is 0, it will close immediately, if is not provided it will wait for all queries to finish before closing. + * + * @param options - The options for the close + * * @example + * ```ts * await sql.close({ timeout: 1 }); + * ``` */ close(options?: { timeout?: number }): Promise; - /** Closes the database connection with optional timeout in seconds. If timeout is 0, it will close immediately, if is not provided it will wait for all queries to finish before closing. - * @alias close + + /** + * Closes the database connection with optional timeout in seconds. If timeout is 0, it will close immediately, if is not provided it will wait for all queries to finish before closing. + * This is an alias of {@link SQL.close} + * + * @param options - The options for the close + * * @example + * ```ts * await sql.end({ timeout: 1 }); + * ``` */ end(options?: { timeout?: number }): Promise; - /** Flushes any pending operations */ - flush(): void; - /** The reserve method pulls out a connection from the pool, and returns a client that wraps the single connection. - * This can be used for running queries on an isolated connection. - * Calling reserve in a reserved Sql will return a new reserved connection, not the same connection (behavior matches postgres package). + + /** + * Flushes any pending operations + * * @example + * ```ts + * sql.flush(); + * ``` + */ + flush(): void; + + /** + * The reserve method pulls out a connection from the pool, and returns a client that wraps the single connection. + * This can be used for running queries on an isolated connection. + * Calling reserve in a reserved Sql will return a new reserved connection, not the same connection (behavior matches postgres package). + * + * @example + * ```ts * const reserved = await sql.reserve(); * await reserved`select * from users`; * await reserved.release(); @@ -1476,12 +1533,14 @@ declare module "bun" { * } finally { * await reserved.release(); * } - * //To make it simpler bun supportsSymbol.dispose and Symbol.asyncDispose + * + * // Bun supports Symbol.dispose and Symbol.asyncDispose * { - * // always release after context (safer) - * using reserved = await sql.reserve() - * await reserved`select * from users` + * // always release after context (safer) + * using reserved = await sql.reserve() + * await reserved`select * from users` * } + * ``` */ reserve(): Promise; /** Begins a new transaction @@ -1626,6 +1685,45 @@ declare module "bun" { [Symbol.asyncDispose](): Promise; } + const SQL: { + /** + * Creates a new SQL client instance + * + * @param connectionString - The connection string for the SQL client + * + * @example + * ```ts + * const sql = new SQL("postgres://localhost:5432/mydb"); + * const sql = new SQL(new URL("postgres://localhost:5432/mydb")); + * ``` + */ + new (connectionString: string | URL): SQL; + + /** + * Creates a new SQL client instance with options + * + * @param connectionString - The connection string for the SQL client + * @param options - The options for the SQL client + * + * @example + * ```ts + * const sql = new SQL("postgres://localhost:5432/mydb", { idleTimeout: 1000 }); + * ``` + */ + new (connectionString: string | URL, options: Omit): SQL; + + /** + * Creates a new SQL client instance with options + * + * @param options - The options for the SQL client + * + * @example + * ```ts + * const sql = new SQL({ url: "postgres://localhost:5432/mydb", idleTimeout: 1000 }); + * ``` + */ + new (options?: SQLOptions): SQL; + }; /** * Represents a reserved connection from the connection pool @@ -1697,21 +1795,14 @@ declare module "bun" { * * @category Database */ - var sql: SQL; + const sql: SQL; /** * SQL client for PostgreSQL * * @category Database */ - var postgres: SQL; - - /** - * The SQL constructor - * - * @category Database - */ - var SQL: SQL; + const postgres: SQL; /** * Generate and verify CSRF tokens diff --git a/packages/bun-types/globals.d.ts b/packages/bun-types/globals.d.ts index ce6b5c558c..db253bdefc 100644 --- a/packages/bun-types/globals.d.ts +++ b/packages/bun-types/globals.d.ts @@ -954,13 +954,6 @@ interface ErrorConstructor { */ captureStackTrace(targetObject: object, constructorOpt?: Function): void; - /** - * Optional override for formatting stack traces - * - * @see https://v8.dev/docs/stack-trace-api#customizing-stack-traces - */ - prepareStackTrace?: ((err: Error, stackTraces: NodeJS.CallSite[]) => any) | undefined; - /** * The maximum number of stack frames to capture. */ diff --git a/test/integration/bun-types/fixture/spawn.ts b/test/integration/bun-types/fixture/spawn.ts index 59ff8e4832..b999e6be8a 100644 --- a/test/integration/bun-types/fixture/spawn.ts +++ b/test/integration/bun-types/fixture/spawn.ts @@ -179,16 +179,11 @@ function depromise(_promise: Promise): T { tsd.expectType(proc.stdin); } tsd.expectAssignable(Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] })); -tsd.expectNotAssignable(Bun.spawn([], { stdio: ["inherit", "inherit", "inherit"] })); tsd.expectAssignable(Bun.spawn([], { stdio: ["ignore", "pipe", "pipe"] })); tsd.expectAssignable(Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] })); -tsd.expectNotAssignable(Bun.spawn([], { stdio: ["pipe", "ignore", "pipe"] })); tsd.expectAssignable(Bun.spawn([], { stdio: ["pipe", "pipe", "pipe"] })); tsd.expectAssignable(Bun.spawn([], { stdio: ["pipe", "ignore", "inherit"] })); -tsd.expectNotAssignable(Bun.spawn([], { stdio: ["ignore", "pipe", "pipe"] })); tsd.expectAssignable(Bun.spawn([], { stdio: ["ignore", "inherit", "ignore"] })); tsd.expectAssignable(Bun.spawn([], { stdio: [null, null, null] })); -tsd.expectNotAssignable(Bun.spawn([], {})); -tsd.expectNotAssignable(Bun.spawn([], {})); tsd.expectAssignable>(Bun.spawnSync([], {})); diff --git a/test/integration/bun-types/fixture/sql.ts b/test/integration/bun-types/fixture/sql.ts new file mode 100644 index 0000000000..245d4d1602 --- /dev/null +++ b/test/integration/bun-types/fixture/sql.ts @@ -0,0 +1,192 @@ +import { sql } from "bun"; +import { expectAssignable, expectType } from "./utilities"; + +{ + const postgres = new Bun.SQL(); + const id = 1; + await postgres`select * from users where id = ${id}`; +} + +{ + const postgres = new Bun.SQL("postgres://localhost:5432/mydb"); + const id = 1; + await postgres`select * from users where id = ${id}`; +} + +{ + const postgres = new Bun.SQL({ url: "postgres://localhost:5432/mydb" }); + const id = 1; + await postgres`select * from users where id = ${id}`; +} + +{ + const postgres = new Bun.SQL(); + postgres("ok"); +} + +const sql1 = new Bun.SQL(); +const sql2 = new Bun.SQL("postgres://localhost:5432/mydb"); +const sql3 = new Bun.SQL(new URL("postgres://localhost:5432/mydb")); +const sql4 = new Bun.SQL({ url: "postgres://localhost:5432/mydb", idleTimeout: 1000 }); + +const query1 = sql1`SELECT * FROM users WHERE id = ${1}`; +const query2 = sql2({ foo: "bar" }); + +query1.cancel().simple().execute().raw().values(); + +const _promise: Promise = query1; + +sql1.connect(); +sql1.close(); +sql1.end(); +sql1.flush(); + +const reservedPromise: Promise = sql1.reserve(); + +sql1.begin(async txn => { + txn`SELECT 1`; + await txn.savepoint("sp", async sp => { + sp`SELECT 2`; + }); +}); + +sql1.transaction(async txn => { + txn`SELECT 3`; +}); + +sql1.begin("read write", async txn => { + txn`SELECT 4`; +}); + +sql1.transaction("read write", async txn => { + txn`SELECT 5`; +}); + +sql1.beginDistributed("foo", async txn => { + txn`SELECT 6`; +}); + +sql1.distributed("bar", async txn => { + txn`SELECT 7`; +}); + +sql1.unsafe("SELECT * FROM users"); +sql1.file("query.sql", [1, 2, 3]); + +sql1.reserve().then(reserved => { + reserved.release(); + reserved[Symbol.dispose]?.(); + reserved`SELECT 8`; +}); + +sql1.begin(async txn => { + txn.savepoint("sp", async sp => { + sp`SELECT 9`; + }); +}); + +sql1.begin(async txn => { + txn.savepoint(async sp => { + sp`SELECT 10`; + }); +}); + +// @ts-expect-error +sql1.commitDistributed(); + +// @ts-expect-error +sql1.rollbackDistributed(); + +// @ts-expect-error +sql1.file(123); + +// @ts-expect-error +sql1.unsafe(123); + +// @ts-expect-error +sql1.begin("read write", 123); + +// @ts-expect-error +sql1.transaction("read write", 123); + +const sqlQueryAny: Bun.SQLQuery = {} as any; +const sqlQueryNumber: Bun.SQLQuery = {} as any; +const sqlQueryString: Bun.SQLQuery = {} as any; + +expectAssignable>(sqlQueryAny); +expectAssignable>(sqlQueryNumber); +expectAssignable>(sqlQueryString); + +expectType(sqlQueryNumber).is>(); +expectType(sqlQueryString).is>(); +expectType(sqlQueryNumber).is>(); + +const queryA = sql`SELECT 1`; +expectType(queryA).is(); +const queryB = sql({ foo: "bar" }); +expectType(queryB).is(); + +expectType(sql).is(); + +const opts2: Bun.SQLOptions = { url: "postgres://localhost" }; +expectType(opts2).is(); + +const txCb: Bun.SQLTransactionContextCallback = async sql => [sql`SELECT 1`]; +const spCb: Bun.SQLSavepointContextCallback = async sql => [sql`SELECT 2`]; +expectType(txCb).is(); +expectType(spCb).is(); + +expectType(queryA.cancel()).is(); +expectType(queryA.simple()).is(); +expectType(queryA.execute()).is(); +expectType(queryA.raw()).is(); +expectType(queryA.values()).is(); + +declare const queryNum: Bun.SQLQuery; +expectType(queryNum.cancel()).is>(); +expectType(queryNum.simple()).is>(); +expectType(queryNum.execute()).is>(); +expectType(queryNum.raw()).is>(); +expectType(queryNum.values()).is>(); + +expectType(await queryNum.cancel()).is(); +expectType(await queryNum.simple()).is(); +expectType(await queryNum.execute()).is(); +expectType(await queryNum.raw()).is(); +expectType(await queryNum.values()).is(); + +const _sqlInstance: Bun.SQL = Bun.sql; + +expectType(sql({ name: "Alice", email: "alice@example.com" })).is(); + +expectType( + sql([ + { name: "Alice", email: "alice@example.com" }, + { name: "Bob", email: "bob@example.com" }, + ]), +).is(); + +const user = { name: "Alice", email: "alice@example.com", age: 25 }; +expectType(sql(user, "name", "email")).is(); + +const users = [ + { id: 1, name: "Alice" }, + { id: 2, name: "Bob" }, +]; +expectType(sql(users, "id")).is(); + +expectType(sql([1, 2, 3])).is(); + +expectType(sql("users")).is(); + +// @ts-expect-error - missing key in object +sql(user, "notAKey"); + +// @ts-expect-error - wrong type for key argument +sql(user, 123); + +// @ts-expect-error - array of objects, missing key +sql(users, "notAKey"); + +// @ts-expect-error - array of numbers, extra key argument +sql([1, 2, 3], "notAKey"); diff --git a/test/integration/bun-types/fixture/utilities.ts b/test/integration/bun-types/fixture/utilities.ts index d3b0d543f6..4a81e3acdf 100644 --- a/test/integration/bun-types/fixture/utilities.ts +++ b/test/integration/bun-types/fixture/utilities.ts @@ -33,5 +33,4 @@ export function expectType(arg?: T) { } export declare const expectAssignable: (expression: T) => void; -export declare const expectNotAssignable: (expression: any) => void; export declare const expectTypeEquals: (expression: T extends S ? (S extends T ? true : false) : false) => void;