Compare commits

...

22 Commits

Author SHA1 Message Date
Alistair Smith
9eb088140a SQL.ResultLike constraint 2025-07-07 17:51:40 -07:00
Alistair Smith
e14cbb1fd5 Merge branch 'main' of github.com:oven-sh/bun into ali/sql-types 2025-07-07 17:16:19 -07:00
Alistair Smith
29c3e8c66d connect_timeout test 2025-07-07 14:36:40 -07:00
Alistair Smith
8e1a9566a1 better use of SQL.Query in sql.file & sql.unsafe 2025-07-07 14:10:21 -07:00
Alistair Smith
d8ecb14e57 .path is broken, document runtime config .connection for pg 2025-07-07 14:04:50 -07:00
Alistair Smith
dca9d767b6 Bun implements sync function for sql.options.pass[word] 2025-07-07 13:43:10 -07:00
Alistair Smith
39c82c2add all optional values should accept undefined 2025-07-07 13:33:43 -07:00
Alistair Smith
56c1a211d5 revert connection_timeout option line 2025-07-07 13:15:24 -07:00
Alistair Smith
9514905605 Default values for sql options, also introduce path 2025-07-07 13:12:41 -07:00
Alistair Smith
f87c3ca843 improve postgres.js sql compatibility (connect_timeout) 2025-07-07 12:43:58 -07:00
Alistair Smith
a2820dd4c3 Merge branch 'main' of github.com:oven-sh/bun into ali/sql-types 2025-07-07 11:37:51 -07:00
Alistair Smith
94f37639be Merge branch 'ali/sql-types' of github.com:oven-sh/bun into ali/sql-types 2025-07-04 09:09:24 -07:00
Alistair Smith
bef1b91d04 Merge branch 'main' of github.com:oven-sh/bun into ali/sql-types 2025-07-04 09:08:42 -07:00
Alistair Smith
cf997aaef4 Update src/js/bun/sql.ts
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-07-03 22:44:17 -07:00
Alistair Smith
180269aa07 SQLOptions → SQL.Options, make Query require a type param 2025-07-03 22:41:34 -07:00
Alistair Smith
a2612fc267 Merge branch 'main' into ali/sql-types 2025-07-03 18:25:32 -07:00
Alistair Smith
c404c842ee move deprecated symbols, change disposable sql types 2025-07-03 18:22:47 -07:00
Alistair Smith
04809d0c87 avoid deprecated symbols 2025-07-03 18:17:36 -07:00
Alistair Smith
170fce2efe move types to SQL namespace 2025-07-03 18:16:35 -07:00
Alistair Smith
a6b2c3bb18 fix 2025-07-03 18:12:01 -07:00
Alistair Smith
878d6e0e0c fix issues 2025-07-03 18:10:39 -07:00
Alistair Smith
e492f4ccb3 types: Introduce SQLHelper in Bun.sql 2025-07-03 17:45:13 -07:00
6 changed files with 549 additions and 240 deletions

View File

