Skip to content

Commit

Permalink
Inserting with skipOnConflict on whole table yields incorrect sql #63
Browse files Browse the repository at this point in the history
  • Loading branch information
Lars-Erik Roald committed Nov 7, 2023
1 parent e4760a8 commit 740f0ef
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 20 deletions.
10 changes: 6 additions & 4 deletions src/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,9 @@ function rdbClient(options = {}) {
return adapter.post(body);
}

async function insert(rows, ...args) {
async function insert(rows, ...rest) {
const strategy = undefined;
const args = [strategy].concat(rest);
if (Array.isArray(rows)) {
let proxy = proxify([], args[0]);
proxy.splice.apply(proxy, [0, 0, ...rows]);
Expand Down Expand Up @@ -459,7 +461,7 @@ function rdbClient(options = {}) {
async function saveArray(array, concurrencyOptions, strategy) {
let deduceStrategy;
if (arguments.length < 3)
deduceStrategy = true;
deduceStrategy = false;
let { json } = rootMap.get(array);
strategy = extractStrategy({ strategy }, array);
strategy = extractFetchingStrategy(array, strategy);
Expand All @@ -486,7 +488,7 @@ function rdbClient(options = {}) {
async function patch(patch, concurrencyOptions, strategy) {
let deduceStrategy;
if (arguments.length < 3)
deduceStrategy = true;
deduceStrategy = false;
if (patch.length === 0)
return;
let body = stringify({ patch, options: { strategy, ...concurrencyOptions, deduceStrategy } });
Expand Down Expand Up @@ -661,7 +663,7 @@ function rdbClient(options = {}) {
async function saveRow(row, concurrencyOptions, strategy) {
let deduceStrategy;
if (arguments.length < 3)
deduceStrategy = true;
deduceStrategy = false;
strategy = extractStrategy({ strategy }, row);
strategy = extractFetchingStrategy(row, strategy);

Expand Down
4 changes: 4 additions & 0 deletions src/mySql/insertSql.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ function insertSql(table, row, options) {
values.push('%s');
addConflictUpdate(column);
}
}
if (conflictColumnUpdates.length === 0) {
const column = table._primaryColumns[0];
conflictColumnUpdates.push(`${column._dbName}=VALUES(${column._dbName})`);
}
conflictColumnUpdateSql = conflictColumnUpdates.join(',');

Expand Down
9 changes: 6 additions & 3 deletions src/pg/insertSql.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ function insertSql(table, row, options) {
if (columnNames.length === 0)
sql += `${outputInserted()}DEFAULT VALUES ${lastInsertedSql(table)}`;
else
sql = sql + '('+ columnNames.join(',') + ') ' + outputInserted() + 'VALUES (' + values.join(',') + ')' + onConflict() + lastInsertedSql(table);
sql = sql + '(' + columnNames.join(',') + ') ' + outputInserted() + 'VALUES (' + values.join(',') + ')' + onConflict() + lastInsertedSql(table);
return sql;

function onConflict() {
if (options.concurrency === 'skipOnConflict' || options.concurrency === 'overwrite') {
const primaryKeys = table._primaryColumns.map(x => x._dbName).join(',');
return ` ON CONFLICT(${primaryKeys}) DO UPDATE SET ${conflictColumnUpdateSql} `;
return ` ON CONFLICT(${primaryKeys}) ${conflictColumnUpdateSql} `;
}
else return '';
}
Expand All @@ -44,7 +44,10 @@ function insertSql(table, row, options) {
addConflictUpdate(column);
}
}
conflictColumnUpdateSql = conflictColumnUpdates.join(',');
if (conflictColumnUpdates.length === 0)
conflictColumnUpdateSql = 'DO NOTHING';
else
conflictColumnUpdateSql = 'DO UPDATE SET ' + conflictColumnUpdates.join(',');

function addConflictUpdate(column) {
let concurrency = options[column.alias]?.concurrency || options.concurrency;
Expand Down
12 changes: 9 additions & 3 deletions src/sap/mergeSql.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ function insertSql(table, row, options) {
addDiscriminators();
addColumns();

let sql = `MERGE INTO ${table._dbName} AS target USING (SELECT ${values.join(',')}) AS source ON ${join()} WHEN MATCHED THEN ${whenMatched()} WHEN NOT MATCHED THEN ${whenNotMatched()};`;
const matched = whenMatched();
let sql;
if (matched)
sql = `MERGE INTO ${table._dbName} AS target USING (SELECT ${values.join(',')}) AS source ON ${join()} WHEN MATCHED THEN ${matched} WHEN NOT MATCHED THEN ${whenNotMatched()};`;
else
sql = `MERGE INTO ${table._dbName} AS target USING (SELECT ${values.join(',')}) AS source ON ${join()} WHEN NOT MATCHED THEN ${whenNotMatched()};`;

return sql;

Expand All @@ -22,7 +27,7 @@ function insertSql(table, row, options) {

function whenMatched() {
if (options.concurrency === 'skipOnConflict' || options.concurrency === 'overwrite') {
return `UPDATE SET ${conflictColumnUpdateSql}`;
return conflictColumnUpdateSql;
}
else return '';
}
Expand Down Expand Up @@ -52,7 +57,8 @@ function insertSql(table, row, options) {
addConflictUpdate(column);
}
}
conflictColumnUpdateSql = conflictColumnUpdates.join(',');
if (conflictColumnUpdates.length > 0)
conflictColumnUpdateSql = 'UPDATE SET ' + conflictColumnUpdates.join(',');

function addConflictUpdate(column) {
let concurrency = options[column.alias]?.concurrency || options.concurrency;
Expand Down
7 changes: 5 additions & 2 deletions src/sqlite/insertSql.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function insertSql(table, row, options) {
function onConflict() {
if (options.concurrency === 'skipOnConflict' || options.concurrency === 'overwrite') {
const primaryKeys = table._primaryColumns.map(x => x._dbName).join(',');
return ` ON CONFLICT(${primaryKeys}) DO UPDATE SET ${conflictColumnUpdateSql}`;
return ` ON CONFLICT(${primaryKeys}) ${conflictColumnUpdateSql}`;
} else {
return '';
}
Expand All @@ -46,7 +46,10 @@ function insertSql(table, row, options) {
addConflictUpdate(column);
}
}
conflictColumnUpdateSql = conflictColumnUpdates.join(',');
if (conflictColumnUpdates.length === 0)
conflictColumnUpdateSql = 'DO NOTHING';
else
conflictColumnUpdateSql = 'DO UPDATE SET ' + conflictColumnUpdates.join(',');

function addConflictUpdate(column) {
let concurrency = options[column.alias]?.concurrency || options.concurrency;
Expand Down
15 changes: 11 additions & 4 deletions src/tedious/mergeSql.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ function insertSql(table, row, options) {
addDiscriminators();
addColumns();

let sql = `MERGE INTO ${table._dbName} AS target USING (SELECT ${values.join(',')}) AS source ON ${join()} WHEN MATCHED THEN ${whenMatched()} WHEN NOT MATCHED THEN ${whenNotMatched()} ${outputInsertedSql(table)};`;

const matched = whenMatched();
let sql;
if (matched)
sql = `MERGE INTO ${table._dbName} AS target USING (SELECT ${values.join(',')}) AS source ON ${join()} WHEN MATCHED THEN ${matched} WHEN NOT MATCHED THEN ${whenNotMatched()} ${outputInsertedSql(table)};`;
else
sql = `MERGE INTO ${table._dbName} AS target USING (SELECT ${values.join(',')}) AS source ON ${join()} WHEN NOT MATCHED THEN ${whenNotMatched()} ${outputInsertedSql(table)};`;
return sql;

function join() {
Expand All @@ -24,7 +28,7 @@ function insertSql(table, row, options) {

function whenMatched() {
if (options.concurrency === 'skipOnConflict' || options.concurrency === 'overwrite') {
return `UPDATE SET ${conflictColumnUpdateSql}`;
return conflictColumnUpdateSql;
}
else return '';
}
Expand Down Expand Up @@ -54,7 +58,10 @@ function insertSql(table, row, options) {
addConflictUpdate(column);
}
}
conflictColumnUpdateSql = conflictColumnUpdates.join(',');

if (conflictColumnUpdates.length > 0)
conflictColumnUpdateSql = 'UPDATE SET ' + conflictColumnUpdates.join(',');


function addConflictUpdate(column) {
let concurrency = options[column.alias]?.concurrency || options.concurrency;
Expand Down
96 changes: 92 additions & 4 deletions tests/conflicts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ const versionArray = process.version.replace('v', '').split('.');
const major = parseInt(versionArray[0]);
const port = 3007;



beforeAll(async () => {

await createMs('mssql');

async function createMs(dbName) {
Expand All @@ -29,7 +26,6 @@ beforeAll(async () => {
});

describe('optimistic fail', () => {

test('pg', async () => await verify('pg'));
test('mssql', async () => await verify('mssql'));
if (major > 17)
Expand Down Expand Up @@ -113,6 +109,98 @@ describe('insert skipOnConflict with overwrite column', () => {
}
});

describe('savechanges overload overwrite', () => {
test('pg', async () => await verify('pg'));
test('mssql', async () => await verify('mssql'));
if (major > 17)
test('mssqlNative', async () => await verify('mssqlNative'));
test('mysql', async () => await verify('mysql'));
test('sqlite', async () => await verify('sqlite'));
test('sap', async () => await verify('sap'));

async function verify(dbName) {
let { db, init } = getDb(dbName);
await init(db);

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

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

const george2 = await db.vendor.getById(1);

george.name = 'John 1';
await george.saveChanges();

george2.name = 'John 2'
await george2.saveChanges({ name: { concurrency: 'overwrite' } });

const expected = {
id: 1,
name: 'John 2',
balance: 100,
isActive: true
};

await george.refresh();

expect(george2).toEqual(expected);
expect(george).toEqual(expected);
}
});

describe('savechanges overload optimistic', () => {
rdb.on('query', console.dir);
test('pg', async () => await verify('pg'));
test('mssql', async () => await verify('mssql'));
if (major > 17)
test('mssqlNative', async () => await verify('mssqlNative'));
test('mysql', async () => await verify('mysql'));
test('sqlite', async () => await verify('sqlite'));
test('sap', async () => await verify('sap'));

async function verify(dbName) {
let { db, init } = getDb(dbName);
await init(db);

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

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

const george2 = await db.vendor.getById(1);

george.name = 'John 1';
await george.saveChanges();

george2.name = 'John 2'
let error;
try {
await george2.saveChanges({ name: { concurrency: 'optimistic' } });
}
catch (e) {
error = e;
}
expect(error.message).toEqual(`The field name was changed by another user. Expected 'John', but was 'John 1'.`);
}
});

describe('insert empty skipOnConflict', () => {
test('pg', async () => await verify('pg'));
test('mssql', async () => await verify('mssql'));
Expand Down
Loading

0 comments on commit 740f0ef

Please sign in to comment.