Skip to content

Commit 40dd28c

Browse files
authored
Merge pull request #36 from G4brym/add-upsert
Add upsert support
2 parents 244b556 + 0f66872 commit 40dd28c

File tree

16 files changed

+472
-153
lines changed

16 files changed

+472
-153
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Currently, 2 databases are supported:
2323
- [x] Create/drop tables
2424
- [x] [Insert/Bulk Inserts/Update/Select/Delete/Join queries](https://workers-qb.massadas.com/basic-queries/)
2525
- [x] [On Conflict for Inserts and Updates](https://workers-qb.massadas.com/advanced-queries/onConflict/)
26+
- [x] [Upsert](https://workers-qb.massadas.com/advanced-queries/upsert/)
2627
- [x] [Support for Cloudflare Workers D1](https://workers-qb.massadas.com/databases/cloudflare-d1/)
2728
- [x] [Support for Cloudflare Workers PostgreSQL (using node-postgres)](https://workers-qb.massadas.com/databases/postgresql/)
2829
- [ ] Named parameters (waiting for full support in D1)

docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ nav:
5050
- advanced-queries/limit.md
5151
- advanced-queries/returning.md
5252
- advanced-queries/onConflict.md
53+
- advanced-queries/upsert.md
5354
- advanced-queries/raw-sql.md
5455
markdown_extensions:
5556
- toc:
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
The Upsert feature in the SQL Builder Library streamlines database operations by combining insert and update actions
2+
into a single operation. It automatically determines whether a record exists based on a specified key and updates or
3+
inserts data accordingly. This simplifies coding, enhances data integrity, and boosts performance.
4+
5+
## Simple Upsert
6+
7+
`new Raw(...)` is used here to let `workers-qb` know that it is not a parameter.
8+
9+
```ts
10+
const qb = new D1QB(env.DB)
11+
12+
const upserted = await qb
13+
.insert({
14+
tableName: 'phonebook2',
15+
data: {
16+
name: 'Alice',
17+
phonenumber: '704-555-1212',
18+
validDate: '2018-05-08',
19+
},
20+
onConflict: {
21+
column: 'name',
22+
data: {
23+
phonenumber: new Raw('excluded.phonenumber'),
24+
validDate: new Raw('excluded.validDate'),
25+
},
26+
},
27+
})
28+
.execute()
29+
```
30+
31+
This will generate this query
32+
33+
```sql
34+
INSERT INTO phonebook2 (name, phonenumber, validDate)
35+
VALUES (?1, ?2, ?3)
36+
ON CONFLICT (name) DO UPDATE SET phonenumber = excluded.phonenumber,
37+
validDate = excluded.validDate
38+
```
39+
40+
## Upsert with where
41+
42+
```ts
43+
const qb = new D1QB(env.DB)
44+
45+
const upserted = await qb
46+
.insert({
47+
tableName: 'phonebook2',
48+
data: {
49+
name: 'Alice',
50+
phonenumber: '704-555-1212',
51+
validDate: '2018-05-08',
52+
},
53+
onConflict: {
54+
column: 'name',
55+
data: {
56+
phonenumber: new Raw('excluded.phonenumber'),
57+
validDate: new Raw('excluded.validDate'),
58+
},
59+
where: {
60+
conditions: 'excluded.validDate > phonebook2.validDate',
61+
},
62+
},
63+
})
64+
.execute()
65+
```
66+
67+
This will generate this query
68+
69+
```sql
70+
INSERT INTO phonebook2 (name, phonenumber, validDate)
71+
VALUES (?1, ?2, ?3)
72+
ON CONFLICT (name) DO UPDATE SET phonenumber = excluded.phonenumber,
73+
validDate = excluded.validDate
74+
WHERE excluded.validDate > phonebook2.validDate
75+
```
76+
77+
## Upsert with multiple columns
78+
79+
```ts
80+
const qb = new D1QB(env.DB)
81+
82+
const upserted = await qb
83+
.insert({
84+
tableName: 'phonebook2',
85+
data: {
86+
name: 'Alice',
87+
phonenumber: '704-555-1212',
88+
validDate: '2018-05-08',
89+
},
90+
onConflict: {
91+
column: ['name', 'phonenumber'],
92+
data: {
93+
validDate: new Raw('excluded.validDate'),
94+
},
95+
where: {
96+
conditions: 'excluded.validDate > phonebook2.validDate',
97+
},
98+
},
99+
})
100+
.execute()
101+
```
102+
103+
This will generate this query
104+
105+
```sql
106+
INSERT INTO phonebook2 (name, phonenumber, validDate)
107+
VALUES (?1, ?2, ?3)
108+
ON CONFLICT (name, phonenumber) DO UPDATE SET validDate = excluded.validDate
109+
WHERE excluded.validDate > phonebook2.validDate
110+
```

examples/postgresql/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/postgresql/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
},
1414
"dependencies": {
1515
"pg": "^8.11.0",
16-
"workers-qb": "^1.0.0"
16+
"workers-qb": "^1.0.2"
1717
}
1818
}

examples/postgresql/src/worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export interface Env {
66
}
77

88
export default {
9-
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
9+
fetch: async function (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
1010
const qb = new PGQB(new Client(env.DB_URL));
1111
await qb.connect();
1212

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module.exports = {
2-
roots: ['<rootDir>/test'],
2+
roots: ['<rootDir>/tests'],
33
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
44
transform: {
55
'^.+\\.(ts|tsx)$': 'ts-jest',

src/Builder.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Delete, Insert, Join, SelectAll, SelectOne, Update } from './interfaces'
1+
import { ConflictUpsert, Delete, Insert, Join, SelectAll, SelectOne, Update } from './interfaces'
22
import { ConflictTypes, FetchTypes, OrderTypes } from './enums'
33
import { Query, Raw } from './tools'
44

@@ -60,6 +60,19 @@ export class QueryBuilder<GenericResult, GenericResultOne> {
6060
insert(params: Insert): Query {
6161
let args: any[] = []
6262

63+
if (typeof params.onConflict === 'object') {
64+
if (params.onConflict.where?.params) {
65+
// 1 - on conflict where parameters
66+
args = args.concat(params.onConflict.where.params)
67+
}
68+
69+
if (params.onConflict.data) {
70+
// 2 - on conflict data parameters
71+
args = args.concat(this._parse_arguments(params.onConflict.data))
72+
}
73+
}
74+
75+
// 3 - insert data parameters
6376
if (Array.isArray(params.data)) {
6477
for (const row of params.data) {
6578
args = args.concat(this._parse_arguments(row))
@@ -116,8 +129,22 @@ export class QueryBuilder<GenericResult, GenericResultOne> {
116129
})
117130
}
118131

119-
_onConflict(resolution?: string | ConflictTypes): string {
132+
_onConflict(resolution?: string | ConflictTypes | ConflictUpsert): string {
120133
if (resolution) {
134+
if (typeof resolution === 'object') {
135+
if (!Array.isArray(resolution.column)) {
136+
resolution.column = [resolution.column]
137+
}
138+
139+
const _update_query = this.update({
140+
tableName: '_REPLACE_',
141+
data: resolution.data,
142+
where: resolution.where,
143+
}).query.replace(' _REPLACE_', '') // Replace here is to lint the query
144+
145+
return ` ON CONFLICT (${resolution.column.join(', ')}) DO ${_update_query}`
146+
}
147+
121148
return `OR ${resolution} `
122149
}
123150
return ''
@@ -131,8 +158,24 @@ export class QueryBuilder<GenericResult, GenericResultOne> {
131158
}
132159

133160
const columns = Object.keys(params.data[0]).join(', ')
134-
135161
let index = 1
162+
163+
let orConflict = '',
164+
onConflict = ''
165+
if (params.onConflict && typeof params.onConflict === 'object') {
166+
onConflict = this._onConflict(params.onConflict)
167+
168+
if (params.onConflict.where?.params) {
169+
index += params.onConflict.where?.params.length
170+
}
171+
172+
if (params.onConflict.data) {
173+
index += this._parse_arguments(params.onConflict.data).length
174+
}
175+
} else {
176+
orConflict = this._onConflict(params.onConflict)
177+
}
178+
136179
for (const row of params.data) {
137180
const values: Array<string> = []
138181
Object.values(row).forEach((value) => {
@@ -149,8 +192,9 @@ export class QueryBuilder<GenericResult, GenericResultOne> {
149192
}
150193

151194
return (
152-
`INSERT ${this._onConflict(params.onConflict)}INTO ${params.tableName} (${columns})` +
195+
`INSERT ${orConflict}INTO ${params.tableName} (${columns})` +
153196
` VALUES ${rows.join(', ')}` +
197+
onConflict +
154198
this._returning(params.returning)
155199
)
156200
}

src/interfaces.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,25 @@ export interface SelectAll extends SelectOne {
2929
limit?: number
3030
}
3131

32+
export interface ConflictUpsert {
33+
column: string | Array<string>
34+
data: Record<string, string | boolean | number | null | Raw>
35+
where?: Where
36+
}
37+
3238
export interface Insert {
3339
tableName: string
3440
data:
3541
| Record<string, string | boolean | number | null | Raw>
3642
| Array<Record<string, string | boolean | number | null | Raw>>
3743
returning?: string | Array<string>
38-
onConflict?: string | ConflictTypes
44+
onConflict?: string | ConflictTypes | ConflictUpsert
3945
}
4046

4147
export interface Update {
4248
tableName: string
4349
data: Record<string, string | boolean | number | null | Raw>
44-
where: Where
50+
where?: Where
4551
returning?: string | Array<string>
4652
onConflict?: string | ConflictTypes
4753
}

0 commit comments

Comments
 (0)