Skip to content

Commit

Permalink
Merge pull request #36 from G4brym/add-upsert
Browse files Browse the repository at this point in the history
Add upsert support
  • Loading branch information
G4brym authored Jul 25, 2023
2 parents 244b556 + 0f66872 commit 40dd28c
Show file tree
Hide file tree
Showing 16 changed files with 472 additions and 153 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Currently, 2 databases are supported:
- [x] Create/drop tables
- [x] [Insert/Bulk Inserts/Update/Select/Delete/Join queries](https://workers-qb.massadas.com/basic-queries/)
- [x] [On Conflict for Inserts and Updates](https://workers-qb.massadas.com/advanced-queries/onConflict/)
- [x] [Upsert](https://workers-qb.massadas.com/advanced-queries/upsert/)
- [x] [Support for Cloudflare Workers D1](https://workers-qb.massadas.com/databases/cloudflare-d1/)
- [x] [Support for Cloudflare Workers PostgreSQL (using node-postgres)](https://workers-qb.massadas.com/databases/postgresql/)
- [ ] Named parameters (waiting for full support in D1)
Expand Down
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ nav:
- advanced-queries/limit.md
- advanced-queries/returning.md
- advanced-queries/onConflict.md
- advanced-queries/upsert.md
- advanced-queries/raw-sql.md
markdown_extensions:
- toc:
Expand Down
110 changes: 110 additions & 0 deletions docs/pages/advanced-queries/upsert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
The Upsert feature in the SQL Builder Library streamlines database operations by combining insert and update actions
into a single operation. It automatically determines whether a record exists based on a specified key and updates or
inserts data accordingly. This simplifies coding, enhances data integrity, and boosts performance.

## Simple Upsert

`new Raw(...)` is used here to let `workers-qb` know that it is not a parameter.

```ts
const qb = new D1QB(env.DB)

const upserted = await qb
.insert({
tableName: 'phonebook2',
data: {
name: 'Alice',
phonenumber: '704-555-1212',
validDate: '2018-05-08',
},
onConflict: {
column: 'name',
data: {
phonenumber: new Raw('excluded.phonenumber'),
validDate: new Raw('excluded.validDate'),
},
},
})
.execute()
```

This will generate this query

```sql
INSERT INTO phonebook2 (name, phonenumber, validDate)
VALUES (?1, ?2, ?3)
ON CONFLICT (name) DO UPDATE SET phonenumber = excluded.phonenumber,
validDate = excluded.validDate
```

## Upsert with where

```ts
const qb = new D1QB(env.DB)

const upserted = await qb
.insert({
tableName: 'phonebook2',
data: {
name: 'Alice',
phonenumber: '704-555-1212',
validDate: '2018-05-08',
},
onConflict: {
column: 'name',
data: {
phonenumber: new Raw('excluded.phonenumber'),
validDate: new Raw('excluded.validDate'),
},
where: {
conditions: 'excluded.validDate > phonebook2.validDate',
},
},
})
.execute()
```

This will generate this query

```sql
INSERT INTO phonebook2 (name, phonenumber, validDate)
VALUES (?1, ?2, ?3)
ON CONFLICT (name) DO UPDATE SET phonenumber = excluded.phonenumber,
validDate = excluded.validDate
WHERE excluded.validDate > phonebook2.validDate
```

## Upsert with multiple columns

```ts
const qb = new D1QB(env.DB)

const upserted = await qb
.insert({
tableName: 'phonebook2',
data: {
name: 'Alice',
phonenumber: '704-555-1212',
validDate: '2018-05-08',
},
onConflict: {
column: ['name', 'phonenumber'],
data: {
validDate: new Raw('excluded.validDate'),
},
where: {
conditions: 'excluded.validDate > phonebook2.validDate',
},
},
})
.execute()
```

This will generate this query

```sql
INSERT INTO phonebook2 (name, phonenumber, validDate)
VALUES (?1, ?2, ?3)
ON CONFLICT (name, phonenumber) DO UPDATE SET validDate = excluded.validDate
WHERE excluded.validDate > phonebook2.validDate
```
8 changes: 4 additions & 4 deletions examples/postgresql/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/postgresql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
},
"dependencies": {
"pg": "^8.11.0",
"workers-qb": "^1.0.0"
"workers-qb": "^1.0.2"
}
}
2 changes: 1 addition & 1 deletion examples/postgresql/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface Env {
}

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

Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module.exports = {
roots: ['<rootDir>/test'],
roots: ['<rootDir>/tests'],
testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
Expand Down
52 changes: 48 additions & 4 deletions src/Builder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Delete, Insert, Join, SelectAll, SelectOne, Update } from './interfaces'
import { ConflictUpsert, Delete, Insert, Join, SelectAll, SelectOne, Update } from './interfaces'
import { ConflictTypes, FetchTypes, OrderTypes } from './enums'
import { Query, Raw } from './tools'

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

if (typeof params.onConflict === 'object') {
if (params.onConflict.where?.params) {
// 1 - on conflict where parameters
args = args.concat(params.onConflict.where.params)
}

if (params.onConflict.data) {
// 2 - on conflict data parameters
args = args.concat(this._parse_arguments(params.onConflict.data))
}
}

// 3 - insert data parameters
if (Array.isArray(params.data)) {
for (const row of params.data) {
args = args.concat(this._parse_arguments(row))
Expand Down Expand Up @@ -116,8 +129,22 @@ export class QueryBuilder<GenericResult, GenericResultOne> {
})
}

_onConflict(resolution?: string | ConflictTypes): string {
_onConflict(resolution?: string | ConflictTypes | ConflictUpsert): string {
if (resolution) {
if (typeof resolution === 'object') {
if (!Array.isArray(resolution.column)) {
resolution.column = [resolution.column]
}

const _update_query = this.update({
tableName: '_REPLACE_',
data: resolution.data,
where: resolution.where,
}).query.replace(' _REPLACE_', '') // Replace here is to lint the query

return ` ON CONFLICT (${resolution.column.join(', ')}) DO ${_update_query}`
}

return `OR ${resolution} `
}
return ''
Expand All @@ -131,8 +158,24 @@ export class QueryBuilder<GenericResult, GenericResultOne> {
}

const columns = Object.keys(params.data[0]).join(', ')

let index = 1

let orConflict = '',
onConflict = ''
if (params.onConflict && typeof params.onConflict === 'object') {
onConflict = this._onConflict(params.onConflict)

if (params.onConflict.where?.params) {
index += params.onConflict.where?.params.length
}

if (params.onConflict.data) {
index += this._parse_arguments(params.onConflict.data).length
}
} else {
orConflict = this._onConflict(params.onConflict)
}

for (const row of params.data) {
const values: Array<string> = []
Object.values(row).forEach((value) => {
Expand All @@ -149,8 +192,9 @@ export class QueryBuilder<GenericResult, GenericResultOne> {
}

return (
`INSERT ${this._onConflict(params.onConflict)}INTO ${params.tableName} (${columns})` +
`INSERT ${orConflict}INTO ${params.tableName} (${columns})` +
` VALUES ${rows.join(', ')}` +
onConflict +
this._returning(params.returning)
)
}
Expand Down
10 changes: 8 additions & 2 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,25 @@ export interface SelectAll extends SelectOne {
limit?: number
}

export interface ConflictUpsert {
column: string | Array<string>
data: Record<string, string | boolean | number | null | Raw>
where?: Where
}

export interface Insert {
tableName: string
data:
| Record<string, string | boolean | number | null | Raw>
| Array<Record<string, string | boolean | number | null | Raw>>
returning?: string | Array<string>
onConflict?: string | ConflictTypes
onConflict?: string | ConflictTypes | ConflictUpsert
}

export interface Update {
tableName: string
data: Record<string, string | boolean | number | null | Raw>
where: Where
where?: Where
returning?: string | Array<string>
onConflict?: string | ConflictTypes
}
Expand Down
Loading

0 comments on commit 40dd28c

Please sign in to comment.