Skip to content

Commit

Permalink
SQLite's OR CONFLICT clause for inserts (#976)
Browse files Browse the repository at this point in the history
Co-authored-by: vincentiusvin <[email protected]>
Co-authored-by: Igal Klebanov <[email protected]>
  • Loading branch information
vincentiusvin and igalklebanov authored Jan 6, 2025
1 parent 5e5c463 commit 1f28e50
Show file tree
Hide file tree
Showing 12 changed files with 509 additions and 187 deletions.
6 changes: 6 additions & 0 deletions src/dialect/sqlite/sqlite-query-compiler.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { DefaultInsertValueNode } from '../../operation-node/default-insert-value-node.js'
import { OrActionNode } from '../../operation-node/or-action-node.js'
import { DefaultQueryCompiler } from '../../query-compiler/default-query-compiler.js'

const ID_WRAP_REGEX = /"/g

export class SqliteQueryCompiler extends DefaultQueryCompiler {
protected override visitOrAction(node: OrActionNode): void {
this.append('or ')
this.append(node.action)
}

protected override getCurrentParameterPlaceholder() {
return '?'
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export * from './operation-node/operation-node-transformer.js'
export * from './operation-node/operation-node-visitor.js'
export * from './operation-node/operation-node.js'
export * from './operation-node/operator-node.js'
export * from './operation-node/or-action-node.js'
export * from './operation-node/or-node.js'
export * from './operation-node/order-by-item-node.js'
export * from './operation-node/order-by-node.js'
Expand Down
4 changes: 4 additions & 0 deletions src/operation-node/insert-query-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ExplainNode } from './explain-node.js'
import { OnConflictNode } from './on-conflict-node.js'
import { OnDuplicateKeyNode } from './on-duplicate-key-node.js'
import { OperationNode } from './operation-node.js'
import { OrActionNode } from './or-action-node.js'
import { OutputNode } from './output-node.js'
import { ReturningNode } from './returning-node.js'
import { TableNode } from './table-node.js'
Expand All @@ -21,7 +22,10 @@ export interface InsertQueryNode extends OperationNode {
readonly onConflict?: OnConflictNode
readonly onDuplicateKey?: OnDuplicateKeyNode
readonly with?: WithNode
// TODO: remove in 0.29
/** @deprecated use {@link orAction} instead. */
readonly ignore?: boolean
readonly orAction?: OrActionNode
readonly replace?: boolean
readonly explain?: ExplainNode
readonly defaultValues?: boolean
Expand Down
8 changes: 8 additions & 0 deletions src/operation-node/operation-node-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import { FetchNode } from './fetch-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
import { RefreshMaterializedViewNode } from './refresh-materialized-view-node.js'
import { OrActionNode } from './or-action-node.js'

/**
* Transforms an operation node tree into another one.
Expand Down Expand Up @@ -231,6 +232,7 @@ export class OperationNodeTransformer {
FetchNode: this.transformFetch.bind(this),
TopNode: this.transformTop.bind(this),
OutputNode: this.transformOutput.bind(this),
OrActionNode: this.transformOrAction.bind(this),
})

transformNode<T extends OperationNode | undefined>(node: T): T {
Expand Down Expand Up @@ -392,6 +394,7 @@ export class OperationNodeTransformer {
endModifiers: this.transformNodeList(node.endModifiers),
with: this.transformNode(node.with),
ignore: node.ignore,
orAction: this.transformNode(node.orAction),
replace: node.replace,
explain: this.transformNode(node.explain),
defaultValues: node.defaultValues,
Expand Down Expand Up @@ -1131,4 +1134,9 @@ export class OperationNodeTransformer {
// An Object.freezed leaf node. No need to clone.
return node
}

protected transformOrAction(node: OrActionNode): OrActionNode {
// An Object.freezed leaf node. No need to clone.
return node
}
}
3 changes: 3 additions & 0 deletions src/operation-node/operation-node-visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import { FetchNode } from './fetch-node.js'
import { TopNode } from './top-node.js'
import { OutputNode } from './output-node.js'
import { RefreshMaterializedViewNode } from './refresh-materialized-view-node.js'
import { OrActionNode } from './or-action-node.js'

export abstract class OperationNodeVisitor {
protected readonly nodeStack: OperationNode[] = []
Expand Down Expand Up @@ -201,6 +202,7 @@ export abstract class OperationNodeVisitor {
FetchNode: this.visitFetch.bind(this),
TopNode: this.visitTop.bind(this),
OutputNode: this.visitOutput.bind(this),
OrActionNode: this.visitOrAction.bind(this),
})

protected readonly visitNode = (node: OperationNode): void => {
Expand Down Expand Up @@ -315,4 +317,5 @@ export abstract class OperationNodeVisitor {
protected abstract visitFetch(node: FetchNode): void
protected abstract visitTop(node: TopNode): void
protected abstract visitOutput(node: OutputNode): void
protected abstract visitOrAction(node: OrActionNode): void
}
1 change: 1 addition & 0 deletions src/operation-node/operation-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export type OperationNodeKind =
| 'FetchNode'
| 'TopNode'
| 'OutputNode'
| 'OrActionNode'

export interface OperationNode {
readonly kind: OperationNodeKind
Expand Down
23 changes: 23 additions & 0 deletions src/operation-node/or-action-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { freeze } from '../util/object-utils.js'
import { OperationNode } from './operation-node.js'

export interface OrActionNode extends OperationNode {
readonly kind: 'OrActionNode'
readonly action: string
}

/**
* @internal
*/
export const OrActionNode = freeze({
is(node: OperationNode): node is OrActionNode {
return node.kind === 'OrActionNode'
},

create(action: string): OrActionNode {
return freeze({
kind: 'OrActionNode',
action,
})
},
})
206 changes: 201 additions & 5 deletions src/query-builder/insert-query-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
SelectExpressionFromOutputCallback,
SelectExpressionFromOutputExpression,
} from './output-interface.js'
import { OrActionNode } from '../operation-node/or-action-node.js'

export class InsertQueryBuilder<DB, TB extends keyof DB, O>
implements
Expand Down Expand Up @@ -412,15 +413,18 @@ export class InsertQueryBuilder<DB, TB extends keyof DB, O>
/**
* Changes an `insert into` query to an `insert ignore into` query.
*
* This is only supported by some dialects like MySQL.
*
* To avoid a footgun, when invoked with the SQLite dialect, this method will
* be handled like {@link orIgnore}. See also, {@link orAbort}, {@link orFail},
* {@link orReplace}, and {@link orRollback}.
*
* If you use the ignore modifier, ignorable errors that occur while executing the
* insert statement are ignored. For example, without ignore, a row that duplicates
* an existing unique index or primary key value in the table causes a duplicate-key
* error and the statement is aborted. With ignore, the row is discarded and no error
* occurs.
*
* This is only supported on some dialects like MySQL. On most dialects you should
* use the {@link onConflict} method.
*
* ### Examples
*
* ```ts
Expand All @@ -437,14 +441,206 @@ export class InsertQueryBuilder<DB, TB extends keyof DB, O>
* The generated SQL (MySQL):
*
* ```sql
* insert ignore into `person` ("first_name", "last_name", "gender") values (?, ?, ?)
* insert ignore into `person` (`first_name`, `last_name`, `gender`) values (?, ?, ?)
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* insert or ignore into "person" ("first_name", "last_name", "gender") values (?, ?, ?)
* ```
*/
ignore(): InsertQueryBuilder<DB, TB, O> {
return new InsertQueryBuilder({
...this.#props,
queryNode: InsertQueryNode.cloneWith(this.#props.queryNode, {
ignore: true,
orAction: OrActionNode.create('ignore'),
}),
})
}

