Skip to content

Commit

Permalink
Fixed SQLite onConflict clauses being overwritten instead of stacked,…
Browse files Browse the repository at this point in the history
… added related tests, removed unused import
  • Loading branch information
Sukairo-02 committed Jan 28, 2025
1 parent f36e3ea commit 348e496
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 7 deletions.
2 changes: 1 addition & 1 deletion drizzle-orm/src/sqlite-core/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { SQLiteCountBuilder } from './query-builders/count.ts';
import { RelationalQueryBuilder } from './query-builders/query.ts';
import { SQLiteRaw } from './query-builders/raw.ts';
import type { SelectedFields } from './query-builders/select.types.ts';
import type { WithBuilder, WithSubqueryWithSelection } from './subquery.ts';
import type { WithBuilder } from './subquery.ts';
import type { SQLiteViewBase } from './view-base.ts';

export class BaseSQLiteDatabase<
Expand Down
4 changes: 3 additions & 1 deletion drizzle-orm/src/sqlite-core/dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,9 @@ export abstract class SQLiteDialect {
? sql` returning ${this.buildSelection(returning, { isSingleTable: true })}`
: undefined;

const onConflictSql = onConflict ? sql` on conflict ${onConflict}` : undefined;
const onConflictSql = onConflict?.length
? sql.join(onConflict)
: undefined;

// if (isSingleValue && valuesSqlList.length === 0){
// return sql`insert into ${table} default values ${onConflictSql}${returningSql}`;
Expand Down
15 changes: 11 additions & 4 deletions drizzle-orm/src/sqlite-core/query-builders/insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface SQLiteInsertConfig<TTable extends SQLiteTable = SQLiteTable> {
table: TTable;
values: Record<string, Param | SQL>[] | SQLiteInsertSelectQueryBuilder<TTable> | SQL;
withList?: Subquery[];
onConflict?: SQL;
onConflict?: SQL[];
returning?: SelectedFieldsOrdered;
select?: boolean;
}
Expand Down Expand Up @@ -303,12 +303,14 @@ export class SQLiteInsertBase<
* ```
*/
onConflictDoNothing(config: { target?: IndexColumn | IndexColumn[]; where?: SQL } = {}): this {
if (!this.config.onConflict) this.config.onConflict = [];

if (config.target === undefined) {
this.config.onConflict = sql`do nothing`;
this.config.onConflict.push(sql` on conflict do nothing`);
} else {
const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`;
const whereSql = config.where ? sql` where ${config.where}` : sql``;
this.config.onConflict = sql`${targetSql} do nothing${whereSql}`;
this.config.onConflict.push(sql` on conflict ${targetSql} do nothing${whereSql}`);
}
return this;
}
Expand Down Expand Up @@ -348,12 +350,17 @@ export class SQLiteInsertBase<
'You cannot use both "where" and "targetWhere"/"setWhere" at the same time - "where" is deprecated, use "targetWhere" or "setWhere" instead.',
);
}

if (!this.config.onConflict) this.config.onConflict = [];

const whereSql = config.where ? sql` where ${config.where}` : undefined;
const targetWhereSql = config.targetWhere ? sql` where ${config.targetWhere}` : undefined;
const setWhereSql = config.setWhere ? sql` where ${config.setWhere}` : undefined;
const targetSql = Array.isArray(config.target) ? sql`${config.target}` : sql`${[config.target]}`;
const setSql = this.dialect.buildUpdateSet(this.config.table, mapUpdateSet(this.config.table, config.set));
this.config.onConflict = sql`${targetSql}${targetWhereSql} do update set ${setSql}${whereSql}${setWhereSql}`;
this.config.onConflict.push(
sql` on conflict ${targetSql}${targetWhereSql} do update set ${setSql}${whereSql}${setWhereSql}`,
);
return this;
}

Expand Down
2 changes: 1 addition & 1 deletion integration-tests/tests/sqlite/better-sqlite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
import { skipTests } from '~/common';
import { anotherUsersMigratorTable, tests, usersMigratorTable } from './sqlite-common';

const ENABLE_LOGGING = false;
const ENABLE_LOGGING = true;

let db: BetterSQLite3Database;
let client: Database.Database;
Expand Down
176 changes: 176 additions & 0 deletions integration-tests/tests/sqlite/sqlite-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ const pkExampleTable = sqliteTable('pk_example', {
compositePk: primaryKey({ columns: [table.id, table.name] }),
}));

const conflictChainExampleTable = sqliteTable('conflict_chain_example', {
id: integer('id').notNull().unique(),
name: text('name').notNull(),
email: text('email').notNull(),
}, (table) => ({
compositePk: primaryKey({ columns: [table.id, table.name] }),
}));

