Skip to content

Commit 4c0d0b1

Browse files
alxndrsnspwoodcock
authored andcommitted
model/query: don't use IN without an array (getodk#1449)
As shown by getodk#1450, using `IN` for a single value can have significant effects on the PostgreSQL query plan.
1 parent 2386d7b commit 4c0d0b1

File tree

6 files changed

+17
-19
lines changed

6 files changed

+17
-19
lines changed

lib/model/query/analytics.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ from (
245245
THEN 1
246246
ELSE 0
247247
END) as "hasGeo",
248-
max(CASE WHEN ff."type" in ('binary') AND ff."name" = 'audit'
248+
max(CASE WHEN ff."type" = 'binary' AND ff."name" = 'audit'
249249
THEN 1
250250
ELSE 0
251251
END) as "hasAudit",
@@ -257,7 +257,7 @@ from (
257257
THEN 1
258258
ELSE 0
259259
END) as "hasGeoRecent",
260-
max(CASE WHEN ff."type" in ('binary') AND ff."name" = 'audit' AND recentsubs."activeFormId" is not null
260+
max(CASE WHEN ff."type" = 'binary' AND ff."name" = 'audit' AND recentsubs."activeFormId" is not null
261261
THEN 1
262262
ELSE 0
263263
END) as "hasAuditRecent"

lib/model/query/client-audits.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// except according to the terms contained in the LICENSE file.
99

1010
const { sql } = require('slonik');
11-
const { insertMany, QueryOptions } = require('../../util/db');
11+
const { insertMany, sqlInArray, QueryOptions } = require('../../util/db');
1212
const { odataFilter } = require('../../data/odata-filter');
1313
const { odataToColumnMap } = require('../../data/submission');
1414
const { streamBlobs } = require('../../util/blob');
@@ -19,10 +19,6 @@ const existsForBlob = (blobId) => ({ maybeOne }) =>
1919
maybeOne(sql`select true from client_audits where "blobId"=${blobId} limit 1`)
2020
.then((x) => x.isDefined());
2121

22-
// TODO: copy-pasted from query/submission-attachments.js
23-
const keyIdCondition = (keyIds) =>
24-
sql.join((((keyIds == null) || (keyIds.length === 0)) ? [ -1 ] : keyIds), sql`,`);
25-
2622
const streamForExport = (formId, draft, keyIds, options = QueryOptions.none) => ({ s3, stream }) => stream(sql`
2723
select client_audits.*, blobs.id AS "blobId", blobs.s3_status, blobs.content, blobs.sha, submissions."instanceId", "localKey", "keyId", index, submissions."instanceId" from submission_defs
2824
inner join
@@ -38,7 +34,7 @@ select client_audits.*, blobs.id AS "blobId", blobs.s3_status, blobs.content, bl
3834
inner join form_defs on submission_defs."formDefId"=form_defs.id
3935
where ${odataFilter(options.filter, odataToColumnMap)}
4036
and current=true
41-
and (form_defs."keyId" is null or form_defs."keyId" in (${keyIdCondition(keyIds)}))
37+
and (form_defs."keyId" is null or ${sqlInArray(sql`form_defs."keyId"`, keyIds)})
4238
order by submission_defs."createdAt" asc, submission_defs.id asc`)
4339
.then(dbStream => streamBlobs(s3, dbStream));
4440

lib/model/query/keys.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const { sql } = require('slonik');
1111
const { map } = require('ramda');
1212
const { Key } = require('../frames');
1313
const { submissionDecryptor } = require('../../util/crypto');
14-
const { insert } = require('../../util/db');
14+
const { insert, sqlInArray } = require('../../util/db');
1515
const { resolve } = require('../../util/promise');
1616
const { construct } = require('../../util/util');
1717

@@ -48,7 +48,7 @@ ORDER BY keys.id DESC`)
4848
.then(map(construct(Key)));
4949

5050
const getManagedByIds = (ids) => ({ all }) =>
51-
all(sql`select * from keys where managed=true and id in (${sql.join(ids, sql`,`)})`)
51+
all(sql`select * from keys where managed=true and ${sqlInArray(sql`id`, ids)}`)
5252
.then(map(construct(Key)));
5353

5454
const getDecryptor = (passphrases = {}) => ({ Keys }) => {

lib/model/query/submission-attachments.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const { Audit, Blob, Submission } = require('../frames');
1616
const { odataFilter } = require('../../data/odata-filter');
1717
const { odataToColumnMap } = require('../../data/submission');
1818
const { submissionXmlToFieldStream } = require('../../data/submission');
19-
const { insertMany, QueryOptions } = require('../../util/db');
19+
const { insertMany, sqlInArray, QueryOptions } = require('../../util/db');
2020
const { resolve } = require('../../util/promise');
2121
const { isBlank, construct } = require('../../util/util');
2222
const { traverseXml, findAll, root, node, text } = require('../../util/xml');
@@ -189,10 +189,6 @@ const getBySubmissionDefIdAndName = (subDefId, name) => ({ maybeOne }) =>
189189
////////////////////////////////////////////////////////////////////////////////
190190
// EXPORT
191191

192-
// TODO: copy-pasted to query/client-audits.js
193-
const keyIdCondition = (keyIds) =>
194-
sql.join((((keyIds == null) || (keyIds.length === 0)) ? [ -1 ] : keyIds), sql`,`);
195-
196192
const streamForExport = (formId, draft, keyIds, options = QueryOptions.none) => ({ s3, stream }) => stream(sql`
197193
select submission_attachments.name, blobs.id AS "blobId", blobs.content, blobs.s3_status, blobs.sha, submission_attachments.index, form_defs."keyId", submissions."instanceId", submission_defs."localKey" from submission_defs
198194
inner join (select * from submissions where draft=${draft}) as submissions
@@ -206,7 +202,7 @@ where submission_defs.current=true
206202
and ${odataFilter(options.filter, odataToColumnMap)}
207203
and submission_attachments.name is distinct from submission_defs."encDataAttachmentName"
208204
and submission_attachments."isClientAudit" is not true
209-
and (form_defs."keyId" is null or form_defs."keyId" in (${keyIdCondition(keyIds)}))`
205+
and (form_defs."keyId" is null or ${sqlInArray(sql`form_defs."keyId"`, keyIds)})`
210206
)
211207
.then(dbStream => streamBlobs(s3, dbStream));
212208

lib/model/query/submissions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const { Frame, table } = require('../frame');
1313
const { Actor, Form, Submission } = require('../frames');
1414
const { odataFilter, odataOrderBy, odataExcludeDeleted } = require('../../data/odata-filter');
1515
const { odataToColumnMap, odataSubTableToColumnMap } = require('../../data/submission');
16-
const { unjoiner, extender, sqlEquals, page, updater, QueryOptions, insertMany, markDeleted, markUndeleted } = require('../../util/db');
16+
const { unjoiner, extender, sqlEquals, page, updater, QueryOptions, insertMany, markDeleted, markUndeleted, sqlInArray } = require('../../util/db');
1717
const { blankStringToNull, construct } = require('../../util/util');
1818
const Problem = require('../../util/problem');
1919
const { streamEncBlobs } = require('../../util/blob');
@@ -395,7 +395,7 @@ inner join
395395
on edits."submissionId"=submission_defs."submissionId"
396396
${joinOnSkiptoken(options.skiptoken, formId, draft)}
397397
where
398-
${encrypted ? sql`(form_defs."encKeyId" is null or form_defs."encKeyId" in (${sql.join(keyIds, sql`,`)})) and` : sql``}
398+
${encrypted ? sql`(form_defs."encKeyId" is null or ${sqlInArray(sql`form_defs."encKeyId"`, keyIds)}) and` : sql``}
399399
${odataFilter(options.filter, options.isSubmissionsTable ? odataToColumnMap : odataSubTableToColumnMap)} and
400400
${sqlEquals(options.condition)}
401401
and submission_defs.current=true and submissions."formId"=${formId} and ${odataExcludeDeleted(options.filter, options.isSubmissionsTable ? odataToColumnMap : odataSubTableToColumnMap)}

lib/util/db.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,10 +587,16 @@ const postgresErrorToProblem = (x) => {
587587
return reject(error);
588588
};
589589

590+
const sqlInArray = (k, arr) => {
591+
if (!arr?.length) return sql`FALSE`;
592+
if (arr.length === 1) return sql`${k} = ${arr[0]}`;
593+
return sql`${k} IN (${sql.join(arr, sql`,`)})`;
594+
};
595+
590596
module.exports = {
591597
connectionString, knexConnection,
592598
unjoiner, extender, sqlEquals, page, queryFuncs,
593-
insert, insertMany, updater, markDeleted, markUndeleted,
599+
insert, insertMany, updater, markDeleted, markUndeleted, sqlInArray,
594600
QueryOptions,
595601
postgresErrorToProblem
596602
};

0 commit comments

Comments
 (0)