interface
TransactionSQL
interface TransactionSQL
The client passed to transaction callbacks (SQL.begin, SQL.transaction). Extends SQL with savepoints.
The client belongs to its transaction. Once COMMIT or ROLLBACK is sent, a query on it never reaches the database. It rejects with ERR_POSTGRES_INVALID_TRANSACTION_STATE (or the MYSQL / SQLITE code), even if it was created inside the callback. Await every query inside the callback, or return the queries in an array.
- options: Merge<SQLiteOptions, PostgresOrMySQLOptions> | Merge<PostgresOrMySQLOptions, SQLiteOptions>
The resolved settings of the client, after the URL, the options and the environment are merged.
Treat it as output. Timeouts are in milliseconds here, and only the long option names are set (
hostname, nothost), sonew SQL(sql.options)does not make an equal client.console.logprints[REDACTED]for the password and the TLS key. - values: any[] | TypedArray<ArrayBufferLike>,
Creates a PostgreSQL array parameter.
nullelements are SQLNULL, and nested arrays make a multi-dimensional array. Throws on MySQL and SQLite.@param valuesArray values to bind
@param typeNameOrTypeIDElement type name, or the OID of the array type. Bun casts the parameter to it:
$1::INT[]. When omitted, Bun adds no cast and the server infers the type from the column, the compared value or a cast in the query. Pass it when the query gives no context: a bareSELECT ${sql.array([1])}returns the text{"1"}. Pass"JSON"or"JSONB"to encode each element as JSON: therenullis the JSON valuenull,undefinedis SQLNULL, andsql.json([1, 2])stores a JSON array as one element.@returnsThe array parameter, ready to interpolate into a query
const array = sql.array([1, 2, 3], "INT"); await sql`CREATE TABLE users_posts (user_id INT, posts_id INT[])`; await sql`INSERT INTO users_posts (user_id, posts_id) VALUES (${user.id}, ${array})`; // the server infers the type from the compared column await sql`SELECT * FROM users WHERE id = ANY(${sql.array([1, 2, 3])})`; Begins a new transaction.
Reserves a connection for the transaction and passes a scoped
sqlinstance to the callback.sql.beginresolves with the callback's return value.BEGINis sent automatically, and if anything fails,ROLLBACKis sent so the connection can be released and execution can continue.const [user, account] = await sql.begin(async sql => { const [user] = await sql` insert into users ( name ) values ( 'Murray' ) returning * ` const [account] = await sql` insert into accounts ( user_id ) values ( ${ user.user_id } ) returning * ` return [user, account] })options: string,Begins a new transaction with options.
Reserves a connection for the transaction and passes a scoped
sqlinstance to the callback.sql.beginresolves with the callback's return value.BEGINis sent with the given options, and if anything fails,ROLLBACKis sent so the connection can be released and execution can continue.const [user, account] = await sql.begin("read write", async sql => { const [user] = await sql` insert into users ( name ) values ( 'Murray' ) returning * ` const [account] = await sql` insert into accounts ( user_id ) values ( ${ user.user_id } ) returning * ` return [user, account] })- name: string,
Begins a distributed transaction, also known as Two-Phase Commit. In phase 1 the coordinator prepares each node, making sure its data is written and ready to commit; in phase 2 the nodes commit or roll back based on the coordinator's decision, ensuring durability and releasing locks.
beginDistributedrolls back automatically if an exception is not caught; otherwise you commit or roll back later with commitDistributed or rollbackDistributed.In PostgreSQL and MySQL, distributed transactions persist beyond the original session, so privileged users or coordinators can commit or roll them back later, which supports recovery and administrative tasks. PostgreSQL implements them with
PREPARE TRANSACTION; MySQL uses XA Transactions. MSSQL also supports distributed/XA transactions, but ties them to the original session, the DTC coordinator, and the specific connection: they are committed or rolled back under the same rules as regular transactions, with no manual intervention from other sessions, and are used to coordinate transactions across Linked Servers.await sql.beginDistributed("numbers", async sql => { await sql`create table if not exists numbers (a int)`; await sql`insert into numbers values(1)`; }); // later you can call await sql.commitDistributed("numbers"); // or await sql.rollbackDistributed("numbers"); - options?: { timeout: number }): Promise<void>;
Rolls the transaction back. SQL.begin then rejects with
ERR_POSTGRES_CONNECTION_CLOSED(or theMYSQL/SQLITEcode).@param optionstimeoutis how many seconds to wait for the running queries of the transaction before Bun sendsROLLBACK. The default is0. - name: string): Promise<void>;
Commits a distributed transaction, also known as a prepared transaction in PostgreSQL or an XA transaction in MySQL
@param nameName of the distributed transaction
await sql.commitDistributed("my_distributed_transaction"); - name: string,
Begins a distributed transaction. Alias of beginDistributed.
- end(options?: { timeout: number }): Promise<void>;
Closes the database connection. Alias of SQL.close.
@param optionsOptional
timeoutin secondsawait sql.end({ timeout: 1 }); - filename: string,values?: any[] | Record<string, any>
Reads a file and runs its contents as a query. The query is lazy: Bun reads the file when the query starts. Pass
valuesas an array if the file uses positional parameters ($1on PostgreSQL,?on MySQL). With the SQLite adapter,valuesmay also be an object of named parameters (:name,$name, or@nameplaceholders). A key works with or without the prefix.const result = await sql.file("query.sql", [1, 2, 3]); Flushes any pending operations. Does nothing on SQLite, where queries run synchronously.
sql.flush();- json(value: unknown
Returns a parameter that stores
valueas JSON, to interpolate into a query. It works on every adapter.Bun sends an object or an array as JSON by itself, and PostgreSQL stores a string in a
jsonorjsonbcolumn as a JSON string. Usesql.jsonto store a JSON number, boolean ornull, to store an object or an array on SQLite, and to store a JSON string where Bun sends a string as it is: MySQL, MariaDB, SQLite, and PostgreSQL withprepare: false.await sql`INSERT INTO settings (data) VALUES (${sql.json(42)})`; // 42 await sql`INSERT INTO settings (data) VALUES (${sql.json(null)})`; // JSON null, not SQL NULL await sql`INSERT INTO settings (data) VALUES (${sql.json("dark")})`; // "dark" - channel: string,onnotify: (payload: string) => void,onlisten?: () => void
Subscribe to a PostgreSQL
LISTENchannel. Resolves once the server has acknowledged the subscription, with a handle that removes it again.Every call is its own registration: several on one channel share a single server-side subscription and each receives every notification. All of them share one dedicated connection, opened by the first
listen()and closed when the last registration is removed. If it drops, it is reconnected with exponential backoff and every channel is re-subscribed;onlistenruns again each time.A throwing
onnotifyoronlistenis reported as an uncaught exception.@param channelChannel name, quoted for you; at most 63 bytes, the PostgreSQL identifier limit
@param onnotifyReceives each notification's payload
@param onlistenRuns once the
LISTENis acknowledged, initially and after every reconnectconst subscription = await sql.listen("events", payload => console.log(payload)); await sql.notify("events", "hello"); await subscription.unlisten(); - channel: string,payload?: string): Promise<void>;
Send a PostgreSQL
NOTIFYviapg_notify. Runs as a normal query on this handle, so on asql.begin()transaction it is delivered on commit and discarded on rollback. Omittingpayloadsends an empty one, like a bareNOTIFY channel.await sql.notify("events", JSON.stringify({ id: 1 })); await sql.notify("cache-invalidated"); Reserves a connection from the pool and returns a client that wraps that single connection.
Inside a transaction,
reserve()returns a brand new connection, not one related to the transaction. This matches the behaviour of thepostgrespackage.@param optionssignalaborts the reservation while it is still waiting for a connection; the returned promise rejects withsignal.reasonand no connection is taken from the pool.- name: string): Promise<void>;
Rolls back a distributed transaction, also known as a prepared transaction in PostgreSQL or an XA transaction in MySQL
@param nameName of the distributed transaction
await sql.rollbackDistributed("my_distributed_transaction"); - name: string,
Creates a savepoint within the current transaction.
The callback gets a client of its own, which ends with the savepoint.
savepointresolves with the callback's return value. If the callback throws, Bun rolls back to the savepoint andsavepointrejects with the error: catch it to continue the transaction.Run the savepoints of one transaction one after the other. A savepoint holds every statement that the connection runs while it is open, so a savepoint that fails also rolls back the work of one that runs next to it.
@param nameA label. Bun appends it to the name it generates, for example
SAVEPOINT s0_auditfor"audit". Each character that is not an ASCII letter, a digit or_becomes_. Begins a new transaction. Alias of begin.
Reserves a connection for the transaction and passes a scoped
sqlinstance to the callback.sql.transactionresolves with the callback's return value.BEGINis sent automatically, and if anything fails,ROLLBACKis sent so the connection can be released and execution can continue.const [user, account] = await sql.transaction(async sql => { const [user] = await sql` insert into users ( name ) values ( 'Murray' ) returning * ` const [account] = await sql` insert into accounts ( user_id ) values ( ${ user.user_id } ) returning * ` return [user, account] })options: string,Begins a new transaction with options. Alias of begin.
Reserves a connection for the transaction and passes a scoped
sqlinstance to the callback.sql.transactionresolves with the callback's return value.BEGINis sent with the given options, and if anything fails,ROLLBACKis sent so the connection can be released and execution can continue.const [user, account] = await sql.transaction("read write", async sql => { const [user] = await sql` insert into users ( name ) values ( 'Murray' ) returning * ` const [account] = await sql` insert into accounts ( user_id ) values ( ${ user.user_id } ) returning * ` return [user, account] });- string: string,values?: any[] | Record<string, any>
Executes any query string as-is. This can lead to SQL injection if the string contains untrusted input.
The string takes the database's own placeholders:
$1on PostgreSQL,?on MySQL, either on SQLite. Passvaluesas an array, which Bun copies in the call. Withoutvalues, the string can hold several statements.sql.unsafecan be nested inside a safesqlexpression, for example when only part of the query is unsafe. Bun adds the nested text as written. On PostgreSQL, a nestedsql.unsafethat hasvaluescannot follow other parameters, because$1in its text is the first parameter of the whole query: such a query rejects with aSyntaxError.With the SQLite adapter,
valuesmay also be an object of named parameters (:name,$name, or@nameplaceholders). A key works with or without the prefix. This form does not work in a nestedsql.unsafe.const result = await sql.unsafe(`select ${danger} from users where id = ${dragons}`) const row = await sql.unsafe("select * from users where id = :id", { id: 1 })
Referenced types
interface Query<T>
A pending SQL query. Extends Promise, so it can be awaited, and adds methods to control how it runs. The array it resolves to also has ResultMetadata.
A query is lazy. It starts in the call to then(), catch(), finally() or execute, and await calls then().
What then() and catch() return is a promise like any other: when it rejects with nothing to handle the rejection, that is reported as an unhandled rejection. So is an error that a finally() callback throws.
The failure of a query that was only started, with execute() or finally(), is not reported once it has a connection, nor when close({ timeout: 0 }) closes the pool while it waits for one, when it was cancelled before it started, or when its reservation, transaction or savepoint has ended. After execute(), a failure to connect, a pool that was already closed, a cancel() or the end of the time of close({ timeout }), with a timeout above 0, while it waits for a connection, and a query that cannot be built are reported. So is the failure of a query that SQLite runs outside a transaction.
Bun reads the parameters when it sends the query. Leave a Buffer, TypedArray, Date or object that you passed alone until the query settles.
Cancels the query.
A query that Bun has not sent yet never runs. It rejects with
ERR_POSTGRES_QUERY_CANCELLED(or theMYSQL/SQLITEcode).PostgreSQL: Bun asks the server to stop a running query, on a second connection. The query rejects with the server's error,
errno"57014", or resolves if it ends first. A query that Bun sent behind another one, or that has another one sent behind it, rejects withERR_POSTGRES_QUERY_CANCELLEDand ahint, because the server may still run it.MySQL: a query that Bun has sent runs to the end and settles with its result.
- onrejected?: null | (reason: any) => TResult | PromiseLike<TResult>
Attaches a callback for only the rejection of the Promise.
@param onrejectedThe callback to execute when the Promise is rejected.
@returnsA Promise for the completion of the callback.
- onfinally?: null | () => void
Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The resolved value cannot be modified from the callback.
@param onfinallyThe callback to execute when the Promise is settled (fulfilled or rejected).
@returnsA Promise for the completion of the callback.
Returns rows as arrays of
Bufferobjects instead of objects.PostgreSQL and MySQL: the bytes the server sent. On PostgreSQL a prepared query gets
bool,int4,float4,float8,numeric,bytea,time,timestamp,timestamptz,int4[]andfloat4[]in binary and every other type as text; all types are text on the first run of a query without parameters, with.simple()and withprepare: false. On MySQL a prepared query is binary. SQLite:Uint8Arrayobjects.Call it before the query starts. After that, SQLite ignores it, and on PostgreSQL some types can arrive in the other format.
PostgreSQL only. Starts a
COPY ... TO STDOUTquery and returns the bytes that the server sends, in the format that the statement names. A chunk can end in the middle of a row.Call it before the query starts. The query takes no parameters, and resolves with the row count in
countwhen the server has finished. A failure reaches the stream, so the query needs no handler of its own.await Bun.write("users.csv", new Response(sql`COPY users TO STDOUT (FORMAT csv)`.readable()));- onrejected?: null | (reason: any) => TResult2 | PromiseLike<TResult2>): Promise<TResult1 | TResult2>;
Attaches callbacks for the resolution and/or rejection of the Promise.
@param onfulfilledThe callback to execute when the Promise is resolved.
@param onrejectedThe callback to execute when the Promise is rejected.
@returnsA Promise for the completion of which ever callback is executed.
- writable(): WritableStream<string | ArrayBufferLike | TypedArray<ArrayBufferLike> | DataView<ArrayBufferLike>>;
PostgreSQL only. Starts a
COPY ... FROM STDINquery and returns a stream that takes the bytes for the server, in the format that the statement names.close()resolves when the server has stored the rows, and rejects with the server's error.abort()stores nothing. The connection runs nothing else until then, so always close or abort the stream. Bun aborts a stream that is still open when its transaction, savepoint or reservation ends.Call it before the query starts. The query takes no parameters, and resolves with the row count in
count.await Bun.file("users.csv").stream().pipeTo(sql`COPY users FROM STDIN (FORMAT csv)`.writable());