const bigIntExample = sqliteTable('big_int_example', {
id: integer('id').primaryKey(),
name: text('name').notNull(),
Expand Down Expand Up @@ -154,6 +162,7 @@ export function tests() {
await db.run(sql`drop table if exists ${orders}`);
await db.run(sql`drop table if exists ${bigIntExample}`);
await db.run(sql`drop table if exists ${pkExampleTable}`);
await db.run(sql`drop table if exists ${conflictChainExampleTable}`);
await db.run(sql`drop table if exists user_notifications_insert_into`);
await db.run(sql`drop table if exists users_insert_into`);
await db.run(sql`drop table if exists notifications_insert_into`);
Expand Down Expand Up @@ -212,6 +221,14 @@ export function tests() {
primary key (id, name)
)
`);
await db.run(sql`
create table ${conflictChainExampleTable} (
id integer not null unique,
name text not null,
email text not null,
primary key (id, name)
)
`);
await db.run(sql`
create table ${bigIntExample} (
id integer primary key,
Expand Down Expand Up @@ -2037,6 +2054,165 @@ export function tests() {
expect(res).toEqual([{ id: 1, name: 'John', email: '[email protected]' }]);
});

test('insert with onConflict chained (.update -> .nothing)', async (ctx) => {
const { db } = ctx.sqlite;

await db.insert(conflictChainExampleTable).values([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'John Second',
email: '[email protected]',
}]).run();

await db
.insert(conflictChainExampleTable)
.values([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'Anthony',
email: '[email protected]',
}])
.onConflictDoUpdate({
target: [conflictChainExampleTable.id, conflictChainExampleTable.name],
set: { email: '[email protected]' },
})
.onConflictDoNothing({ target: conflictChainExampleTable.id })
.run();

const res = await db
.select({
id: conflictChainExampleTable.id,
name: conflictChainExampleTable.name,
email: conflictChainExampleTable.email,
})
.from(conflictChainExampleTable)
.orderBy(conflictChainExampleTable.id)
.all();

expect(res).toEqual([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'John Second',
email: '[email protected]',
}]);
});

test('insert with onConflict chained (.nothing -> .update)', async (ctx) => {
const { db } = ctx.sqlite;

await db.insert(conflictChainExampleTable).values([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'John Second',
email: '[email protected]',
}]).run();

await db
.insert(conflictChainExampleTable)
.values([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'Anthony',
email: '[email protected]',
}])
.onConflictDoUpdate({
target: [conflictChainExampleTable.id, conflictChainExampleTable.name],
set: { email: '[email protected]' },
})
.onConflictDoNothing({ target: conflictChainExampleTable.id })
.run();

const res = await db
.select({
id: conflictChainExampleTable.id,
name: conflictChainExampleTable.name,
email: conflictChainExampleTable.email,
})
.from(conflictChainExampleTable)
.orderBy(conflictChainExampleTable.id)
.all();

expect(res).toEqual([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'John Second',
email: '[email protected]',
}]);
});

test('insert with onConflict chained (.update -> .update)', async (ctx) => {
const { db } = ctx.sqlite;

await db.insert(conflictChainExampleTable).values([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'John Second',
email: '[email protected]',
}]).run();

await db
.insert(conflictChainExampleTable)
.values([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'Anthony',
email: '[email protected]',
}])
.onConflictDoUpdate({
target: [conflictChainExampleTable.id, conflictChainExampleTable.name],
set: { email: '[email protected]' },
})
.onConflictDoUpdate({ target: conflictChainExampleTable.id, set: { email: '[email protected]' } })
.run();

const res = await db
.select({
id: conflictChainExampleTable.id,
name: conflictChainExampleTable.name,
email: conflictChainExampleTable.email,
})
.from(conflictChainExampleTable)
.orderBy(conflictChainExampleTable.id)
.all();

expect(res).toEqual([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'John Second',
email: '[email protected]',
}]);
});

test('insert with onConflict chained (.nothing -> .nothing)', async (ctx) => {
const { db } = ctx.sqlite;

await db.insert(conflictChainExampleTable).values([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'John Second',
email: '[email protected]',
}]).run();

await db
.insert(conflictChainExampleTable)
.values([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'Anthony',
email: '[email protected]',
}])
.onConflictDoNothing({
target: [conflictChainExampleTable.id, conflictChainExampleTable.name],
})
.onConflictDoNothing({ target: conflictChainExampleTable.id })
.run();

const res = await db
.select({
id: conflictChainExampleTable.id,
name: conflictChainExampleTable.name,
email: conflictChainExampleTable.email,
})
.from(conflictChainExampleTable)
.orderBy(conflictChainExampleTable.id)
.all();

expect(res).toEqual([{ id: 1, name: 'John', email: '[email protected]' }, {
id: 2,
name: 'John Second',
email: '[email protected]',
}]);
});

test('insert undefined', async (ctx) => {
const { db } = ctx.sqlite;

Expand Down

0 comments on commit 348e496

Please sign in to comment.