Skip to content

Commit c1b7f45

Browse files
committed
feat: support on conflict for insert stmt in pg
1 parent 6a4e366 commit c1b7f45

3 files changed

Lines changed: 125 additions & 3 deletions

File tree

pegjs/postgresql.pegjs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2414,8 +2414,6 @@ set_item
24142414
// => { column: ident; value: column_ref; table?: ident; keyword: 'values' }
24152415
return { column: c, value: v, table: tbl && tbl[0], keyword: 'values' };
24162416
}
2417-
conflict_stmt
2418-
= KW_ON __ 'CONFLICT'i __
24192417

24202418
returning_stmt
24212419
= k:KW_RETURNING __ c:(column_clause / select_stmt) {
@@ -2440,18 +2438,64 @@ insert_partition
24402438
return v
24412439
}
24422440

2441+
conflict_target
2442+
= LPAREN __ c:column_ref_list __ RPAREN {
2443+
// => { type: 'column'; expr: column_ref_list; parentheses: true; }
2444+
return {
2445+
type: 'column',
2446+
expr: c,
2447+
parentheses: true,
2448+
}
2449+
}
2450+
2451+
conflict_action
2452+
= 'DO'i __ 'NOTHING'i {
2453+
// => { keyword: "do"; expr: {type: 'origin'; value: string; }; }
2454+
return {
2455+
keyword: 'do',
2456+
expr: {
2457+
type: 'origin',
2458+
value: 'nothing'
2459+
}
2460+
}
2461+
}
2462+
/ 'DO'i __ KW_UPDATE __ KW_SET __ s:set_list __ w:where_clause? {
2463+
// => { keyword: "do"; expr: {type: 'update'; set: set_list; where: where_clause; }; }
2464+
return {
2465+
keyword: 'do',
2466+
expr: {
2467+
type: 'update',
2468+
set: s,
2469+
where: w,
2470+
}
2471+
}
2472+
}
2473+
2474+
on_conflict
2475+
= KW_ON __ 'CONFLICT'i __ ct:conflict_target? __ ca:conflict_action {
2476+
// => { type: "conflict"; keyword: "on"; target: conflict_target; action: conflict_action; }
2477+
return {
2478+
type: 'conflict',
2479+
keyword: 'on',
2480+
target: ct,
2481+
action: ca,
2482+
}
2483+
}
2484+
24432485
replace_insert_stmt
24442486
= ri:replace_insert __
24452487
KW_INTO? __
24462488
t:table_name __
24472489
p:insert_partition? __ LPAREN __ c:column_list __ RPAREN __
24482490
v:insert_value_clause __
2491+
oc:on_conflict? __
24492492
r:returning_stmt? {
24502493
/*
24512494
export interface replace_insert_stmt_node {
24522495
type: 'insert' | 'replace';
24532496
table?: [table_name];
24542497
columns: column_list;
2498+
conflict?: on_clifict;
24552499
values: insert_value_clause;
24562500
partition?: insert_partition;
24572501
returning?: returning_stmt;
@@ -2482,6 +2526,7 @@ replace_insert_stmt
24822526
columns: c,
24832527
values: v,
24842528
partition: p,
2529+
conflict: oc,
24852530
returning: r,
24862531
}
24872532
};

src/insert.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { tablesToSQL } from './tables'
22
import { exprToSQL } from './expr'
3-
import { identifierToSql, commonOptionConnector, hasVal, toUpper, returningToSQL } from './util'
3+
import { columnRefToSQL } from './column'
4+
import { identifierToSql, commonOptionConnector, hasVal, toUpper, returningToSQL, literalToSQL } from './util'
45
import { selectToSQL } from './select'
56
import { setToSQL } from './update'
67

@@ -27,12 +28,44 @@ function partitionToSQL(partition) {
2728
return partitionArr.filter(hasVal).join('')
2829
}
2930

31+
function conflictTargetToSQL(conflictTarget) {
32+
if (!conflictTarget) return ''
33+
const { type } = conflictTarget
34+
switch (type) {
35+
case 'column':
36+
return `(${conflictTarget.expr.map(columnRefToSQL).join(', ')})`
37+
}
38+
}
39+
40+
function conflictActionToSQL(conflictAction) {
41+
const { expr, keyword } = conflictAction
42+
const { type } = expr
43+
const result = [toUpper(keyword)]
44+
switch (type) {
45+
case 'origin':
46+
result.push(literalToSQL(expr))
47+
break
48+
case 'update':
49+
result.push('UPDATE', commonOptionConnector('SET', setToSQL, expr.set), commonOptionConnector('WHERE', exprToSQL, expr.where))
50+
break
51+
}
52+
return result.filter(hasVal).join(' ')
53+
}
54+
55+
function conflictToSQL(conflict) {
56+
if (!conflict) return ''
57+
const { action, target } = conflict
58+
const result = [conflictTargetToSQL(target), conflictActionToSQL(action)]
59+
return result.filter(hasVal).join(' ')
60+
}
61+
3062
function insertToSQL(stmt) {
3163
const {
3264
table,
3365
type,
3466
prefix = 'into',
3567
columns,
68+
conflict,
3669
values,
3770
where,
3871
on_duplicate_update: onDuplicateUpdate,
@@ -44,6 +77,7 @@ function insertToSQL(stmt) {
4477
const clauses = [toUpper(type), toUpper(prefix), tablesToSQL(table), partitionToSQL(partition)]
4578
if (Array.isArray(columns)) clauses.push(`(${columns.map(identifierToSql).join(', ')})`)
4679
clauses.push(commonOptionConnector(Array.isArray(values) ? 'VALUES' : '', valuesToSQL, values))
80+
clauses.push(commonOptionConnector('ON CONFLICT', conflictToSQL, conflict))
4781
clauses.push(commonOptionConnector('SET', setToSQL, set))
4882
clauses.push(commonOptionConnector('WHERE', exprToSQL, where))
4983
clauses.push(returningToSQL(returning))
@@ -52,6 +86,7 @@ function insertToSQL(stmt) {
5286
}
5387

5488
export {
89+
conflictToSQL,
5590
insertToSQL,
5691
valuesToSQL,
5792
}

test/postgres.spec.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const { expect } = require('chai')
2+
const { conflictToSQL } = require('../src/insert')
23
const Parser = require('../src/parser').default
34

45
describe('Postgres', () => {
@@ -922,6 +923,42 @@ describe('Postgres', () => {
922923
`SELECT * FROM partitions WHERE code ~ xyz;`,
923924
`SELECT * FROM "partitions" WHERE "code" ~ "xyz"`
924925
]
926+
},
927+
{
928+
title: 'insert stmt',
929+
sql: [
930+
`insert into table1 (id, firstname, lastname, email)
931+
values ($id, $firstname, $lastname, $email)
932+
RETURNING *`,
933+
'INSERT INTO "table1" ("id", "firstname", "lastname", "email") VALUES ($id,$firstname,$lastname,$email) RETURNING *'
934+
]
935+
},
936+
{
937+
title: 'insert with on conflict do update',
938+
sql: [
939+
`insert into table1 (id, firstname, lastname, email)
940+
values ($id, $firstname, $lastname, $email)
941+
on conflict (id)
942+
do
943+
update set
944+
firstname = $firstname,
945+
lastname = $lastname,
946+
email = $email,
947+
updatedon = CURRENT_TIMESTAMP
948+
RETURNING *`,
949+
'INSERT INTO "table1" ("id", "firstname", "lastname", "email") VALUES ($id,$firstname,$lastname,$email) ON CONFLICT ("id") DO UPDATE SET "firstname" = $firstname, "lastname" = $lastname, "email" = $email, "updatedon" = CURRENT_TIMESTAMP RETURNING *'
950+
]
951+
},
952+
{
953+
title: 'insert with on conflict do nothing',
954+
sql: [
955+
`insert into table1 (id, firstname, lastname, email)
956+
values ($id, $firstname, $lastname, $email)
957+
on conflict
958+
do nothing
959+
RETURNING *`,
960+
'INSERT INTO "table1" ("id", "firstname", "lastname", "email") VALUES ($id,$firstname,$lastname,$email) ON CONFLICT DO NOTHING RETURNING *'
961+
]
925962
}
926963
]
927964
function neatlyNestTestedSQL(sqlList){
@@ -1100,5 +1137,10 @@ describe('Postgres', () => {
11001137
expect(ast.columnList).to.be.eql(['select::null::Id'])
11011138
expect(parser.sqlify(ast.ast, opt)).to.be.equals(sql.slice(0, -1))
11021139
})
1140+
1141+
it('should support conflict be empty', () => {
1142+
expect(conflictToSQL(null)).to.be.equal('')
1143+
})
11031144
})
1145+
11041146
})

0 commit comments

Comments
 (0)