/**
* Changes an `insert into` query to an `insert or ignore into` query.
*
* This is only supported by some dialects like SQLite.
*
* To avoid a footgun, when invoked with the MySQL dialect, this method will
* be handled like {@link ignore}.
*
* See also, {@link orAbort}, {@link orFail}, {@link orReplace}, and {@link orRollback}.
*
* ### Examples
*
* ```ts
* await db.insertInto('person')
* .orIgnore()
* .values({
* first_name: 'John',
* last_name: 'Doe',
* gender: 'female',
* })
* .execute()
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* insert or ignore into "person" ("first_name", "last_name", "gender") values (?, ?, ?)
* ```
*
* The generated SQL (MySQL):
*
* ```sql
* insert ignore into `person` (`first_name`, `last_name`, `gender`) values (?, ?, ?)
* ```
*/
orIgnore(): InsertQueryBuilder<DB, TB, O> {
return new InsertQueryBuilder({
...this.#props,
queryNode: InsertQueryNode.cloneWith(this.#props.queryNode, {
orAction: OrActionNode.create('ignore'),
}),
})
}

/**
* Changes an `insert into` query to an `insert or abort into` query.
*
* This is only supported by some dialects like SQLite.
*
* See also, {@link orIgnore}, {@link orFail}, {@link orReplace}, and {@link orRollback}.
*
* ### Examples
*
* ```ts
* await db.insertInto('person')
* .orAbort()
* .values({
* first_name: 'John',
* last_name: 'Doe',
* gender: 'female',
* })
* .execute()
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* insert or abort into "person" ("first_name", "last_name", "gender") values (?, ?, ?)
* ```
*/
orAbort(): InsertQueryBuilder<DB, TB, O> {
return new InsertQueryBuilder({
...this.#props,
queryNode: InsertQueryNode.cloneWith(this.#props.queryNode, {
orAction: OrActionNode.create('abort'),
}),
})
}

