A postgres:// URL of the database: the server, the user and the
database it names are the ones used. The database is given apart from
the URL as well, since Bun 1.4.4 would take PGDATABASE over it.
url:
constconfig: {
DATABASE_URL:string;
}
config.
typeDATABASE_URL: string
DATABASE_URL });
database() returns Kysely on a pool of Bun.SQL, for the database the
generated schema describes. The URL is a postgres:// or postgresql://
one, and the other options are Bun’s options of a pool, such as max,
idleTimeout or tls, but for those the package sets itself. The package
reads nothing from the environment: the URL is passed in, from the
application’s configuration. Bun fills in what the URL leaves out — the
host, the port, the user, the password and sslmode — from PGHOST,
PGPORT, PGUSER, PGPASSWORD and PGSSLMODE, so name them all in the
URL.
Open it once, where the application is wired, and hand it to what needs it,
as any other dependency. db.destroy() closes the pool, on shutdown:
A postgres:// URL of the database: the server, the user and the
database it names are the ones used. The database is given apart from
the URL as well, since Bun 1.4.4 would take PGDATABASE over it.
Resources to release, in order, after the server has stopped.
After, never before: a request still in flight may reach for the pool
that closing it early would have taken away.
close: [() =>
constdb:Kysely<DB>
db.destroy()] });
On a Bun older than 1.4.4, database() throws: before it, Bun read an
array of uuid, of a range and of more types as its literal, where the
generated types say it reads as an array.
The package changes nothing of Kysely’s API but two things Bun.SQL cannot
do yet: the kill session strategy of cancelling a
query, and stream(), which needs a cursor: it
throws, so read a long result in
pages instead, by key. A query of raw SQL, with Kysely’s sql`…`,
binds its values as parameters too. One that binds no value may hold
several statements, and resolves to the rows of the last; with a value,
Postgres refuses more than one.
Values go through as the generated types
say: an int8 is a bigint both ways, unless the types were generated with
--int8 string, a numeric a string, a json or
jsonb column takes an object or an array and returns what Postgres parsed.
The driver mends what Bun does not do:
a Date is written to any timestamp and to a date as its instant, in
ISO text, which a date takes of any year, where it refuses Bun’s own
text of a year past 9999 or before 1; an invalid Date throws a
RangeError before anything is sent, where Bun would send it for
Postgres to refuse;
a number that is not whole, such as 2.5, is sent as its text where the
builder writes it to a column — a value of an insert, an upsert or a
merge, a set — and to any parameter of a query
file, which Postgres has typed. Postgres
reads it as that type: an integer refuses it, invalid input syntax for type integer: "2.5", where Bun would send a float8 the column rounds
to the even, 2, without an error, and a float8, a float4 or a
numeric reads the number it is. Where the builder compares or computes
with it, and in raw SQL, it is bound as Bun binds it, a float8:
where("rank", ">", 2.5) means what it says, and a raw insert rounds;
an int4[] or a float4[], which Bun reads as an Int32Array or a
Float32Array when a query has parameters, is read as an array, as it is
without them.
database() writes a JavaScript array to a column of arrays as the literal
Postgres reads, such as {"gift","glass"}. Bun binds an array as Postgres
reads it only to an array of a type it knows: not of an enum, a domain, a
composite type, a range of one’s own or an extension’s type, and not every
array of json (Bun’s limits). A
literal is read as the column’s own type, whatever it is. database()
writes one wherever a query says the array goes to such a column:
a value of an insert, an upsert or a merge, and a set of an update;
a value compared with a column of arrays of the same query, such as
where("tags", "@>", ["gift"]);
the argument of any() or all(), which only an array can be.
Elements are written as their column takes them: by the delimiter of the
type, which is ; for a box, and as JSON for an array of json or
jsonb. An array of arrays is written as one of more dimensions.
Anywhere else an array is bound as Bun binds it: as JSON into a jsonb
column, and as a Postgres array where raw SQL, or the argument of another
function, casts it to an array of a built-in type:
import { sql } from"kysely";
const
constlabels:string[]
labels= ["gift", "fragile"];
const {
constrows: {
tagged:boolean;
}[]
The rows returned by the query. This is always defined and is
empty if the query returned no rows.
rows } =awaitsql<{
tagged: boolean
tagged:boolean }>`
select ${
constlabels:string[]
labels}::text[] && array['gift'] as tagged
`.execute(
constdb:Kysely<DB>
db);
An array of an enum, or of another type Bun does not know, takes a second
cast from one it knows: ${moods}::text[]::mood[].
A value never becomes text of a query. The builder, sql`…` and the
functions of query files send every value as a parameter, apart from the
SQL, and Postgres reads it as data: a name such as
'; drop table customers; -- is stored as it is. An array goes as one
parameter too, the elements of its literal quoted and escaped.
What a parameter cannot carry is left to the code:
SQL made of input.sql.raw() and sql.lit() write their argument
into the query as it is: never hand them input.
A name from input, such as the column a list is sorted by. A name is
not a value, so it cannot be a parameter: in a query file,
order by :column sorts by a constant. Kysely quotes a name it is given,
but the name can be any column, so map the input to the names it may be.
set, which takes no parameter: sql`set local app.user_id = ${id}`
fails with 42601. set_config() takes the value as one.
A pattern of like, where % and _ from input match anything.
Escape them.
import { sql } from"kysely";
const
constsortable: {
readonlyplaced:"placed_at";
readonlytotal:"total";
}
sortable= {
placed: "placed_at"
placed: "placed_at",
total: "total"
total: "total" } asconst;
functionopenOrders(
sort: "total"|"placed"
sort:keyoftypeof
constsortable: {
readonlyplaced:"placed_at";
readonlytotal:"total";
}
sortable) {
return
constdb:Kysely<DB>
db
.selectFrom("orders")
.select(["id", "total", "placed_at"])
.where("status", "=", "new")
.orderBy(
constsortable: {
readonlyplaced:"placed_at";
readonlytotal:"total";
}
sortable[
sort: "total"|"placed"
sort], "desc")
.execute();
}
await
constdb:Kysely<DB>
db.transaction().execute(async (
trx: Transaction<DB>
trx) => {
awaitsql`select set_config('app.user_id', ${
constuserId:string
userId}, true)`.execute(
trx: Transaction<DB>
trx);
// the transaction's queries, and its policies, read current_setting('app.user_id')
});
const
constmatching: {
email:string;
}[]
matching=await
constdb:Kysely<DB>
db
.selectFrom("customers")
.select("email")
.where("email", "like", `${
consttyped:string
typed.replaceAll(/[\\%_]/g, "\\$&")}%`)
.execute();
set_config() with true holds for the transaction, as set local does.
A query runs on Bun’s pool, which sends the queries of many callers down one
connection without waiting for each answer. A query holds no connection of
its own, and max, 10 by default, limits connections, not queries: the
benchmark runs 32 queries at a
time on 10 connections.
Three things reserve a connection for themselves:
a transaction, from its begin to its end;
the work of db.connection(), below;
a query given a signal with Kysely’s cancel query strategy, to cancel it
by.
Reservations take turns, in the order they come, at one fewer connection
than max, at least one. Bun hands a connection that comes free to what
has waited longest, a query or a reservation, so were every connection
theirs to take, a query that comes behind a burst of transactions — a
health check included — would wait for each of them to start, and behind
long ones for as long as they run. The connection left over is the pool’s.
A reservation inside another’s work, such as a transaction opened on db
inside the callback of db.connection(), needs a turn of its own, and waits
for it while holding the first. With max: 2 there is one turn, and it
waits forever. Run the inner queries on what the work is handed — the
trx of a transaction, the connection of connection() — and it needs no
turn.
Queries of db share no session: two of them may run on different
connections. Work that needs one — a set that later queries read, a
temporary table, a session’s advisory lock — runs in db.connection(),
whose queries share one connection, reserved for it:
The connection goes back to the pool as the work left it: Bun resets
nothing. Undo what the work set for the session before it ends, or set it
with set local inside a transaction, which ends with it. connection()
keeps its session on a Kysely made from db too, by withSchema() or
withPlugin(). A query the work runs on db itself, rather than on what it
is handed, runs on the pool.
Why "somewhat" type-safe? Because the function calls are not bound to the
current query context. They allow you to reference columns and tables that
are not in the current query. E.g. remove the innerJoin from the previous
query and TypeScript won't even complain.
If you want to make the function calls fully type-safe, you can use the
ExpressionBuilder.fn
getter for a query context-aware, stricter
FunctionModule
.
await db.selectFrom('person')
.innerJoin('pet', 'pet.owner_id', 'person.id')
.select((eb) => [
'person.id',
eb.fn.count('pet.id').as('pet_count')
])
.groupBy('person.id')
.having((eb) => eb.fn.count('pet.id'), '>', 10)
.execute()
fn.sum<string>("total").as("total")])
.groupBy("status")
.execute({
signal?: AbortSignal |undefined
An optional signal that can be used to abort the execution of (async) operations.
This is useful for cancelling long-running queries, for example when
the user navigates away from the page or closes the browser tab.
'ignore query' stops waiting for query results. The query continues running
on the database server, and the connection is released back to the pool only
after the in-flight query settles.
'cancel query' attempts to cancel the query on the database side (e.g. pg_cancel_backend
in PostgreSQL or kill query in MySQL). This requires the dialect's connection
to implement
DatabaseConnection.cancelQuery
. Otherwise, falls back
to 'ignore query' with a warning. Writes (insert, update, delete) are not
cancellable in most database engines, so your mileage
may vary. Also, some databases do not cancel the running query immediately
resulting in changes being committed sometimes - consider 'kill session'
when you need stronger guarantees, at all costs.
'kill session' attempts to kill the database process/session the query is
running in (e.g. pg_terminate_backend in PostgreSQL) and with it any running
queries, transactions and obtained locks. This requires the dialect's connection
to implement
DatabaseConnection.killSession
. Otherwise, falls back
to 'cancel query' with a warning. Killing the session is very aggressive,
and will require reconnection on the next database operation if there are
no idle connections available in the pool.
Default is 'ignore query'.
inflightQueryAbortStrategy: "cancel query",
});
The promise then fails with the signal’s reason. What happens to the query
depends on where it was:
Waiting for a connection none can take it on, as when transactions
reserve every one, it is taken out of Bun’s queue, and never runs. On a
pool busy with other queries, Bun sends a query at once, down a
connection behind theirs, so it is sent, as below: a write given up there
may still be made. Make a write one that can run twice, with a key of its
own, before trying it again.
Sent, with the default ignore query, it runs to its end on the
server, and its answer is dropped: a write is made, and a transaction it
runs in goes on.
Sent, with cancel query, a connection of the database’s own sends
Postgres the cancel for its backend: Postgres stops it, and inside a
transaction the transaction then fails to commit. Kysely prints the
query’s own failure, `query` failed in the background after abortion, on the console.
kill session is not supported: Bun’s pool may wait forever to close a
connection the server ended.
A limit the server holds for every query of the pool is
statement_timeout, a setting Bun sends as each connection opens:
import { database } from"@tetsujs/sql";
import { schema } from"./db.generated";
exportconst
constdb:Kysely<DB>
db=database(schema, {
url: string
A postgres:// URL of the database: the server, the user and the
database it names are the ones used. The database is given apart from
the URL as well, since Bun 1.4.4 would take PGDATABASE over it.
url:
constconfig: {
DATABASE_URL:string;
}
config.
typeDATABASE_URL: string
DATABASE_URL,
connection?: Record<string, string | number | boolean>|undefined