|
| 1 | +# Reusable helpers |
| 2 | + |
| 3 | +:::info |
| 4 | +[Here's](https://kyse.link/qm67s) a playground link containing all the code in this recipe. |
| 5 | +::: |
| 6 | + |
| 7 | +Let's say you want to write the following query: |
| 8 | + |
| 9 | +```sql |
| 10 | +SELECT id, first_name |
| 11 | +FROM person |
| 12 | +WHERE upper(last_name) = $1 |
| 13 | +``` |
| 14 | + |
| 15 | +Kysely doesn't have a built-in `upper` function but there are at least three ways you could write this: |
| 16 | + |
| 17 | +```ts |
| 18 | +const lastName = 'STALLONE' |
| 19 | + |
| 20 | +const persons = await db |
| 21 | + .selectFrom('person') |
| 22 | + .select(['id', 'first_name']) |
| 23 | + // 1. `sql` template tag. This is the least type-safe option. |
| 24 | + // You're providing the column name without any type-checking, |
| 25 | + // and plugins won't affect it. |
| 26 | + .where( |
| 27 | + sql<string>`upper(last_name)`, '=', lastName |
| 28 | + ) |
| 29 | + // 2. `sql` template tag with `ref`. Anything passed to `ref` |
| 30 | + // gets type-checked against the accumulated query context. |
| 31 | + .where(({ eb, ref }) => eb( |
| 32 | + sql<string>`upper(${ref('last_name')})`, '=', lastName |
| 33 | + )) |
| 34 | + // 3. The `fn` function helps you avoid missing parentheses/commas |
| 35 | + // errors and uses refs as 1st class arguments. |
| 36 | + .where(({ eb, fn }) => eb( |
| 37 | + fn<string>('upper', ['last_name']), '=', lastName |
| 38 | + )) |
| 39 | + .execute() |
| 40 | +``` |
| 41 | + |
| 42 | +but each option could be more readable or type-safe. |
| 43 | + |
| 44 | +Fortunately Kysely allows you to easily create composable, reusable and type-safe helper functions: |
| 45 | + |
| 46 | +```ts |
| 47 | +import { Expression, sql } from 'kysely' |
| 48 | + |
| 49 | +function upper(expr: Expression<string>) { |
| 50 | + return sql<string>`upper(${expr})` |
| 51 | +} |
| 52 | + |
| 53 | +function lower(expr: Expression<string>) { |
| 54 | + return sql<string>`lower(${expr})` |
| 55 | +} |
| 56 | + |
| 57 | +function concat(...exprs: Expression<string>[]) { |
| 58 | + return sql.join<string>(exprs, sql`||`) |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +Using the `upper` helper, our query would look like this: |
| 63 | + |
| 64 | +```ts |
| 65 | +const lastName = 'STALLONE' |
| 66 | + |
| 67 | +const persons = await db |
| 68 | + .selectFrom('person') |
| 69 | + .select(['id', 'first_name']) |
| 70 | + .where(({ eb, ref }) => eb( |
| 71 | + upper(ref('last_name')), '=', lastName |
| 72 | + )) |
| 73 | + .execute() |
| 74 | +``` |
| 75 | + |
| 76 | +The recipe for helper functions is simple: take inputs as `Expression<T>` instances where `T` is the type of the expression. For example `upper` takes in any `string` expression since it transforms strings to upper case. If you implemented the `round` function, it'd take in `Expression<number>` since you can only round numbers. |
| 77 | + |
| 78 | +The helper functions should then use the inputs to create an output that's also an `Expression`. Everything you can create using the expression builder is an instance of `Expression`. So is the output of the `sql` template tag and all methods under the `sql` object. Same goes for `SelectQueryBuilder` and pretty much everything else in Kysely. Everything's an expression. |
| 79 | + |
| 80 | +See [this recipe](https://kysely.dev/docs/recipes/expressions) to learn more about expressions. |
| 81 | + |
| 82 | +So we've learned that everything's an expression and that expressions are composable. Let's put this idea to use: |
| 83 | + |
| 84 | +```ts |
| 85 | +const persons = await db |
| 86 | + .selectFrom('person') |
| 87 | + .select(['id', 'first_name']) |
| 88 | + .where(({ eb, ref, val }) => eb( |
| 89 | + concat( |
| 90 | + lower(ref('first_name')), |
| 91 | + val(' '), |
| 92 | + upper(ref('last_name')) |
| 93 | + ), |
| 94 | + '=', |
| 95 | + 'sylvester STALLONE' |
| 96 | + )) |
| 97 | + .execute() |
| 98 | +``` |
| 99 | + |
| 100 | +So far we've only used our helper functions in the first argument of `where` but you can use them anywhere: |
| 101 | + |
| 102 | +```ts |
| 103 | +const persons = await db |
| 104 | + .selectFrom('person') |
| 105 | + .innerJoin('pet', (join) => join.on(eb => eb( |
| 106 | + 'person.first_name', '=', lower(eb.ref('pet.name')) |
| 107 | + ))) |
| 108 | + .select(({ ref, val }) => [ |
| 109 | + 'first_name', |
| 110 | + // If you use a helper in `select`, you need to always provide an explicit |
| 111 | + // name for it using the `as` method. |
| 112 | + concat(ref('person.first_name'), val(' '), ref('pet.name')).as('name_with_pet') |
| 113 | + ]) |
| 114 | + .orderBy(({ ref }) => lower(ref('first_name'))) |
| 115 | + .execute() |
| 116 | +``` |
| 117 | + |
| 118 | +## Reusable helpers using `ExpressionBuilder` |
| 119 | + |
| 120 | +Here's an example of a helper function that uses the expression builder instead of raw SQL: |
| 121 | + |
| 122 | +```ts |
| 123 | +import { Expression, expressionBuilder } from 'kysely' |
| 124 | + |
| 125 | +function idsOfPersonsThatHaveDogNamed(name: Expression<string>) { |
| 126 | + const eb = expressionBuilder<DB>() |
| 127 | + |
| 128 | + // A subquery that returns the identifiers of all persons |
| 129 | + // that have a dog named `name`. |
| 130 | + return eb |
| 131 | + .selectFrom('pet') |
| 132 | + .select('pet.owner_id') |
| 133 | + .where('pet.species', '=', 'dog') |
| 134 | + .where('pet.name', '=', name) |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +And here's how you could use it: |
| 139 | + |
| 140 | +```ts |
| 141 | +const dogName = 'Doggo' |
| 142 | + |
| 143 | +const persons = await db |
| 144 | + .selectFrom('person') |
| 145 | + .selectAll('person') |
| 146 | + .where((eb) => eb( |
| 147 | + 'person.id', 'in', idsOfPersonsThatHaveDogNamed(eb.val(dogName)) |
| 148 | + )) |
| 149 | + .execute() |
| 150 | +``` |
| 151 | + |
| 152 | +Note that `idsOfPersonsThatHaveDogNamed` doesn't execute a separate query but instead returns a subquery expression that's compiled as a part of the parent query: |
| 153 | + |
| 154 | +```sql |
| 155 | +select |
| 156 | + person.* |
| 157 | +from |
| 158 | + person |
| 159 | +where |
| 160 | + person.id in ( |
| 161 | + select pet.owner_id |
| 162 | + from pet |
| 163 | + where pet.species = 'dog' |
| 164 | + and pet.name = ? |
| 165 | + ) |
| 166 | +``` |
| 167 | + |
| 168 | +In all our examples we've used the following syntax: |
| 169 | + |
| 170 | +```ts |
| 171 | +.where(eb => eb(left, operator, right)) |
| 172 | +``` |
| 173 | + |
| 174 | +When the expression builder `eb` is used as a function, it creates a binary expression. All binary expressions with a comparison operator are represented as a `Expression<SqlBool>`. You don't always need to return `eb(left, operator, right)` from the callback though. Since `Expressions` are composable and reusable, you can return any `Expression<SqlBool>`. |
| 175 | + |
| 176 | +This means you can create helpers like this: |
| 177 | + |
| 178 | +```ts |
| 179 | +function isOlderThan(age: Expression<number>) { |
| 180 | + return sql<SqlBool>`age > ${age}` |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +```ts |
| 185 | +const persons = await db |
| 186 | + .selectFrom('person') |
| 187 | + .select(['id', 'first_name']) |
| 188 | + .where(({ val }) => isOlderThan(val(60))) |
| 189 | + .execute() |
| 190 | +``` |
| 191 | + |
| 192 | +## Dealing with nullable expressions |
| 193 | + |
| 194 | +If you want your helpers to work with nullable expressions (nullable columns etc.), you can do something like this: |
| 195 | + |
| 196 | +```ts |
| 197 | +import { Expression } from 'kysely' |
| 198 | + |
| 199 | +// This function accepts both nullable and non-nullable string expressions. |
| 200 | +function toInt<T extends string | null>(expr: Expression<T>) { |
| 201 | + // This returns `Expression<number | null>` if `expr` is nullable |
| 202 | + // and `Expression<number>` otherwise. |
| 203 | + return sql<T extends null ? (number | null) : number>`(${expr})::integer` |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +## Passing select queries as expressions |
| 208 | + |
| 209 | +Let's say we have the following query: |
| 210 | + |
| 211 | +```ts |
| 212 | +const expr: Expression<{ name: string }> = db |
| 213 | + .selectFrom('pet') |
| 214 | + .select('pet.name') |
| 215 | +``` |
| 216 | + |
| 217 | +The expression type of our query is `Expression<{ name: string }>` but SQL allows you to use a query like that as an `Expression<string>`. In other words, SQL allows you to use single-column record types like scalars. Most of the time Kysely is able to automatically handle this case but with helper functions you need to use `$asScalar()` to convert the type. Here's an example: |
| 218 | + |
| 219 | +```ts |
| 220 | +const persons = await db |
| 221 | + .selectFrom('person') |
| 222 | + .select((eb) => [ |
| 223 | + 'id', |
| 224 | + 'first_name', |
| 225 | + upper( |
| 226 | + eb.selectFrom('pet') |
| 227 | + .select('name') |
| 228 | + .whereRef('person.id', '=', 'pet.owner_id') |
| 229 | + .limit(1) |
| 230 | + .$asScalar() // <-- This is needed |
| 231 | + .$notNull() |
| 232 | + ).as('pet_name') |
| 233 | + ]) |
| 234 | +``` |
| 235 | + |
| 236 | +The subquery is an `Expression<{ name: string }>` but our `upper` function only accepts `Expression<string>`. That's why we need to call `$asScalar()`. `$asScalar()` has no effect on the generated SQL. It's simply a type-level helper. |
| 237 | + |
| 238 | +We also used `$notNull()` in the example because our simple `upper` function doesn't support nullable expressions. |
0 commit comments