/**
* Changes an `insert into` query to an `insert or fail into` query.
*
* This is only supported by some dialects like SQLite.
*
* See also, {@link orIgnore}, {@link orAbort}, {@link orReplace}, and {@link orRollback}.
*
* ### Examples
*
* ```ts
* await db.insertInto('person')
* .orFail()
* .values({
* first_name: 'John',
* last_name: 'Doe',
* gender: 'female',
* })
* .execute()
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* insert or fail into "person" ("first_name", "last_name", "gender") values (?, ?, ?)
* ```
*/
orFail(): InsertQueryBuilder<DB, TB, O> {
return new InsertQueryBuilder({
...this.#props,
queryNode: InsertQueryNode.cloneWith(this.#props.queryNode, {
orAction: OrActionNode.create('fail'),
}),
})
}

/**
* Changes an `insert into` query to an `insert or replace into` query.
*
* This is only supported by some dialects like SQLite.
*
* You can also use {@link Kysely.replaceInto} to achieve the same result.
*
* See also, {@link orIgnore}, {@link orAbort}, {@link orFail}, and {@link orRollback}.
*
* ### Examples
*
* ```ts
* await db.insertInto('person')
* .orReplace()
* .values({
* first_name: 'John',
* last_name: 'Doe',
* gender: 'female',
* })
* .execute()
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* insert or replace into "person" ("first_name", "last_name", "gender") values (?, ?, ?)
* ```
*/
orReplace(): InsertQueryBuilder<DB, TB, O> {
return new InsertQueryBuilder({
...this.#props,
queryNode: InsertQueryNode.cloneWith(this.#props.queryNode, {
orAction: OrActionNode.create('replace'),
}),
})
}

/**
* Changes an `insert into` query to an `insert or rollback into` query.
*
* This is only supported by some dialects like SQLite.
*
* See also, {@link orIgnore}, {@link orAbort}, {@link orFail}, and {@link orReplace}.
*
* ### Examples
*
* ```ts
* await db.insertInto('person')
* .orRollback()
* .values({
* first_name: 'John',
* last_name: 'Doe',
* gender: 'female',
* })
* .execute()
* ```
*
* The generated SQL (SQLite):
*
* ```sql
* insert or rollback into "person" ("first_name", "last_name", "gender") values (?, ?, ?)
* ```
*/
orRollback(): InsertQueryBuilder<DB, TB, O> {
return new InsertQueryBuilder({
...this.#props,
queryNode: InsertQueryNode.cloneWith(this.#props.queryNode, {
orAction: OrActionNode.create('rollback'),
}),
})
}
Expand Down
Loading

0 comments on commit 1f28e50

Please sign in to comment.