Kysely is a type-safe SQL query builder for TypeScript, and most people also use it to execute their SQL queries too. One issue with letting Kysely execute your queries it that logic you would like to apply to all of your queries can't be injected into Kysely easliy, for example retries. As Kysely mentions, it is a SQL query builder and allows you to just create queries and execute them separately.
This post shows how to split query building and execution, gives an example of retry logic, and provides two utility functions that can mimic the .execute
and .takeFirstOrThrow
methods while maintaining type safety.
A common way for developers to deploy their applications now is with serverless providers such as AWS Lambda, or Cloudflare Workers (among many other providers). With these environments a new database connection must be created for each function, and from my experience with a few databases that work well with serverless applications (Neon/Turso), I have seen connection issues or timeouts that will cause your queries to fail and your site to return a 500 status response:
ECONNRESET
, EPIPE
, ETIMEDOUT
One way to solve for these issues is to add retry logic to your SQL queries, that way if the first connection failed the second will probably work. This also solves the issue where your serverless database has to perform a cold start and your connection request times out in the first connection attempt.
The typicaly way that you create and execute a Kysely query looks like this:
Basic Kysely Example
The way you can implement simple retry logic in Kysely looks like this:
Kysely Retry Example
The single failure retry logic will work, but there are two downsides. First, if we want this logic to apply to all database queries we have to wrap every query in this try/catch logic. This will result in a lot of duplicated code and some lengthy functions. Second, this retry logic only works for one failure right after the first failure - but what if we wanted to try a few times?
The next logical step is to extract this retry logic into a function:
Extracted Retry Logic
Using Extraced Retry Logic
Great! Now we can run queries all over our codebase with retry logic, however passing a function just to get our type safety is a bit cumbersome. It would be great to get functions that have type safety and implement the same logic as .execute
, .takeFirst
, and .takeFirstOrThrow
. In the next section we will implement both of these functions and show some examples.
execute
, takeFirst
, and takeFirstOrThrow
FunctionsWhen we split the Kysely logic of query building and query execution all we need to pass to the .executeQuery
function is the query and we will have type safety. A similar interface should exist for the execute
, takeFirst
, and takeFirstOrThrow
function. Examples of this can be seen on the Kysely docs for splitting query building .
Kysely .executeQuery
When we use the .executeQuery
it accepts a wider range of inputs that may or may not have .execute
, .takeFirst
, or .takeFirstOrThrow
. If you have a look at the type of the .executeQuery
it accepts either Compilable<T>
or CompiledQuery<T>
as the query for execution:
Type Signature .executeQuery
Now that we have an idea of what our functions should accept and return, we can build the logic for these. Note that when the command.query.kind
is not SelectQueryNode
, the returned type has possible value of Promise<InferResult<C>>
or Promise<QueryResult<T>>
. The Promise<QueryResult<T>>
won't return a list of items and for this reason the takeFirst
and takeFirstOrThrow
will need to be called on the result of an execute
query. See the example below on how to use this.
Our Functions
Our Function's Examples
This is how you can implement retry logic with Kysely, there is probably a way to fix the takeFirst
and takeFirstOrThrow
so that they don't need to work off the result of an execute
statement, but this has been good enough for my purposes. I will update this blog if a better method arises, I hope you found something useful!