Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ jobs:
# @iobroker/adapter-core, which calls process.exit(10) when js-controller cannot be
# resolved, and js-controller is not a dependency of this repository.
- name: Run unit tests
run: node node_modules/mocha/bin/mocha test/testCommons.js test/testIntegral.js test/testDockerCompose.js test/testErrors.js test/testSqlClient.js test/testConnectionOptions.js test/testMessageGuard.js test/testStatistics.js test/testPackageFiles.js --exit
run: node node_modules/mocha/bin/mocha test/testCommons.js test/testIntegral.js test/testDockerCompose.js test/testErrors.js test/testSqlClient.js test/testConnectionOptions.js test/testMessageGuard.js test/testStatistics.js test/testInsertStatements.js test/testPackageFiles.js --exit

- name: Run SQLite tests
run: node node_modules/mocha/bin/mocha test/testSQLite.js --exit
Expand Down
83 changes: 83 additions & 0 deletions test/testInsertStatements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const assert = require('node:assert');

// Imported per dialect, never from build/main: that would pull in @iobroker/adapter-core, which
// calls process.exit(10) when js-controller cannot be resolved.
const DIALECTS = {
mysql: require('../build/lib/mysql'),
postgresql: require('../build/lib/postgresql'),
mssql: require('../build/lib/mssql'),
sqlite: require('../build/lib/sqlite'),
};

// A statement that carries a second one after a semicolon. The trailing semicolon of the statement
// itself is not a match - only a semicolon with another statement behind it.
const CONCATENATED = /;\s*(INSERT|SELECT|UPDATE|DELETE)/i;

function rows() {
// The batch from issue #294: a counter datapoint produces rows in ts_counter *and* ts_number
// within the same flush, which is what used to end up in one query.
return [
{ table: 'ts_counter', state: { val: 53.32, ts: 1676135872618 }, from: 2 },
{ table: 'ts_counter', state: { val: 53, ts: 1676138387812 }, from: 2 },
{ table: 'ts_number', state: { val: 53, ts: 1676138387812, ack: true }, from: 2 },
{ table: 'ts_counter', state: { val: 52.95, ts: 1676138670902 }, from: 2 },
{ table: 'ts_number', state: { val: 52.95, ts: 1676138670902, ack: true }, from: 2 },
];
}

describe('Test insert() never concatenates statements', function () {
for (const [dialect, sql] of Object.entries(DIALECTS)) {
it(`${dialect}: emits one statement per table instead of one joined query`, function () {
const queries = sql.insert('iobroker', 5, rows());

assert.ok(Array.isArray(queries), 'insert() returns a list of statements');
assert.strictEqual(queries.length, 2, `ts_counter and ts_number, got ${queries.length}`);

// This is issue #294: the adapter used to send
// INSERT INTO ...ts_counter ...;INSERT INTO ...ts_number ...;
// as a single query, which MariaDB rejects with a syntax error because no driver here
// is configured for multi-statement batches.
for (const query of queries) {
assert.ok(!CONCATENATED.test(query), `carries a second statement: ${query}`);
assert.strictEqual((query.match(/INSERT/gi) || []).length, 1, `more than one INSERT in: ${query}`);
}

const tables = queries.map(q => (q.match(/ts_\w+/) || [])[0]);
assert.deepStrictEqual([...tables].sort(), ['ts_counter', 'ts_number']);
});

it(`${dialect}: keeps the statements apart for every table`, function () {
const queries = sql.insert('iobroker', 5, [
{ table: 'ts_number', state: { val: 1, ts: 1, ack: true }, from: 2 },
{ table: 'ts_string', state: { val: 'x', ts: 2, ack: true }, from: 2 },
{ table: 'ts_bool', state: { val: true, ts: 3, ack: true }, from: 2 },
{ table: 'ts_counter', state: { val: 4, ts: 4 }, from: 2 },
]);

assert.strictEqual(queries.length, 4, 'one per table');
for (const query of queries) {
assert.ok(!CONCATENATED.test(query), `carries a second statement: ${query}`);
}
});

it(`${dialect}: chunks a large batch without joining the chunks`, function () {
const many = [];
for (let i = 0; i < 1200; i++) {
many.push({ table: 'ts_number', state: { val: i, ts: 1700000000000 + i, ack: true }, from: 2 });
}

const queries = sql.insert('iobroker', 5, many);

// 500 rows per statement, so the chunks must stay separate queries rather than being
// glued back together with semicolons.
assert.strictEqual(queries.length, 3, `expected 500/500/200, got ${queries.length} chunks`);
for (const query of queries) {
assert.ok(!CONCATENATED.test(query), `carries a second statement: ${query}`);
assert.strictEqual((query.match(/INSERT/gi) || []).length, 1);
}

const tuples = queries.map(q => (q.match(/\),\(/g) || []).length + 1);
assert.deepStrictEqual(tuples, [500, 500, 200]);
});
}
});
Loading