Skip to content

Commit

Permalink
fix: onConflict..doUpdateSet using select types instead of update t…
Browse files Browse the repository at this point in the history
…ypes. (kysely-org#792)

* make OnConflictDatabase use Updateable types of tables.

* extract & add complex type use case 4 insert on conflict do update set.
  • Loading branch information
igalklebanov authored and thecodrr committed Sep 3, 2024
1 parent 356b384 commit 06ffa1f
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 195 deletions.
3 changes: 2 additions & 1 deletion src/query-builder/on-conflict-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
UpdateObjectExpression,
parseUpdateObjectExpression,
} from '../parser/update-set-parser.js'
import { Updateable } from '../util/column-type.js'
import { freeze } from '../util/object-utils.js'
import { preventAwait } from '../util/prevent-await.js'
import { AnyColumn, SqlBool } from '../util/type-utils.js'
Expand Down Expand Up @@ -257,7 +258,7 @@ export interface OnConflictBuilderProps {
preventAwait(OnConflictBuilder, "don't await OnConflictBuilder instances.")

export type OnConflictDatabase<DB, TB extends keyof DB> = {
[K in keyof DB | 'excluded']: K extends keyof DB ? DB[K] : DB[TB]
[K in keyof DB | 'excluded']: Updateable<K extends keyof DB ? DB[K] : DB[TB]>
}

export type OnConflictTables<TB> = TB | 'excluded'
Expand Down
2 changes: 2 additions & 0 deletions test/typings/shared.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export interface Person {
// we never want the user to be able to insert or
// update.
modified_at: ColumnType<Date, never, never>
// A column that cannot be inserted, but can be updated.
deleted_at: ColumnType<Date | null, never, string | undefined>
}

export interface PersonMetadata {
Expand Down
3 changes: 3 additions & 0 deletions test/typings/test-d/delete-query-builder.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ async function testDelete(db: Kysely<Database>) {
gender: 'male' | 'female' | 'other'
modified_at: Date
marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null
deleted_at: Date | null

name: string
owner_id: number
Expand All @@ -136,6 +137,7 @@ async function testDelete(db: Kysely<Database>) {
gender: 'male' | 'female' | 'other'
modified_at: Date
marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null
deleted_at: Date | null

name: string
owner_id: number
Expand Down Expand Up @@ -173,6 +175,7 @@ async function testDelete(db: Kysely<Database>) {
gender: 'male' | 'female' | 'other'
modified_at: Date
marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null
deleted_at: Date | null

name: string
owner_id: number
Expand Down
196 changes: 2 additions & 194 deletions test/typings/test-d/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,202 +7,10 @@
* happy, but we can catch it here.
*/

import {
Kysely,
Transaction,
InsertResult,
UpdateResult,
Selectable,
sql,
ExpressionBuilder,
} from '..'
import { Kysely, Transaction, InsertResult, UpdateResult, Selectable } from '..'

import { Database, Person } from '../shared'
import { expectType, expectError, expectAssignable } from 'tsd'

async function testInsert(db: Kysely<Database>) {
const person = {
first_name: 'Jennifer',
last_name: 'Aniston',
gender: 'other' as const,
age: 30,
}

// Insert one row
const r1 = await db.insertInto('person').values(person).execute()

expectType<InsertResult[]>(r1)

// Should be able to leave out nullable columns like last_name
const r2 = await db
.insertInto('person')
.values({ first_name: 'fname', age: 10, gender: 'other' })
.executeTakeFirst()

expectType<InsertResult>(r2)

// The result type is correct when executeTakeFirstOrThrow is used
const r3 = await db
.insertInto('person')
.values(person)
.executeTakeFirstOrThrow()

expectType<InsertResult>(r3)

// Insert values from a CTE
const r4 = await db
.with('foo', (db) =>
db.selectFrom('person').select('id').where('person.id', '=', 1)
)
.insertInto('movie')
.values({
stars: (eb) => eb.selectFrom('foo').select('foo.id'),
})
.executeTakeFirst()

expectType<InsertResult>(r4)

// Insert with an on conflict statement
const r5 = await db
.insertInto('person')
.values(person)
.onConflict((oc) =>
oc.column('id').doUpdateSet({
// Should be able to reference the `excluded` "table"
first_name: (eb) => eb.ref('excluded.first_name'),
last_name: (eb) => eb.ref('last_name'),
})
)
.executeTakeFirst()

expectType<InsertResult>(r5)

// Non-existent table
expectError(db.insertInto('doesnt_exists'))

// Non-existent column
expectError(db.insertInto('person').values({ not_column: 'foo' }))

// Wrong type for a column
expectError(
db.insertInto('person').values({ first_name: 10, age: 10, gender: 'other' })
)

// Missing required columns
expectError(db.insertInto('person').values({ first_name: 'Jennifer' }))

// Explicitly excluded column
expectError(db.insertInto('person').values({ modified_at: new Date() }))

// Non-existent column in a `doUpdateSet` call.
expectError(
db
.insertInto('person')
.values(person)
.onConflict((oc) =>
oc.column('id').doUpdateSet({
first_name: (eb) => eb.ref('doesnt_exist'),
})
)
)

// GeneratedAlways column is not allowed to be inserted
expectError(db.insertInto('book').values({ id: 1, name: 'foo' }))

// Wrong subquery return value type
expectError(
db.insertInto('person').values({
first_name: 'what',
gender: 'male',
age: (eb) => eb.selectFrom('pet').select('pet.name'),
})
)

// Nullable column as undefined
const insertObject: {
first_name: string
last_name: string | undefined
age: number
gender: 'male' | 'female' | 'other'
} = {
first_name: 'emily',
last_name: 'smith',
age: 25,
gender: 'female',
}

db.insertInto('person').values(insertObject)
}

async function testReturning(db: Kysely<Database>) {
const person = {
first_name: 'Jennifer',
last_name: 'Aniston',
gender: 'other' as const,
age: 30,
}

// One returning expression
const r1 = await db
.insertInto('person')
.values(person)
.returning('id')
.executeTakeFirst()

expectType<
| {
id: number
}
| undefined
>(r1)

// Multiple returning expressions
const r2 = await db
.insertInto('person')
.values(person)
.returning(['id', 'person.first_name as fn'])
.execute()

expectType<
{
id: number
fn: string
}[]
>(r2)

// Non-column reference returning expressions
const r3 = await db
.insertInto('person')
.values(person)
.returning([
'id',
sql<string>`concat(first_name, ' ', last_name)`.as('full_name'),
(qb) => qb.selectFrom('pet').select('pet.id').as('sub'),
])
.execute()

expectType<
{
id: number
full_name: string
sub: string | null
}[]
>(r3)

const r4 = await db
.insertInto('movie')
.values({ stars: 5 })
.returningAll()
.executeTakeFirstOrThrow()

expectType<{
id: string
stars: number
}>(r4)

// Non-existent column
expectError(db.insertInto('person').values(person).returning('not_column'))
}
import { expectType, expectError } from 'tsd'

async function testUpdate(db: Kysely<Database>) {
const r1 = await db
Expand Down
Loading

0 comments on commit 06ffa1f

Please sign in to comment.