Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

override previous O shared keys @ AllSelection & rowMode: 'array' processing @ PostgresDriver. #1101

Open
wants to merge 10 commits into
base: v0.28
Choose a base branch
from
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
node-version: [18.x, 20.x, 22.4.1]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -42,7 +42,7 @@ jobs:

strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
node-version: [18.x, 20.x, 22.4.1]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
node-version: 22.4.1
cache: 'npm'

- name: Install dependencies
Expand Down Expand Up @@ -107,7 +107,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
node-version: 22.4.1
cache: 'npm'

- name: Install dependencies
Expand All @@ -134,7 +134,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
node-version: 22.4.1
cache: 'npm'

- name: Install dependencies
Expand All @@ -156,7 +156,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
node-version: 22.4.1
cache: 'npm'

- name: Install dependencies
Expand Down
4 changes: 1 addition & 3 deletions site/docs/examples/update/0010-single-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,4 @@ export const singleRow = `const result = await db
last_name: 'Aniston'
})
.where('id', '=', '1')
.executeTakeFirst()

console.log(result.numUpdatedRows)`
.executeTakeFirst()`
4 changes: 1 addition & 3 deletions site/docs/examples/update/0020-complex-values.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@ export const complexValues = `const result = await db
last_name: 'updated',
}))
.where('id', '=', '1')
.executeTakeFirst()

console.log(result.numUpdatedRows)`
.executeTakeFirst()`
7 changes: 7 additions & 0 deletions site/docs/examples/update/0030-my-sql-joins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const mySqlJoins = `const result = await db
.updateTable(['person', 'pet'])
.set('person.first_name', 'Updated person')
.set('pet.name', 'Updated doggo')
.whereRef('person.id', '=', 'pet.owner_id')
.where('person.id', '=', '1')
.executeTakeFirst()`
40 changes: 40 additions & 0 deletions site/docs/examples/update/0030-my-sql-joins.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: 'MySQL joins'
---

# MySQL joins

MySQL allows you to join tables directly to the "main" table and update
rows of all joined tables. This is possible by passing all tables to the
`updateTable` method as a list and adding the `ON` conditions as `WHERE`
statements. You can then use the `set(column, value)` variant to update
columns using table qualified names.

The `UpdateQueryBuilder` also has `innerJoin` etc. join methods, but those
can only be used as part of a PostgreSQL `update set from join` query.
Due to type complexity issues, we unfortunately can't make the same
methods work in both cases.

import {
Playground,
exampleSetup,
} from '../../../src/components/Playground'

import {
mySqlJoins
} from './0030-my-sql-joins'

<div style={{ marginBottom: '1em' }}>
<Playground code={mySqlJoins} setupCode={exampleSetup} />
</div>

