From 04f8c26ded649157c5d42f4cd87c308c7b9a83e4 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 4 Dec 2023 00:52:10 +0200 Subject: [PATCH 1/2] make OnConflictDatabase use Updateable types of tables. --- src/query-builder/on-conflict-builder.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/query-builder/on-conflict-builder.ts b/src/query-builder/on-conflict-builder.ts index 575936226..f5ce407da 100644 --- a/src/query-builder/on-conflict-builder.ts +++ b/src/query-builder/on-conflict-builder.ts @@ -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' @@ -257,7 +258,7 @@ export interface OnConflictBuilderProps { preventAwait(OnConflictBuilder, "don't await OnConflictBuilder instances.") export type OnConflictDatabase = { - [K in keyof DB | 'excluded']: K extends keyof DB ? DB[K] : DB[TB] + [K in keyof DB | 'excluded']: Updateable } export type OnConflictTables = TB | 'excluded' From 04ba87d97d937f215638c79a8e7b00f0e83bd3a3 Mon Sep 17 00:00:00 2001 From: igalklebanov Date: Mon, 4 Dec 2023 00:52:48 +0200 Subject: [PATCH 2/2] extract & add complex type use case 4 insert on conflict do update set. --- test/typings/shared.d.ts | 2 + .../test-d/delete-query-builder.test-d.ts | 3 + test/typings/test-d/index.test-d.ts | 196 +----------------- test/typings/test-d/insert.test-d.ts | 189 +++++++++++++++++ test/typings/test-d/join.test-d.ts | 4 + test/typings/test-d/select-from.test-d.ts | 1 + test/typings/test-d/select.test-d.ts | 2 + 7 files changed, 203 insertions(+), 194 deletions(-) create mode 100644 test/typings/test-d/insert.test-d.ts diff --git a/test/typings/shared.d.ts b/test/typings/shared.d.ts index 383eafcde..9a7bc1afd 100644 --- a/test/typings/shared.d.ts +++ b/test/typings/shared.d.ts @@ -64,6 +64,8 @@ export interface Person { // we never want the user to be able to insert or // update. modified_at: ColumnType + // A column that cannot be inserted, but can be updated. + deleted_at: ColumnType } export interface PersonMetadata { diff --git a/test/typings/test-d/delete-query-builder.test-d.ts b/test/typings/test-d/delete-query-builder.test-d.ts index 44e232618..5eca057f3 100644 --- a/test/typings/test-d/delete-query-builder.test-d.ts +++ b/test/typings/test-d/delete-query-builder.test-d.ts @@ -111,6 +111,7 @@ async function testDelete(db: Kysely) { gender: 'male' | 'female' | 'other' modified_at: Date marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null + deleted_at: Date | null name: string owner_id: number @@ -136,6 +137,7 @@ async function testDelete(db: Kysely) { gender: 'male' | 'female' | 'other' modified_at: Date marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null + deleted_at: Date | null name: string owner_id: number @@ -173,6 +175,7 @@ async function testDelete(db: Kysely) { gender: 'male' | 'female' | 'other' modified_at: Date marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null + deleted_at: Date | null name: string owner_id: number diff --git a/test/typings/test-d/index.test-d.ts b/test/typings/test-d/index.test-d.ts index 9e36e3594..22937507d 100644 --- a/test/typings/test-d/index.test-d.ts +++ b/test/typings/test-d/index.test-d.ts @@ -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) { - 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(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(r2) - - // The result type is correct when executeTakeFirstOrThrow is used - const r3 = await db - .insertInto('person') - .values(person) - .executeTakeFirstOrThrow() - - expectType(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(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(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) { - 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`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) { const r1 = await db diff --git a/test/typings/test-d/insert.test-d.ts b/test/typings/test-d/insert.test-d.ts new file mode 100644 index 000000000..d4804f477 --- /dev/null +++ b/test/typings/test-d/insert.test-d.ts @@ -0,0 +1,189 @@ +import { expectError, expectType } from 'tsd' +import { InsertResult, Kysely, sql } from '..' +import { Database } from '../shared' + +async function testInsert(db: Kysely) { + 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(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(r2) + + // The result type is correct when executeTakeFirstOrThrow is used + const r3 = await db + .insertInto('person') + .values(person) + .executeTakeFirstOrThrow() + + expectType(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(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'), + // `excluded` "table" should take the `UpdateType` of complex columns. + deleted_at: (eb) => eb.ref('excluded.deleted_at'), + }) + ) + .executeTakeFirst() + + expectType(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) { + 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`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')) +} diff --git a/test/typings/test-d/join.test-d.ts b/test/typings/test-d/join.test-d.ts index 51e0e88ef..6e2061328 100644 --- a/test/typings/test-d/join.test-d.ts +++ b/test/typings/test-d/join.test-d.ts @@ -20,6 +20,7 @@ async function testJoin(db: Kysely) { gender: 'male' | 'female' | 'other' modified_at: Date marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null + deleted_at: Date | null // Pet columns. name: string @@ -84,6 +85,7 @@ async function testJoin(db: Kysely) { gender: 'male' | 'female' | 'other' modified_at: Date marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null + deleted_at: Date | null // All Pet columns should be nullable because of the left join name: string | null @@ -111,6 +113,7 @@ async function testJoin(db: Kysely) { gender: 'male' | 'female' | 'other' | null modified_at: Date | null marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null + deleted_at: Date | null // All Pet columns should also be nullable because there's another // right join after the Pet join. @@ -140,6 +143,7 @@ async function testJoin(db: Kysely) { gender: 'male' | 'female' | 'other' | null modified_at: Date | null marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null + deleted_at: Date | null name: string | null species: 'dog' | 'cat' | null diff --git a/test/typings/test-d/select-from.test-d.ts b/test/typings/test-d/select-from.test-d.ts index 3685a6fd4..ced58541b 100644 --- a/test/typings/test-d/select-from.test-d.ts +++ b/test/typings/test-d/select-from.test-d.ts @@ -14,6 +14,7 @@ async function testFromSingle(db: Kysely) { gender: 'male' | 'female' | 'other' modified_at: Date marital_status: 'single' | 'married' | 'divorced' | 'widowed' | null + deleted_at: Date | null }>(r1) // Table with alias diff --git a/test/typings/test-d/select.test-d.ts b/test/typings/test-d/select.test-d.ts index f40057178..9216a62c4 100644 --- a/test/typings/test-d/select.test-d.ts +++ b/test/typings/test-d/select.test-d.ts @@ -132,6 +132,7 @@ async function testSelectAll(db: Kysely) { modified_at: Date owner_id: number species: 'dog' | 'cat' + deleted_at: Date | null }>(r2) // Select all from a single table when there are two tables to select from @@ -171,6 +172,7 @@ async function testSelectAll(db: Kysely) { modified_at: Date owner_id: number species: 'dog' | 'cat' + deleted_at: Date | null }>(r5) }