Skip to content

Commit

Permalink
V3.2.3 (#54)
Browse files Browse the repository at this point in the history
INSERT ON Conflict resolution #42
  • Loading branch information
lroal authored Oct 10, 2023
1 parent a538fd0 commit 90d69fb
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 70 deletions.
79 changes: 77 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ const db = map.pg(`Driver=${__dirname}/libsybdrvodb.so;SERVER=sapase;Port=5000;U
</details>
<details><summary><strong>Inserting rows</strong></summary>
<details id="inserting-rows"><summary><strong>Inserting rows</strong></summary>
In the code below, we initially import the table-mapping feature "map.js" and the setup script "init.js", both of which were defined in the preceding step. The setup script executes a raw query that creates the necessary tables. Subsequently, we insert two customers, named "George" and "Harry", into the customer table, and this is achieved through calling "db.customer.insert".
Expand Down Expand Up @@ -400,6 +400,59 @@ async function insertRows() {
}
```
__Conflict resolution__
By default, the strategy for inserting rows is set to an optimistic approach. In this case, if a row is being inserted with an already existing primary key, the database raises an exception.
Currently, there are three concurrency strategies:
- <strong>`optimistic`</strong> Raises an exception if another row was already inserted on that primary key.
- <strong>`overwrite`</strong> Overwrites the property, regardless of changes by others.
- <strong>`skipOnConflict`</strong> Silently avoids updating the property if another user has modified it in the interim.
The <strong>concurrency</strong> option can be set either globally a table or individually for each column. In the example below, we've set the concurrency strategy on <strong>vendor</strong> table to <strong>overwrite</strong> except for the column <strong>balance</strong> which uses the <strong>skipOnConflict</strong> strategy.. In this particular case, a row with id: 1 already exists, the name and isActive fields will be overwritten, but the balance will remain the same as in the original record, demonstrating the effectiveness of combining multiple <strong>concurrency</strong> strategies.
```javascript
import map from './map';
const db = map.sqlite('demo.db');
import init from './init';

insertRows();

async function insertRows() {
await init();

db2 = db({
vendor: {
balance: {
concurrency: 'skipOnConflict'
},
concurrency: 'overwrite'
}
});

await db2.vendor.insert({
id: 1,
name: 'John',
balance: 100,
isActive: true
});

//this will overwrite all fields but balance
const george = await db2.vendor.insert({
id: 1,
name: 'George',
balance: 177,
isActive: false
});
console.dir(orders, {depth: Infinity});
// {
// id: 1,
// name: 'George',
// balance: 100,
// isActive: false
// }
}
```
</details>
<details><summary><strong>Fetching rows</strong></summary>
Expand Down Expand Up @@ -589,7 +642,7 @@ async function update() {
}
```
__Updating with concurrency__
__Conflict resolution__
Rows get updated using an <i>optimistic</i> concurrency approach by default. This means if a property being edited was meanwhile altered, an exception is raised, indicating the row was modified by a different user. You can change the concurrency strategy either at the table or column level.
Currently, there are three concurrency strategies:
Expand Down Expand Up @@ -950,6 +1003,28 @@ const map = rdb.map(x => ({
```
</details>
<details><summary><strong>Default values</strong></summary>
Utilizing default values can be especially useful for automatically populating these fields when the underlying database doesn't offer native support for default value generation.
In the provided code, the id column's default value is set to a UUID generated by crypto.randomUUID(), and the isActive column's default is set to true.
```javascript
import rdb from 'rdb';
import crypto 'crypto';

const map = rdb.map(x => ({
myTable: x.table('myTable').map(({ column }) => ({
id: column('id').uuid().primary().default(() => crypto.randomUUID()),
name: column('name').string(),
balance: column('balance').numeric(),
isActive: column('isActive').boolean().default(true),
}))
}));

module.exports = map;
```
</details>
<details><summary><strong>Validation</strong></summary>
In the previous sections you have already seen the <strong><i>notNull()</i></strong> validator being used on some columns. This will not only generate correct typescript mapping, but also throw an error if value is set to null or undefined. However, sometimes we do not want the notNull-validator to be run on inserts. Typically, when we have an autoincremental key or server generated uuid, it does not make sense to check for null on insert. This is where <strong><i>notNullExceptInsert()</strong></i> comes to rescue. You can also create your own custom validator as shown below. The last kind of validator, is the <a href="https://ajv.js.org/json-schema.html">ajv JSON schema validator</a>. This can be used on json columns as well as any other column type.
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@

## Changelog
__3.2.3__
Conflict resolution on insert. See [#42](https://github.com/alfateam/rdb/issues/42) and [Inserting rows](https://github.com/alfateam/rdb/tree/master#user-content-inserting-rows).
__3.2.2__
Bugfix for Sql Server: OFFSET was ignored. See [#46](https://github.com/alfateam/rdb/issues/49).
__3.2.1__
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rdb",
"version": "3.2.2",
"version": "3.2.3",
"main": "./src/index.js",
"browser": "./src/client/index.mjs",
"bin": {
Expand Down
30 changes: 20 additions & 10 deletions src/client/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5532,16 +5532,6 @@ function rdbClient(options = {}) {
return adapter.post(body);
}

async function insertAndForget() {
let args = Array.prototype.slice.call(arguments);
let body = stringify({
path: 'insertAndForget',
args
});
let adapter = netAdapter(url, tableName, { axios: axiosInterceptor, tableOptions });
return adapter.post(body);
}

async function _delete() {
let args = Array.prototype.slice.call(arguments);
let body = stringify({
Expand Down Expand Up @@ -5577,6 +5567,21 @@ function rdbClient(options = {}) {
}
}

async function insertAndForget(rows, arg1, arg2, ...rest) {
let args = [arg1, {insertAndForget: true, ...arg2}].concat(rest);
if (Array.isArray(rows)) {
let proxy = proxify([], args[0]);
proxy.splice.apply(proxy, [0, 0, ...rows]);
await proxy.saveChanges.apply(proxy, args);
}
else {
let proxy = proxify([], args[0]);
proxy.splice.apply(proxy, [0, 0, rows]);
await proxy.saveChanges.apply(proxy, args);
}
}


function proxify(itemOrArray, strategy) {
if (Array.isArray(itemOrArray))
return proxifyArray(itemOrArray, strategy);
Expand Down Expand Up @@ -5728,6 +5733,11 @@ function rdbClient(options = {}) {
let body = stringify({ patch, options: { strategy, ...concurrencyOptions, deduceStrategy } });
let adapter = netAdapter(url, tableName, { axios: axiosInterceptor, tableOptions });
let p = adapter.patch(body);
if (strategy?.insertAndForget) {
await p;
return;
}

let updatedPositions = extractChangedRowsPositions(array, patch, meta);
let insertedPositions = getInsertedRowsPosition(array);
let { changed, strategy: newStrategy } = await p;
Expand Down
4 changes: 1 addition & 3 deletions tests/conflicts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,14 +278,12 @@ describe('insert overwrite with optimistic column changed', () => {

let message;
try {

const row = await db.vendor.insert({
await db.vendor.insert({
id: 1,
name: 'George',
balance: 177,
isActive: false
});
console.dir(row, {depth: Infinity});
}
catch (e) {
message = e.message;
Expand Down
66 changes: 66 additions & 0 deletions tests/db2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const rdb = require('../src/index');

const nameSchema = {
type: 'string',
};


function validateName(value) {
if (value && value.length > 10)
throw new Error('Length cannot exceed 10 characters');
}

function truthy(value) {
if (!value)
throw new Error('Name must be set');
}

const map = rdb.map(x => ({
customer: x.table('customer').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
name: column('name').string().validate(validateName).validate(truthy).JSONSchema(nameSchema),
balance: column('balance').numeric(),
isActive: column('isActive').boolean(),
})),

package: x.table('package').map(({ column }) => ({
id: column('packageId').numeric().primary().notNullExceptInsert(),
lineId: column('lineId').numeric().notNullExceptInsert(),
sscc: column('sscc').string()
})),

order: x.table('_order').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
orderDate: column('orderDate').date().notNull(),
customerId: column('customerId').numeric().notNullExceptInsert(),
})),

orderLine: x.table('orderLine').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
orderId: column('orderId').numeric(),
product: column('product').string(),
})),

deliveryAddress: x.table('deliveryAddress').map(({ column }) => ({
id: column('id').numeric().primary().notNullExceptInsert(),
orderId: column('orderId').numeric(),
name: column('name').string(),
street: column('street').string(),
postalCode: column('postalCode').string(),
postalPlace: column('postalPlace').string(),
countryCode: column('countryCode').string(),
}))
})).map(x => ({
orderLine: x.orderLine.map(({ hasMany }) => ({
packages: hasMany(x.package).by('lineId')
}))
})).map(x => ({
order: x.order.map(({ hasOne, hasMany, references }) => ({
customer: references(x.customer).by('customerId'),
deliveryAddress: hasOne(x.deliveryAddress).by('orderId'),
lines: hasMany(x.orderLine).by('orderId')
}))

}));

module.exports = map;
55 changes: 1 addition & 54 deletions tests/readonly.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, test, beforeAll, afterAll, expect } from 'vitest';
import { fileURLToPath } from 'url';
const map = require('./db');
const map = require('./db2');
import express from 'express';
import cors from 'cors';
import { json } from 'body-parser';
Expand Down Expand Up @@ -140,48 +140,6 @@ describe('readonly everything', () => {
balance: { readonly: true },
isActive: { readonly: true }
},
vendor: {
id: { readonly: true },
name: { readonly: true },
balance: { readonly: true },
isActive: { readonly: true }
},
customer2: {
id: { readonly: true },
name: { readonly: true },
balance: { readonly: true },
isActive: { readonly: true },
data: { readonly: true },
picture: { readonly: true }
},
customerDbNull: {
balance: {
readonly: true,
},
id: {
readonly: true,
},
isActive: {
readonly: true,
},
name: {
readonly: true,
},
},
customerDefault: {
balance: {
readonly: true,
},
id: {
readonly: true,
},
isActive: {
readonly: true,
},
name: {
readonly: true,
},
},
order: {
id: { readonly: true },
orderDate: { readonly: true },
Expand Down Expand Up @@ -247,17 +205,6 @@ describe('readonly everything', () => {
sscc: {
readonly: true,
},
},
datetest: {
id: { readonly: true },
date: { readonly: true },
datetime: { readonly: true },
},
datetestWithTz: {
id: { readonly: true },
date: { readonly: true },
datetime: { readonly: true },
datetime_tz: { readonly: true },
}
};
expect(error?.message).toEqual('Cannot update column name because it is readonly');
Expand Down

0 comments on commit 90d69fb

Please sign in to comment.