:::info[More examples]
The API documentation is packed with examples. The API docs are hosted [here](https://kysely-org.github.io/kysely-apidoc/),
but you can access the same documentation by hovering over functions/methods/classes in your IDE. The examples are always
just one hover away!

For example, check out these sections:
- [set method](https://kysely-org.github.io/kysely-apidoc/classes/UpdateQueryBuilder.html#set)
- [returning method](https://kysely-org.github.io/kysely-apidoc/classes/UpdateQueryBuilder.html#returning)
- [updateTable method](https://kysely-org.github.io/kysely-apidoc/classes/Kysely.html#updateTable)
:::
238 changes: 238 additions & 0 deletions site/docs/recipes/0001-reusable-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
# Reusable helpers

:::info
[Here's](https://kyse.link/qm67s) a playground link containing all the code in this recipe.
:::

Let's say you want to write the following query:

```sql
SELECT id, first_name
FROM person
WHERE upper(last_name) = $1
```

Kysely doesn't have a built-in `upper` function but there are at least three ways you could write this:

```ts
const lastName = 'STALLONE'

const persons = await db
.selectFrom('person')
.select(['id', 'first_name'])
// 1. `sql` template tag. This is the least type-safe option.
// You're providing the column name without any type-checking,
// and plugins won't affect it.
.where(
sql<string>`upper(last_name)`, '=', lastName
)
// 2. `sql` template tag with `ref`. Anything passed to `ref`
// gets type-checked against the accumulated query context.
.where(({ eb, ref }) => eb(
sql<string>`upper(${ref('last_name')})`, '=', lastName
))
// 3. The `fn` function helps you avoid missing parentheses/commas
// errors and uses refs as 1st class arguments.
.where(({ eb, fn }) => eb(
fn<string>('upper', ['last_name']), '=', lastName
))
.execute()
```

but each option could be more readable or type-safe.

Fortunately Kysely allows you to easily create composable, reusable and type-safe helper functions:

```ts
import { Expression, sql } from 'kysely'

function upper(expr: Expression<string>) {
return sql<string>`upper(${expr})`
}

function lower(expr: Expression<string>) {
return sql<string>`lower(${expr})`
}

function concat(...exprs: Expression<string>[]) {
return sql.join<string>(exprs, sql`||`)
}
```

Using the `upper` helper, our query would look like this:

```ts
const lastName = 'STALLONE'

const persons = await db
.selectFrom('person')
.select(['id', 'first_name'])
.where(({ eb, ref }) => eb(
upper(ref('last_name')), '=', lastName
))
.execute()
```

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.

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.

See [this recipe](https://kysely.dev/docs/recipes/expressions) to learn more about expressions.

So we've learned that everything's an expression and that expressions are composable. Let's put this idea to use:

```ts
const persons = await db
.selectFrom('person')
.select(['id', 'first_name'])
.where(({ eb, ref, val }) => eb(
concat(
lower(ref('first_name')),
val(' '),
upper(ref('last_name'))
),
'=',
'sylvester STALLONE'
))
.execute()
```

So far we've only used our helper functions in the first argument of `where` but you can use them anywhere:

```ts
const persons = await db
.selectFrom('person')
.innerJoin('pet', (join) => join.on(eb => eb(
'person.first_name', '=', lower(eb.ref('pet.name'))
)))
.select(({ ref, val }) => [
'first_name',
// If you use a helper in `select`, you need to always provide an explicit
// name for it using the `as` method.
concat(ref('person.first_name'), val(' '), ref('pet.name')).as('name_with_pet')
])
.orderBy(({ ref }) => lower(ref('first_name')))
.execute()
```

## Reusable helpers using `ExpressionBuilder`

Here's an example of a helper function that uses the expression builder instead of raw SQL:

```ts
import { Expression, expressionBuilder } from 'kysely'

function idsOfPersonsThatHaveDogNamed(name: Expression<string>) {
const eb = expressionBuilder<DB>()

// A subquery that returns the identifiers of all persons
// that have a dog named `name`.
return eb
.selectFrom('pet')
.select('pet.owner_id')
.where('pet.species', '=', 'dog')
.where('pet.name', '=', name)
}
```

And here's how you could use it:

```ts
const dogName = 'Doggo'

const persons = await db
.selectFrom('person')
.selectAll('person')
.where((eb) => eb(
'person.id', 'in', idsOfPersonsThatHaveDogNamed(eb.val(dogName))
))
.execute()
```

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:

```sql
select
person.*
from
person
where
person.id in (
select pet.owner_id
from pet
where pet.species = 'dog'
and pet.name = ?
)
```

In all our examples we've used the following syntax:

```ts
.where(eb => eb(left, operator, right))
```

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>`.

This means you can create helpers like this:

```ts
function isOlderThan(age: Expression<number>) {
return sql<SqlBool>`age > ${age}`
}
```

```ts
const persons = await db
.selectFrom('person')
.select(['id', 'first_name'])
.where(({ val }) => isOlderThan(val(60)))
.execute()
```

## Dealing with nullable expressions

If you want your helpers to work with nullable expressions (nullable columns etc.), you can do something like this:

```ts
import { Expression } from 'kysely'

// This function accepts both nullable and non-nullable string expressions.
function toInt<T extends string | null>(expr: Expression<T>) {
// This returns `Expression<number | null>` if `expr` is nullable
// and `Expression<number>` otherwise.
return sql<T extends null ? (number | null) : number>`(${expr})::integer`
}
```

## Passing select queries as expressions

Let's say we have the following query:

```ts
const expr: Expression<{ name: string }> = db
.selectFrom('pet')
.select('pet.name')
```

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:

```ts
const persons = await db
.selectFrom('person')
.select((eb) => [
'id',
'first_name',
upper(
eb.selectFrom('pet')
.select('name')
.whereRef('person.id', '=', 'pet.owner_id')
.limit(1)
.$asScalar() // <-- This is needed
.$notNull()
).as('pet_name')
])
```

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.

We also used `$notNull()` in the example because our simple `upper` function doesn't support nullable expressions.
Loading