@@ -1315,116 +1315,296 @@ declare module "bun" {
stat(): Promise<import("node:fs").Stats>;
}
/**
* Configuration options for SQL client connection and behavior
* @example
* const config: SQLOptions = {
* host: 'localhost',
* port: 5432,
* user: 'dbuser',
* password: 'secretpass',
* database: 'myapp',
* idleTimeout: 30,
* max: 20,
* onconnect: (client) => {
* console.log('Connected to database');
* }
* };
*/
namespace SQL {
type AwaitPromisesArray<T extends Array<PromiseLike<any>>> = {
[K in keyof T]: Awaited<T[K]>;
};
interface SQLOptions {
/** Connection URL (can be string or URL object) */
url?: URL | string;
/** Database server hostname */
host?: string;
/** Database server hostname (alias for host) */
hostname?: string;
/** Database server port number */
port?: number | string;
/** Database user for authentication */
username?: string;
/** Database user for authentication (alias for username) */
user?: string;
/** Database password for authentication */
password?: string | (() => Promise<string>);
/** Database password for authentication (alias for password) */
pass?: string | (() => Promise<string>);
/** Name of the database to connect to */
database?: string;
/** Name of the database to connect to (alias for database) */
db?: string;
/** Database adapter/driver to use */
adapter?: string;
/** Maximum time in seconds to wait for connection to become available */
idleTimeout?: number;
/** Maximum time in seconds to wait for connection to become available (alias for idleTimeout) */
idle_timeout?: number;
/** Maximum time in seconds to wait when establishing a connection */
connectionTimeout?: number;
/** Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout) */
connection_timeout?: number;
/** Maximum lifetime in seconds of a connection */
maxLifetime?: number;
/** Maximum lifetime in seconds of a connection (alias for maxLifetime) */
max_lifetime?: number;
/** Whether to use TLS/SSL for the connection */
tls?: TLSOptions | boolean;
/** Whether to use TLS/SSL for the connection (alias for tls) */
ssl?: TLSOptions | boolean;
/** Callback function executed when a connection is established */
onconnect?: (client: SQL) => void;
/** Callback function executed when a connection is closed */
onclose?: (client: SQL) => void;
/** Maximum number of connections in the pool */
max?: number;
/** By default values outside i32 range are returned as strings. If this is true, values outside i32 range are returned as BigInts. */
bigint?: boolean;
/** Automatic creation of prepared statements, defaults to true */
prepare?: boolean;
type ContextCallbackResult<T> = T extends Array<PromiseLike<any>> ? AwaitPromisesArray<T> : Awaited<T>;
type ContextCallback<T, SQL> = (sql: SQL) => Promise<T>;
/**
* Configuration options for SQL client connection and behavior
*
* @example
* ```ts
* const config: Bun.SQL.Options = {
* host: 'localhost',
* port: 5432,
* user: 'dbuser',
* password: 'secretpass',
* database: 'myapp',
* idleTimeout: 30,
* max: 20,
* onconnect: (client) => {
* console.log('Connected to database');
* }
* };
* ```
*/
interface Options {
/**
* Connection URL (can be string or URL object)
*/
url?: URL | string | undefined;
/**
* Database server hostname
* @default "localhost"
*/
host?: string | undefined;
/**
* Database server hostname (alias for host)
* @deprecated Prefer {@link host}
* @default "localhost"
*/
hostname?: string | undefined;
/**
* Database server port number
* @default 5432
*/
port?: number | string | undefined;
/**
* Database user for authentication
* @default "postgres"
*/
username?: string | undefined;
/**
* Database user for authentication (alias for username)
* @deprecated Prefer {@link username}
* @default "postgres"
*/
user?: string | undefined;
/**
* Database password for authentication
* @default ""
*/
password?: string | (() => MaybePromise<string>) | undefined;
/**
* Database password for authentication (alias for password)
* @deprecated Prefer {@link password}
* @default ""
*/
pass?: string | (() => MaybePromise<string>) | undefined;
/**
* Name of the database to connect to
* @default The username value
*/
database?: string | undefined;
/**
* Name of the database to connect to (alias for database)
* @deprecated Prefer {@link database}
* @default The username value
*/
db?: string | undefined;
/**
* Database adapter/driver to use
* @default "postgres"
*/
adapter?: "postgres" /*| "sqlite" | "mysql"*/ | (string & {}) | undefined;
/**
* Maximum time in seconds to wait for connection to become available
* @default 0 (no timeout)
*/
idleTimeout?: number | undefined;
/**
* Maximum time in seconds to wait for connection to become available (alias for idleTimeout)
* @deprecated Prefer {@link idleTimeout}
* @default 0 (no timeout)
*/
idle_timeout?: number | undefined;
/**
* Maximum time in seconds to wait when establishing a connection
* @default 30
*/
connectionTimeout?: number | undefined;
/**
* Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout)
* @deprecated Prefer {@link connectionTimeout}
* @default 30
*/
connection_timeout?: number | undefined;
/**
* Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout)
* @deprecated Prefer {@link connectionTimeout}
* @default 30
*/
connectTimeout?: number | undefined;
/**
* Maximum time in seconds to wait when establishing a connection (alias for connectionTimeout)
* @deprecated Prefer {@link connectionTimeout}
* @default 30
*/
connect_timeout?: number | undefined;
/**
* Maximum lifetime in seconds of a connection
* @default 0 (no maximum lifetime)
*/
maxLifetime?: number | undefined;
/**
* Maximum lifetime in seconds of a connection (alias for maxLifetime)
* @deprecated Prefer {@link maxLifetime}
* @default 0 (no maximum lifetime)
*/
max_lifetime?: number | undefined;
/**
* Whether to use TLS/SSL for the connection
* @default false
*/
tls?: TLSOptions | boolean | undefined;
/**
* Whether to use TLS/SSL for the connection (alias for tls)
* @default false
*/
ssl?: TLSOptions | boolean | undefined;
// `.path` is currently unsupported in Bun, the implementation is incomplete.
//
// /**
// * Unix domain socket path for connection
// * @default ""
// */
// path?: string | undefined;
/**
* Callback function executed when a connection is established
*/
onconnect?: ((client: SQL) => void) | undefined;
/**
* Callback function executed when a connection is closed
*/
onclose?: ((client: SQL) => void) | undefined;
/**
* Postgres client runtime configuration options
*
* @see https://www.postgresql.org/docs/current/runtime-config-client.html
*/
connection?: Record<string, string | boolean | number> | undefined;
/**
* Maximum number of connections in the pool
* @default 10
*/
max?: number | undefined;
/**
* By default values outside i32 range are returned as strings. If this is true, values outside i32 range are returned as BigInts.
* @default false
*/
bigint?: boolean | undefined;
/**
* Automatic creation of prepared statements
* @default true
*/
prepare?: boolean | undefined;
}
/**
* Calling sql`` will always return an array of values, ResultLike represents "at least" that list.
*
* @example
* ```ts
* const [one] = await sql<[{value}]>`SELECT 1 as value`;
* console.log(one.value); // => 1
* ```
*/
export type ResultLike = unknown[];
/**
* Represents a SQL query that can be executed, with additional control methods
* Extends Promise to allow for async/await usage
*/
interface Query<T extends ResultLike> extends Promise<T> {
/**
* Indicates if the query is currently executing
*/
active: boolean;
/**
* Indicates if the query has been cancelled
*/
cancelled: boolean;
/**
* Cancels the executing query
*/
cancel(): Query<T>;
/**
* Executes the query as a simple query, no parameters are allowed but can execute multiple commands separated by semicolons
*/
simple(): Query<T>;
/**
* Executes the query
*/
execute(): Query<T>;
/**
* Returns the raw query result
*/
raw(): Query<T>;
/**
* Returns only the values from the query result
*/
values(): Query<T>;
}
/**
* Callback function type for transaction contexts
* @param sql Function to execute SQL queries within the transaction
*/
type TransactionContextCallback<T> = ContextCallback<T, TransactionSQL>;
/**
* Callback function type for savepoint contexts
* @param sql Function to execute SQL queries within the savepoint
*/
type SavepointContextCallback<T> = ContextCallback<T, SavepointSQL>;
/**
* SQL.Helper represents a parameter or serializable
* value inside of a query.
*
* @example
* ```ts
* const helper = sql(users, 'id');
* await sql`insert into users ${helper}`;
* ```
*/
interface Helper<T> {
readonly value: T[];
readonly columns: (keyof T)[];
}
}
/**
* Represents a SQL query that can be executed, with additional control methods
* Extends Promise to allow for async/await usage
*/
interface SQLQuery<T = any> extends Promise<T> {
/** Indicates if the query is currently executing */
active: boolean;
/** Indicates if the query has been cancelled */
cancelled: boolean;
/** Cancels the executing query */
cancel(): SQLQuery<T>;
/** Execute as a simple query, no parameters are allowed but can execute multiple commands separated by semicolons */
simple(): SQLQuery<T>;
/** Executes the query */
execute(): SQLQuery<T>;
/** Returns the raw query result */
raw(): SQLQuery<T>;
/** Returns only the values from the query result */
values(): SQLQuery<T>;
}
/**
* Callback function type for transaction contexts
* @param sql Function to execute SQL queries within the transaction
*/
type SQLTransactionContextCallback = (sql: TransactionSQL) => Promise<any> | Array<SQLQuery>;
/**
* Callback function type for savepoint contexts
* @param sql Function to execute SQL queries within the savepoint
*/
type SQLSavepointContextCallback = (sql: SavepointSQL) => Promise<any> | Array<SQLQuery>;
/**
* Main SQL client interface providing connection and transaction management
*/
interface SQL {
interface SQL extends AsyncDisposable {
/**
* Executes a SQL query using template literals
* @example
@@ -1432,7 +1612,12 @@ declare module "bun" {
* const [user] = await sql`select * from users where id = ${1}`;
* ```
*/
(strings: string[] | TemplateStringsArray, ...values: any[]): SQLQuery;
<T extends SQL.ResultLike = any[]>(strings: TemplateStringsArray, ...values: unknown[]): SQL.Query<T>;
/**
* Execute a SQL query using a string
*/
<T extends SQL.ResultLike = any[]>(string: string): SQL.Query<T>;
/**
* Helper function for inserting an object into a query
@@ -1440,16 +1625,19 @@ declare module "bun" {
* @example
* ```ts
* // Insert an object
* const result = await sql`insert into users ${sql(users)} RETURNING *`;
* 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 *`;
* 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 *`;
* const result = await sql`insert into users ${sql(user)} returning *`;
* ```
*/
<T extends { [Key in PropertyKey]: unknown }>(obj: T | T[] | readonly T[], ...columns: (keyof T)[]): SQLQuery;
<T extends { [Key in PropertyKey]: unknown }, Keys extends keyof T = keyof T>(
obj: T | T[] | readonly T[],
...columns: readonly Keys[]
): SQL.Helper<Pick<T, Keys>>;
/**
* Helper function for inserting any serializable value into a query
@@ -1459,7 +1647,7 @@ declare module "bun" {
* const result = await sql`SELECT * FROM users WHERE id IN ${sql([1, 2, 3])}`;
* ```
*/
(obj: unknown): SQLQuery;
<T>(value: T): SQL.Helper<T>;
/**
* Commits a distributed transaction also know as prepared transaction in postgres or XA transaction in MySQL
@@ -1531,6 +1719,7 @@ declare module "bun" {
/**
* 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).
*
@@ -1556,7 +1745,10 @@ declare module "bun" {
* ```
*/
reserve(): Promise<ReservedSQL>;
/** Begins a new transaction
/**
* Begins a new transaction.
*
* Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.begin will resolve with the returned value from the callback function.
* BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
* @example
@@ -1580,8 +1772,11 @@ declare module "bun" {
* return [user, account]
* })
*/
begin(fn: SQLTransactionContextCallback): Promise<any>;
/** Begins a new transaction with options
begin<const T>(fn: SQL.TransactionContextCallback<T>): Promise<SQL.ContextCallbackResult<T>>;
/**
* Begins a new transaction with options.
*
* Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.begin will resolve with the returned value from the callback function.
* BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
* @example
@@ -1605,8 +1800,11 @@ declare module "bun" {
* return [user, account]
* })
*/
begin(options: string, fn: SQLTransactionContextCallback): Promise<any>;
/** Alternative method to begin a transaction
begin<const T>(options: string, fn: SQL.TransactionContextCallback<T>): Promise<SQL.ContextCallbackResult<T>>;
/**
* Alternative method to begin a transaction.
*
* Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.transaction will resolve with the returned value from the callback function.
* BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
* @alias begin
@@ -1631,11 +1829,15 @@ declare module "bun" {
* return [user, account]
* })
*/
transaction(fn: SQLTransactionContextCallback): Promise<any>;
/** Alternative method to begin a transaction with options
transaction<const T>(fn: SQL.TransactionContextCallback<T>): Promise<SQL.ContextCallbackResult<T>>;
/**
* Alternative method to begin a transaction with options
* Will reserve a connection for the transaction and supply a scoped sql instance for all transaction uses in the callback function. sql.transaction will resolve with the returned value from the callback function.
* BEGIN is automatically sent with the optional options, and if anything fails ROLLBACK will be called so the connection can be released and execution can continue.
* @alias begin
*
* @alias {@link begin}
*
* @example
* const [user, account] = await sql.transaction("read write", async sql => {
* const [user] = await sql`
@@ -1655,15 +1857,18 @@ declare module "bun" {
* returning *
* `
* return [user, account]
* })
* });
*/
transaction(options: string, fn: SQLTransactionContextCallback): Promise<any>;
/** Begins a distributed transaction
transaction<const T>(options: string, fn: SQL.TransactionContextCallback<T>): Promise<SQL.ContextCallbackResult<T>>;
/**
* Begins a distributed transaction
* Also know as Two-Phase Commit, in a distributed transaction, Phase 1 involves the coordinator preparing nodes by ensuring data is written and ready to commit, while Phase 2 finalizes with nodes committing or rolling back based on the coordinator's decision, ensuring durability and releasing locks.
* In PostgreSQL and MySQL distributed transactions persist beyond the original session, allowing privileged users or coordinators to commit/rollback them, ensuring support for distributed transactions, recovery, and administrative tasks.
* beginDistributed will automatic rollback if any exception are not caught, and you can commit and rollback later if everything goes well.
* PostgreSQL natively supports distributed transactions using PREPARE TRANSACTION, while MySQL uses XA Transactions, and MSSQL also supports distributed/XA transactions. However, in MSSQL, distributed transactions are tied to the original session, the DTC coordinator, and the specific connection.
* These transactions are automatically committed or rolled back following the same rules as regular transactions, with no option for manual intervention from other sessions, in MSSQL distributed transactions are used to coordinate transactions using Linked Servers.
*
* @example
* await sql.beginDistributed("numbers", async sql => {
* await sql`create table if not exists numbers (a int)`;
@@ -1673,31 +1878,38 @@ declare module "bun" {
* await sql.commitDistributed("numbers");
* // or await sql.rollbackDistributed("numbers");
*/
beginDistributed(name: string, fn: SQLTransactionContextCallback): Promise<any>;
beginDistributed<const T>(
name: string,
fn: SQL.TransactionContextCallback<T>,
): Promise<SQL.ContextCallbackResult<T>>;
/** Alternative method to begin a distributed transaction
* @alias beginDistributed
* @alias {@link beginDistributed}
*/
distributed(name: string, fn: SQLTransactionContextCallback): Promise<any>;
distributed<const T>(name: string, fn: SQL.TransactionContextCallback<T>): Promise<SQL.ContextCallbackResult<T>>;
/**If you know what you're doing, you can use unsafe to pass any string you'd like.
* Please note that this can lead to SQL injection if you're not careful.
* You can also nest sql.unsafe within a safe sql expression. This is useful if only part of your fraction has unsafe elements.
* @example
* const result = await sql.unsafe(`select ${danger} from users where id = ${dragons}`)
*/
unsafe(string: string, values?: any[]): SQLQuery;
unsafe<T extends SQL.ResultLike = any[]>(string: string, values?: any[]): SQL.Query<T>;
/**
* Reads a file and uses the contents as a query.
* Optional parameters can be used if the file includes $1, $2, etc
* @example
* const result = await sql.file("query.sql", [1, 2, 3]);
*/
file(filename: string, values?: any[]): SQLQuery;
file<T extends SQL.ResultLike = any[]>(filename: string, values?: any[]): SQL.Query<T>;
/** Current client options */
options: SQLOptions;
[Symbol.asyncDispose](): Promise<any>;
/**
* Current client options
*/
options: SQL.Options;
}
const SQL: {
/**
* Creates a new SQL client instance
@@ -1723,7 +1935,7 @@ declare module "bun" {
* const sql = new SQL("postgres://localhost:5432/mydb", { idleTimeout: 1000 });
* ```
*/
new (connectionString: string | URL, options: Omit<SQLOptions, "url">): SQL;
new (connectionString: string | URL, options: Omit<SQL.Options, "url">): SQL;
/**
* Creates a new SQL client instance with options
@@ -1735,17 +1947,18 @@ declare module "bun" {
* const sql = new SQL({ url: "postgres://localhost:5432/mydb", idleTimeout: 1000 });
* ```
*/
new (options?: SQLOptions): SQL;
new (options?: SQL.Options): SQL;
};
/**
* Represents a reserved connection from the connection pool
* Extends SQL with additional release functionality
*/
interface ReservedSQL extends SQL {
/** Releases the client back to the connection pool */
interface ReservedSQL extends SQL, Disposable {
/**
* Releases the client back to the connection pool
*/
release(): void;
[Symbol.dispose](): void;
}
/**
@@ -1754,26 +1967,30 @@ declare module "bun" {
*/
interface TransactionSQL extends SQL {
/** Creates a savepoint within the current transaction */
savepoint(name: string, fn: SQLSavepointContextCallback): Promise<any>;
savepoint(fn: SQLSavepointContextCallback): Promise<any>;
savepoint<T>(name: string, fn: SQLSavepointContextCallback<T>): Promise<T>;
savepoint<T>(fn: SQLSavepointContextCallback<T>): Promise<T>;
}
/**
* Represents a savepoint within a transaction
*/
interface SavepointSQL extends SQL {}
type CSRFAlgorithm = "blake2b256" | "blake2b512" | "sha256" | "sha384" | "sha512" | "sha512-256";
interface CSRFGenerateOptions {
/**
* The number of milliseconds until the token expires. 0 means the token never expires.
* @default 24 * 60 * 60 * 1000 (24 hours)
*/
expiresIn?: number;
/**
* The encoding of the token.
* @default "base64url"
*/
encoding?: "base64" | "base64url" | "hex";
/**
* The algorithm to use for the token.
* @default "sha256"
@@ -1786,16 +2003,19 @@ declare module "bun" {
* The secret to use for the token. If not provided, a random default secret will be generated in memory and used.
*/
secret?: string;
/**
* The encoding of the token.
* @default "base64url"
*/
encoding?: "base64" | "base64url" | "hex";
/**
* The algorithm to use for the token.
* @default "sha256"
*/
algorithm?: CSRFAlgorithm;
/**
* The number of milliseconds until the token expires. 0 means the token never expires.
* @default 24 * 60 * 60 * 1000 (24 hours)
@@ -1805,15 +2025,11 @@ declare module "bun" {
/**
* SQL client
*
* @category Database
*/
const sql: SQL;
/**
* SQL client for PostgreSQL
*
* @category Database
*/
const postgres: SQL;

View File

@@ -14,6 +14,18 @@ declare module "bun" {
): void;
}
/** @deprecated Use {@link SQL.Query Bun.SQL.Query} */
type SQLQuery<T extends SQL.ResultLike = any[]> = SQL.Query<T>;
/** @deprecated Use {@link SQL.TransactionContextCallback Bun.SQL.TransactionContextCallback} */
type SQLTransactionContextCallback<T> = SQL.TransactionContextCallback<T>;
/** @deprecated Use {@link SQL.SavepointContextCallback Bun.SQL.SavepointContextCallback} */
type SQLSavepointContextCallback<T> = SQL.SavepointContextCallback<T>;
/** @deprecated Use {@link SQL.Options Bun.SQL.Options} */
type SQLOptions = SQL.Options;
/**
* @deprecated Renamed to `ErrorLike`
*/

View File

@@ -280,13 +280,13 @@ function normalizeQuery(strings, values, binding_idx = 1) {
binding_values.push(sub_values[j]);
}
binding_idx += sub_values.length;
} else if (value instanceof SQLArrayParameter) {
} else if (value instanceof SQLHelper) {
const command = detectCommand(query);
// only selectIn, insert, update, updateSet are allowed
if (command === SQLCommand.none || command === SQLCommand.where) {
throw new SyntaxError("Helper are only allowed for INSERT, UPDATE and WHERE IN commands");
throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and WHERE IN commands");
}
const { columns, value: items } = value as SQLArrayParameter;
const { columns, value: items } = value as SQLHelper;
const columnCount = columns.length;
if (columnCount === 0 && command !== SQLCommand.whereIn) {
throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`);
@@ -1300,7 +1300,7 @@ function doCreateQuery(strings, values, allowUnsafeTransaction, poolSize, bigint
return createQuery(sqlString, final_values, new SQLResultArray(), undefined, !!bigint, !!simple);
}
class SQLArrayParameter {
class SQLHelper {
value: any;
columns: string[];
constructor(value, keys) {
@@ -1339,7 +1339,7 @@ function decodeIfValid(value) {
}
return null;
}
function loadOptions(o) {
function loadOptions(o: Bun.SQL.Options) {
var hostname,
port,
username,
@@ -1453,6 +1453,8 @@ function loadOptions(o) {
idleTimeout ??= o.idle_timeout;
connectionTimeout ??= o.connectionTimeout;
connectionTimeout ??= o.connection_timeout;
connectionTimeout ??= o.connectTimeout;
connectionTimeout ??= o.connect_timeout;
maxLifetime ??= o.maxLifetime;
maxLifetime ??= o.max_lifetime;
bigint ??= o.bigint;
@@ -1746,14 +1748,10 @@ function SQL(o, e = {}) {
if ($isArray(strings)) {
// detect if is tagged template
if (!$isArray((strings as unknown as TemplateStringsArray).raw)) {
return new SQLArrayParameter(strings, values);
return new SQLHelper(strings, values);
}
} else if (
typeof strings === "object" &&
!(strings instanceof Query) &&
!(strings instanceof SQLArrayParameter)
) {
return new SQLArrayParameter([strings], values);
} else if (typeof strings === "object" && !(strings instanceof Query) && !(strings instanceof SQLHelper)) {
return new SQLHelper([strings], values);
}
// we use the same code path as the transaction sql
return queryFromTransaction(strings, values, pooledConnection, state.queries);
@@ -2079,14 +2077,10 @@ function SQL(o, e = {}) {
if ($isArray(strings)) {
// detect if is tagged template
if (!$isArray((strings as unknown as TemplateStringsArray).raw)) {
return new SQLArrayParameter(strings, values);
return new SQLHelper(strings, values);
}
} else if (
typeof strings === "object" &&
!(strings instanceof Query) &&
!(strings instanceof SQLArrayParameter)
) {
return new SQLArrayParameter([strings], values);
} else if (typeof strings === "object" && !(strings instanceof Query) && !(strings instanceof SQLHelper)) {
return new SQLHelper([strings], values);
}
return queryFromTransaction(strings, values, pooledConnection, state.queries);
@@ -2313,10 +2307,10 @@ function SQL(o, e = {}) {
if ($isArray(strings)) {
// detect if is tagged template
if (!$isArray((strings as unknown as TemplateStringsArray).raw)) {
return new SQLArrayParameter(strings, values);
return new SQLHelper(strings, values);
}
} else if (typeof strings === "object" && !(strings instanceof Query) && !(strings instanceof SQLArrayParameter)) {
return new SQLArrayParameter([strings], values);
} else if (typeof strings === "object" && !(strings instanceof Query) && !(strings instanceof SQLHelper)) {
return new SQLHelper([strings], values);
}
return queryFromPool(strings, values);

View File

@@ -29,12 +29,13 @@ 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 query1 = sql1<[{ id: string; name: string }]>`SELECT * FROM users WHERE id = ${1}`;
const query2 = sql2({ foo: "bar" });
query1.cancel().simple().execute().raw().values();
const _promise: Promise<any> = query1;
expectType(query1).extends<Promise<any>>();
expectType(query1).extends<Promise<[{ id: string; name: string }]>>();
sql1.connect();
sql1.close();
@@ -50,33 +51,74 @@ sql1.begin(async txn => {
});
});
sql1.transaction(async txn => {
txn`SELECT 3`;
});
expectType(
sql1.transaction(async txn => {
txn`SELECT 3`;
}),
).is<Promise<void>>();
sql1.begin("read write", async txn => {
txn`SELECT 4`;
});
expectType(
sql1.begin("read write", async txn => {
txn`SELECT 4`;
}),
).is<Promise<void>>();
sql1.transaction("read write", async txn => {
txn`SELECT 5`;
});
expectType(
sql1.transaction("read write", async txn => {
txn`SELECT 5`;
}),
).is<Promise<void>>();
sql1.beginDistributed("foo", async txn => {
txn`SELECT 6`;
});
expectType(
sql1.beginDistributed("foo", async txn => {
txn`SELECT 6`;
}),
).is<Promise<void>>();
sql1.distributed("bar", async txn => {
txn`SELECT 7`;
});
expectType(
sql1.distributed("bar", async txn => {
txn`SELECT 7`;
}),
).is<Promise<void>>();
sql1.unsafe("SELECT * FROM users");
sql1.file("query.sql", [1, 2, 3]);
expectType(
sql1.beginDistributed("foo", async txn => {
txn`SELECT 8`;
}),
).is<Promise<void>>();
{
const tx = await sql1.transaction(async txn => {
return [await txn<[9]>`SELECT 9`, await txn<[10]>`SELECT 10`];
});
expectType(tx).is<readonly [[9], [10]]>();
}
{
const tx = await sql1.begin(async txn => {
return [await txn<[9]>`SELECT 9`, await txn<[10]>`SELECT 10`];
});
expectType(tx).is<readonly [[9], [10]]>();
}
{
const tx = await sql1.distributed("name", async txn => {
return [await txn<[9]>`SELECT 9`, await txn<[10]>`SELECT 10`];
});
expectType(tx).is<readonly [[9], [10]]>();
}
expectType(sql1.unsafe("SELECT * FROM users")).is<Bun.SQL.Query<any[]>>();
expectType(sql1.unsafe<{ id: string }[]>("SELECT * FROM users")).is<Bun.SQL.Query<{ id: string }[]>>();
expectType(sql1.file("query.sql", [1, 2, 3])).is<Bun.SQL.Query<any[]>>();
sql1.reserve().then(reserved => {
reserved.release();
reserved[Symbol.dispose]?.();
reserved`SELECT 8`;
expectType(reserved<[8]>`SELECT 8`).is<Bun.SQL.Query<[8]>>();
});
sql1.begin(async txn => {
@@ -109,75 +151,102 @@ sql1.begin("read write", 123);
// @ts-expect-error
sql1.transaction("read write", 123);
const sqlQueryAny: Bun.SQLQuery = {} as any;
const sqlQueryNumber: Bun.SQLQuery<number> = {} as any;
const sqlQueryString: Bun.SQLQuery<string> = {} as any;
const sqlQueryAny: Bun.SQL.Query<any[]> = {} as any;
const sqlQueryNumber: Bun.SQL.Query<[number]> = {} as any;
const sqlQueryString: Bun.SQL.Query<[string]> = {} as any;
expectAssignable<Promise<any>>(sqlQueryAny);
expectAssignable<Promise<number>>(sqlQueryNumber);
expectAssignable<Promise<string>>(sqlQueryString);
expectAssignable<Promise<[number]>>(sqlQueryNumber);
expectAssignable<Promise<[string]>>(sqlQueryString);
expectType(sqlQueryNumber).is<Bun.SQLQuery<number>>();
expectType(sqlQueryString).is<Bun.SQLQuery<string>>();
expectType(sqlQueryNumber).is<Bun.SQLQuery<number>>();
expectType(sqlQueryNumber).is<Bun.SQL.Query<[number]>>();
expectType(sqlQueryString).is<Bun.SQL.Query<[string]>>();
const queryA = sql`SELECT 1`;
expectType(queryA).is<Bun.SQLQuery>();
expectType(queryA).is<Bun.SQL.Query<any[]>>();
expectType(await queryA).is<any[]>();
const queryB = sql({ foo: "bar" });
expectType(queryB).is<Bun.SQLQuery>();
expectType(queryB).is<Bun.SQL.Helper<{ foo: string }>>();
expectType(sql).is<Bun.SQL>();
const opts2: Bun.SQLOptions = { url: "postgres://localhost" };
expectType(opts2).is<Bun.SQLOptions>();
const opts2 = { url: "postgres://localhost" } satisfies Bun.SQL.Options;
expectType(opts2).extends<Bun.SQL.Options>();
const txCb: Bun.SQLTransactionContextCallback = async sql => [sql`SELECT 1`];
const spCb: Bun.SQLSavepointContextCallback = async sql => [sql`SELECT 2`];
expectType(txCb).is<Bun.SQLTransactionContextCallback>();
expectType(spCb).is<Bun.SQLSavepointContextCallback>();
const txCb = (async sql => [sql<[1]>`SELECT 1`]) satisfies Bun.SQL.TransactionContextCallback<unknown>;
const spCb = (async sql => [sql<[2]>`SELECT 2`]) satisfies Bun.SQL.SavepointContextCallback<unknown>;
expectType(queryA.cancel()).is<Bun.SQLQuery>();
expectType(queryA.simple()).is<Bun.SQLQuery>();
expectType(queryA.execute()).is<Bun.SQLQuery>();
expectType(queryA.raw()).is<Bun.SQLQuery>();
expectType(queryA.values()).is<Bun.SQLQuery>();
expectType(await sql.begin(txCb)).is<[1][]>();
expectType(await sql.begin(spCb)).is<[2][]>();
declare const queryNum: Bun.SQLQuery<number>;
expectType(queryNum.cancel()).is<Bun.SQLQuery<number>>();
expectType(queryNum.simple()).is<Bun.SQLQuery<number>>();
expectType(queryNum.execute()).is<Bun.SQLQuery<number>>();
expectType(queryNum.raw()).is<Bun.SQLQuery<number>>();
expectType(queryNum.values()).is<Bun.SQLQuery<number>>();
expectType(queryA.cancel()).is<Bun.SQL.Query<any[]>>();
expectType(queryA.simple()).is<Bun.SQL.Query<any[]>>();
expectType(queryA.execute()).is<Bun.SQL.Query<any[]>>();
expectType(queryA.raw()).is<Bun.SQL.Query<any[]>>();
expectType(queryA.values()).is<Bun.SQL.Query<any[]>>();
expectType(await queryNum.cancel()).is<number>();
expectType(await queryNum.simple()).is<number>();
expectType(await queryNum.execute()).is<number>();
expectType(await queryNum.raw()).is<number>();
expectType(await queryNum.values()).is<number>();
declare const queryNum: Bun.SQL.Query<number[]>;
expectType(queryNum.cancel()).is<Bun.SQL.Query<number[]>>();
expectType(queryNum.simple()).is<Bun.SQL.Query<number[]>>();
expectType(queryNum.execute()).is<Bun.SQL.Query<number[]>>();
expectType(queryNum.raw()).is<Bun.SQL.Query<number[]>>();
expectType(queryNum.values()).is<Bun.SQL.Query<number[]>>();
const _sqlInstance: Bun.SQL = Bun.sql;
expectType(await queryNum.cancel()).is<number[]>();
expectType(await queryNum.simple()).is<number[]>();
expectType(await queryNum.execute()).is<number[]>();
expectType(await queryNum.raw()).is<number[]>();
expectType(await queryNum.values()).is<number[]>();
expectType(sql({ name: "Alice", email: "alice@example.com" })).is<Bun.SQLQuery>();
expectType<Bun.SQL.Options>({
password: () => "hey",
pass: async () => "hey",
});
expectType<Bun.SQL.Options>({
password: "hey",
});
expectType(sql({ name: "Alice", email: "alice@example.com" })).is<
Bun.SQL.Helper<{
name: string;
email: string;
}>
>();
expectType(
sql([
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
]),
).is<Bun.SQLQuery>();
).is<
Bun.SQL.Helper<{
name: string;
email: string;
}>
>();
const user = { name: "Alice", email: "alice@example.com", age: 25 };
expectType(sql(user, "name", "email")).is<Bun.SQLQuery>();
const userWithAge = { name: "Alice", email: "alice@example.com", age: 25 };
expectType(sql(userWithAge, "name", "email")).is<
Bun.SQL.Helper<{
name: string;
email: string;
}>
>();
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
];
expectType(sql(users, "id")).is<Bun.SQLQuery>();
expectType(sql(users, "id")).is<Bun.SQL.Helper<{ id: number }>>();
expectType(sql([1, 2, 3])).is<Bun.SQLQuery>();
expectType(sql([1, 2, 3])).is<Bun.SQL.Helper<number[]>>();
expectType(sql([1, 2, 3] as const)).is<Bun.SQL.Helper<readonly [1, 2, 3]>>();
expectType(sql("users")).is<Bun.SQLQuery>();
expectType(sql("users")).is<Bun.SQL.Query<any[]>>();
expectType(sql<[1]>("users")).is<Bun.SQL.Query<[1]>>();
// @ts-expect-error - missing key in object
sql(user, "notAKey");
@@ -190,3 +259,9 @@ sql(users, "notAKey");
// @ts-expect-error - array of numbers, extra key argument
sql([1, 2, 3], "notAKey");
// check the deprecated stuff still exists
expectType<Bun.SQLQuery<"hey"[]>>();
expectType(Bun.sql`hello world`).is<Bun.SQLQuery>();
expectType<Bun.SQLTransactionContextCallback<"hey">>();
expectType<Bun.SQLSavepointContextCallback<"hey">>();

View File

@@ -27,9 +27,11 @@ export function expectType<T>(arg: T): {
* ```
*/
is<X extends T>(...args: IfEquals<X, T> extends true ? [] : [expected: X, but_got: T]): void;
extends<X>(...args: T extends X ? [] : [expected: T, but_got: X]): void;
};
export function expectType<T>(arg?: T) {
return { is() {} };
return { is() {}, extends() {} };
}
export declare const expectAssignable: <T>(expression: T) => void;

View File

@@ -2318,21 +2318,31 @@ if (isDockerEnabled()) {
// ]
// })
// t('connect_timeout', { timeout: 20 }, async() => {
// const connect_timeout = 0.2
// const server = net.createServer()
// server.listen()
// const sql = postgres({ port: server.address().port, host: '127.0.0.1', connect_timeout })
// const start = Date.now()
// let end
// await sql`select 1`.catch((e) => {
// if (e.code !== 'CONNECT_TIMEOUT')
// throw e
// end = Date.now()
// })
// server.close()
// return [connect_timeout, Math.floor((end - start) / 100) / 10]
// })
test.each(["connect_timeout", "connectTimeout", "connectionTimeout", "connection_timeout"] as const)(
"connection timeout key %p throws",
async key => {
const server = net.createServer().listen();
const port = (server.address() as import("node:net").AddressInfo).port;
const sql = postgres({ port, host: "127.0.0.1", [key]: 0.2 });
try {
await sql`select 1`;
throw new Error("should not reach");
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e.code).toBe("ERR_POSTGRES_CONNECTION_TIMEOUT");
expect(e.message).toMatch(/Connection timed out after 200ms/);
} finally {
sql.close();
server.close();
}
},
{
timeout: 1000,
},
);
// t('connect_timeout throws proper error', async() => [
// 'CONNECT_TIMEOUT',