velocious 1.0.534 → 1.0.536
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/database/drivers/base.js +17 -0
- package/build/database/drivers/mysql/index.js +39 -11
- package/build/database/drivers/mysql/query-stream.js +43 -0
- package/build/database/tenants/data-copier.js +209 -5
- package/build/src/database/drivers/base.d.ts +10 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +16 -1
- package/build/src/database/drivers/mysql/index.d.ts +8 -0
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +34 -10
- package/build/src/database/drivers/mysql/query-stream.d.ts +14 -0
- package/build/src/database/drivers/mysql/query-stream.d.ts.map +1 -0
- package/build/src/database/drivers/mysql/query-stream.js +43 -0
- package/build/src/database/tenants/data-copier.d.ts +97 -3
- package/build/src/database/tenants/data-copier.d.ts.map +1 -1
- package/build/src/database/tenants/data-copier.js +183 -6
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/database/drivers/base.js +17 -0
- package/src/database/drivers/mysql/index.js +39 -11
- package/src/database/drivers/mysql/query-stream.js +43 -0
- package/src/database/tenants/data-copier.js +209 -5
|
@@ -14,6 +14,7 @@ import Options from "./options.js"
|
|
|
14
14
|
import mysql from "mysql"
|
|
15
15
|
import query from "./query.js"
|
|
16
16
|
import QueryParser from "./query-parser.js"
|
|
17
|
+
import streamQuery from "./query-stream.js"
|
|
17
18
|
import RemoveIndex from "./sql/remove-index.js"
|
|
18
19
|
import Table from "./table.js"
|
|
19
20
|
import StructureSql from "./structure-sql.js"
|
|
@@ -320,19 +321,32 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
320
321
|
* @returns {import("../base.js").RetryableDatabaseErrorResult} - Retry info.
|
|
321
322
|
*/
|
|
322
323
|
retryableDatabaseError(error) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
message.
|
|
330
|
-
|
|
331
|
-
|
|
324
|
+
/** @type {Error | undefined} */
|
|
325
|
+
let currentError = error
|
|
326
|
+
let shouldReconnect = false
|
|
327
|
+
|
|
328
|
+
while (currentError) {
|
|
329
|
+
const errorCode = "code" in currentError && typeof currentError.code == "string" ? currentError.code : undefined
|
|
330
|
+
const message = currentError.message || ""
|
|
331
|
+
|
|
332
|
+
if (errorCode == "ER_CHECKREAD" || message.includes("Record has changed since last read")) {
|
|
333
|
+
return {retry: true, reconnect: false, waitMs: 50}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
shouldReconnect ||= (
|
|
337
|
+
errorCode == "ECONNREFUSED" ||
|
|
338
|
+
message.includes("ECONNREFUSED") ||
|
|
339
|
+
message.includes("connect ECONNREFUSED") ||
|
|
340
|
+
message.includes("PROTOCOL_CONNECTION_LOST") ||
|
|
341
|
+
message.includes("Connection lost")
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
currentError = currentError.cause instanceof Error ? currentError.cause : undefined
|
|
345
|
+
}
|
|
332
346
|
|
|
333
347
|
return {
|
|
334
|
-
retry:
|
|
335
|
-
reconnect:
|
|
348
|
+
retry: shouldReconnect,
|
|
349
|
+
reconnect: shouldReconnect,
|
|
336
350
|
waitMs: 50
|
|
337
351
|
}
|
|
338
352
|
}
|
|
@@ -358,6 +372,20 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
358
372
|
}
|
|
359
373
|
}
|
|
360
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Streams the rows of `sql` from a dedicated pooled connection using the MySQL cursor, so a
|
|
377
|
+
* large result set is read incrementally instead of being buffered. Overrides the base
|
|
378
|
+
* buffered fallback with true server-side streaming.
|
|
379
|
+
* @param {string} sql - SQL string to stream.
|
|
380
|
+
* @yields {Record<string, unknown>} - The result rows, one at a time.
|
|
381
|
+
*/
|
|
382
|
+
async *queryStream(sql) {
|
|
383
|
+
if (!this.pool) await this.connect()
|
|
384
|
+
if (!this.pool) throw new Error("MySQL pool failed to initialize")
|
|
385
|
+
|
|
386
|
+
yield* streamQuery(this.pool, sql)
|
|
387
|
+
}
|
|
388
|
+
|
|
361
389
|
/**
|
|
362
390
|
* Executes a mutation with affected-row metadata.
|
|
363
391
|
* @param {string} sql - Mutation SQL.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {PassThrough} from "node:stream"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Streams the rows of `sql` from a dedicated pooled connection, yielding row objects one at a
|
|
5
|
+
* time so an arbitrarily large result set is never buffered in memory. The `mysql` package's
|
|
6
|
+
* query stream is a `readable-stream` polyfill that is not async-iterable, so it is piped through
|
|
7
|
+
* a native {@link PassThrough} (which is) — `pipe` preserves backpressure, pausing the source
|
|
8
|
+
* connection when the consumer falls behind. The connection is released back to the pool on
|
|
9
|
+
* normal completion, and destroyed if iteration is aborted (a `break`/`throw` out of the
|
|
10
|
+
* consuming `for await`) so a half-drained connection is never handed back to the pool.
|
|
11
|
+
* @param {import("mysql").Pool} pool - Pool to check a streaming connection out of.
|
|
12
|
+
* @param {string} sql - SQL string to stream.
|
|
13
|
+
* @yields {Record<string, unknown>} - The result rows, one at a time.
|
|
14
|
+
*/
|
|
15
|
+
export default async function* streamQuery(pool, sql) {
|
|
16
|
+
const connection = await new Promise((resolve, reject) => {
|
|
17
|
+
pool.getConnection((error, pooledConnection) => {
|
|
18
|
+
if (error) reject(error)
|
|
19
|
+
else resolve(pooledConnection)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
let completed = false
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const sourceStream = connection.query(sql).stream()
|
|
26
|
+
const rowStream = new PassThrough({objectMode: true})
|
|
27
|
+
|
|
28
|
+
sourceStream.on("error", (/** @type {unknown} */ error) => rowStream.destroy(error instanceof Error ? error : new Error(String(error))))
|
|
29
|
+
sourceStream.pipe(rowStream)
|
|
30
|
+
|
|
31
|
+
for await (const row of rowStream) {
|
|
32
|
+
yield row
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
completed = true
|
|
36
|
+
} finally {
|
|
37
|
+
if (completed) {
|
|
38
|
+
connection.release()
|
|
39
|
+
} else {
|
|
40
|
+
connection.destroy()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_INSERT_CHUNK_SIZE = 100
|
|
4
4
|
const DEFAULT_QUERY_CHUNK_SIZE = 500
|
|
5
|
+
const DEFAULT_STREAM_BATCH_SIZE = 1000
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Splits an array into chunks of at most `chunkSize` items.
|
|
@@ -130,6 +131,35 @@ export default class DataCopier {
|
|
|
130
131
|
* @returns {Promise<Map<string, Record<string, unknown>[]>>} - Loaded rows grouped by table name.
|
|
131
132
|
*/
|
|
132
133
|
async loadRows(db, keyValue) {
|
|
134
|
+
return (await this.traversePlan(db, keyValue)).rowsByTableName
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Loads only the ids for `keyValue` for every table in the plan from `db`, using the same
|
|
139
|
+
* parent/child traversal as {@link DataCopier#loadRows} but selecting just the id column.
|
|
140
|
+
* Callers that only need to compare row membership — for example verifying a tenant already
|
|
141
|
+
* holds every default row before a cleanup delete — should use this instead of loadRows so
|
|
142
|
+
* they never materialise full rows; for large tenants that is the difference between a few
|
|
143
|
+
* kilobytes of ids and gigabytes of row data.
|
|
144
|
+
* @param {import("../drivers/base.js").default} db - Source or target database to traverse.
|
|
145
|
+
* @param {string} keyValue - Tenant key selecting the root plan rows.
|
|
146
|
+
* @returns {Promise<Map<string, string[]>>} - Loaded ids grouped by table name.
|
|
147
|
+
*/
|
|
148
|
+
async loadRowIds(db, keyValue) {
|
|
149
|
+
return (await this.traversePlan(db, keyValue, [this.idColumn])).idsByTableName
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Traverses the table plan for `keyValue`, querying each table by its tenant key column or
|
|
154
|
+
* (for child tables) by the ids already selected for its parent, and returns both the ids
|
|
155
|
+
* and the loaded rows grouped by table name. `selectColumns` bounds the columns each query
|
|
156
|
+
* selects; pass `[idColumn]` for an id-only traversal, or omit it to load full rows.
|
|
157
|
+
* @param {import("../drivers/base.js").default} db - Source or target database to traverse.
|
|
158
|
+
* @param {string} keyValue - Tenant key selecting the root plan rows.
|
|
159
|
+
* @param {string[]} [selectColumns] - Columns to select; defaults to every column.
|
|
160
|
+
* @returns {Promise<{idsByTableName: Map<string, string[]>, rowsByTableName: Map<string, Record<string, unknown>[]>}>} - Ids and rows grouped by table name.
|
|
161
|
+
*/
|
|
162
|
+
async traversePlan(db, keyValue, selectColumns) {
|
|
133
163
|
/** @type {Map<string, string[]>} */
|
|
134
164
|
const idsByTableName = new Map()
|
|
135
165
|
/** @type {Map<string, Record<string, unknown>[]>} */
|
|
@@ -142,6 +172,7 @@ export default class DataCopier {
|
|
|
142
172
|
rows = await this.queryRowsByColumn({
|
|
143
173
|
columnName: tableConfig.keyColumn,
|
|
144
174
|
db,
|
|
175
|
+
selectColumns,
|
|
145
176
|
tableName: tableConfig.tableName,
|
|
146
177
|
values: [keyValue]
|
|
147
178
|
})
|
|
@@ -157,6 +188,7 @@ export default class DataCopier {
|
|
|
157
188
|
rows = await this.queryRowsByColumn({
|
|
158
189
|
columnName: tableConfig.parentColumn,
|
|
159
190
|
db,
|
|
191
|
+
selectColumns,
|
|
160
192
|
tableName: tableConfig.tableName,
|
|
161
193
|
values: idsByTableName.get(tableConfig.parentTableName) || []
|
|
162
194
|
})
|
|
@@ -170,15 +202,184 @@ export default class DataCopier {
|
|
|
170
202
|
rowsByTableName.set(tableConfig.tableName, rows)
|
|
171
203
|
}
|
|
172
204
|
|
|
173
|
-
return rowsByTableName
|
|
205
|
+
return {idsByTableName, rowsByTableName}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The plan tables referenced as a parent by some child entry. Their ids must be retained
|
|
210
|
+
* while streaming so their children can be scoped; leaf tables — typically the high-volume
|
|
211
|
+
* ones — are never in this set and so are never accumulated in memory.
|
|
212
|
+
* @returns {Set<string>} - Table names that are a parent of another plan entry.
|
|
213
|
+
*/
|
|
214
|
+
parentTableNames() {
|
|
215
|
+
/** @type {Set<string>} */
|
|
216
|
+
const names = new Set()
|
|
217
|
+
|
|
218
|
+
for (const tableConfig of this.tablePlan) {
|
|
219
|
+
if (tableConfig.parentTableName) {
|
|
220
|
+
names.add(tableConfig.parentTableName)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return names
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Streams every plan table's source rows scoped to `keyValue` in bounded `batchSize` batches,
|
|
229
|
+
* following parent/child chaining. Each table is read through a real
|
|
230
|
+
* {@link import("../drivers/base.js").default#queryStream} cursor, so a large table is never
|
|
231
|
+
* buffered; only the ids of tables that are themselves a parent are retained (to scope their
|
|
232
|
+
* children). `selectColumns` bounds each row's projection — pass `[idColumn]` for a light
|
|
233
|
+
* id-only scan, or omit it to stream full rows — and must include the id column for any table
|
|
234
|
+
* that has children. Memory stays bounded to one batch plus the retained parent-table ids.
|
|
235
|
+
* @param {import("../drivers/base.js").default} db - Source database to stream.
|
|
236
|
+
* @param {string} keyValue - Tenant key selecting the root plan rows.
|
|
237
|
+
* @param {{batchSize: number, selectColumns?: string[]}} options - Batch size and optional projection.
|
|
238
|
+
* @yields {{rows: Record<string, unknown>[], tableName: string}} - Successive row batches per table.
|
|
239
|
+
*/
|
|
240
|
+
async *streamPlanSourceBatches(db, keyValue, {batchSize, selectColumns}) {
|
|
241
|
+
/** @type {Map<string, string[]>} */
|
|
242
|
+
const retainedIdsByTableName = new Map()
|
|
243
|
+
const parentTableNames = this.parentTableNames()
|
|
244
|
+
|
|
245
|
+
for (const tableConfig of this.tablePlan) {
|
|
246
|
+
/** @type {string} */
|
|
247
|
+
let scopeColumn
|
|
248
|
+
/** @type {string[]} */
|
|
249
|
+
let scopeValues
|
|
250
|
+
|
|
251
|
+
if (tableConfig.keyColumn) {
|
|
252
|
+
scopeColumn = tableConfig.keyColumn
|
|
253
|
+
scopeValues = [keyValue]
|
|
254
|
+
} else {
|
|
255
|
+
if (!tableConfig.parentColumn || !tableConfig.parentTableName) {
|
|
256
|
+
throw new Error(`Expected keyColumn or parentTableName+parentColumn for table ${tableConfig.tableName} in the tenant table plan.`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!retainedIdsByTableName.has(tableConfig.parentTableName)) {
|
|
260
|
+
throw new Error(`Tenant table plan entry ${tableConfig.tableName} references parent table ${tableConfig.parentTableName}, which has not been streamed; parent tables must appear before their children in the plan.`)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
scopeColumn = tableConfig.parentColumn
|
|
264
|
+
scopeValues = retainedIdsByTableName.get(tableConfig.parentTableName) || []
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** @type {string[] | null} */
|
|
268
|
+
const retainedIds = parentTableNames.has(tableConfig.tableName) ? [] : null
|
|
269
|
+
const quotedTable = db.quoteTable(tableConfig.tableName)
|
|
270
|
+
const quotedScope = db.quoteColumn(scopeColumn)
|
|
271
|
+
const selectList = selectColumns
|
|
272
|
+
? selectColumns.map((column) => `${quotedTable}.${db.quoteColumn(column)}`).join(", ")
|
|
273
|
+
: `${quotedTable}.*`
|
|
274
|
+
/** @type {Record<string, unknown>[]} */
|
|
275
|
+
let batch = []
|
|
276
|
+
|
|
277
|
+
for (const scopeChunk of chunks(uniqueStrings(scopeValues), this.queryChunkSize)) {
|
|
278
|
+
const sql = `SELECT ${selectList} FROM ${quotedTable} WHERE ${quotedScope} IN (${this.quotedValuesSql(db, scopeChunk)})`
|
|
279
|
+
|
|
280
|
+
for await (const row of db.queryStream(sql)) {
|
|
281
|
+
batch.push(row)
|
|
282
|
+
|
|
283
|
+
if (retainedIds) {
|
|
284
|
+
retainedIds.push(String(row[this.idColumn]))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (batch.length >= batchSize) {
|
|
288
|
+
yield {rows: batch, tableName: tableConfig.tableName}
|
|
289
|
+
batch = []
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (batch.length > 0) {
|
|
295
|
+
yield {rows: batch, tableName: tableConfig.tableName}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
retainedIdsByTableName.set(tableConfig.tableName, retainedIds || [])
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Returns which of `ids` currently exist in `tableName` in `db`. `ids` is one already-bounded
|
|
304
|
+
* batch, so it is probed with a single `IN (...)` lookup.
|
|
305
|
+
* @param {{db: import("../drivers/base.js").default, ids: string[], tableName: string}} args - Database, ids to probe, and table.
|
|
306
|
+
* @returns {Promise<Set<string>>} - The subset of `ids` present in the table.
|
|
307
|
+
*/
|
|
308
|
+
async queryExistingIds({db, ids, tableName}) {
|
|
309
|
+
if (ids.length <= 0) {
|
|
310
|
+
return new Set()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const quotedTable = db.quoteTable(tableName)
|
|
314
|
+
const quotedId = db.quoteColumn(this.idColumn)
|
|
315
|
+
const rows = await this.executeQuietQuery(db, `SELECT ${quotedTable}.${quotedId} FROM ${quotedTable} WHERE ${quotedId} IN (${this.quotedValuesSql(db, ids)})`)
|
|
316
|
+
|
|
317
|
+
return new Set(rows.map((row) => String(row[this.idColumn])))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Streams the source ids for `keyValue` and returns the first table found to be missing rows in
|
|
322
|
+
* the target, with that batch's missing ids — stopping at the first shortfall instead of
|
|
323
|
+
* enumerating every missing row. Callers verifying a tenant already holds every source row (for
|
|
324
|
+
* example before deleting the source copies) treat an empty result as the go-ahead and any entry
|
|
325
|
+
* as a hard stop; failing fast keeps memory bounded to a single batch even when the target is
|
|
326
|
+
* far behind.
|
|
327
|
+
* @param {string} keyValue - Tenant key selecting the source rows to check.
|
|
328
|
+
* @param {{batchSize?: number}} [options] - Streaming batch size.
|
|
329
|
+
* @returns {Promise<Map<string, string[]>>} - The first table missing rows and that batch's missing ids, or empty when the target holds everything.
|
|
330
|
+
*/
|
|
331
|
+
async findMissingRowIds(keyValue, {batchSize = DEFAULT_STREAM_BATCH_SIZE} = {}) {
|
|
332
|
+
/** @type {Map<string, string[]>} */
|
|
333
|
+
const missingByTableName = new Map()
|
|
334
|
+
|
|
335
|
+
for await (const {rows, tableName} of this.streamPlanSourceBatches(this.sourceDb, keyValue, {batchSize, selectColumns: [this.idColumn]})) {
|
|
336
|
+
const ids = rows.map((row) => String(row[this.idColumn]))
|
|
337
|
+
const existingIds = await this.queryExistingIds({db: this.targetDb, ids, tableName})
|
|
338
|
+
const missingIds = ids.filter((id) => !existingIds.has(id))
|
|
339
|
+
|
|
340
|
+
if (missingIds.length > 0) {
|
|
341
|
+
missingByTableName.set(tableName, missingIds)
|
|
342
|
+
break
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return missingByTableName
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Streams the source rows for `keyValue` and copies into the target, batch by batch, only the
|
|
351
|
+
* rows missing there. Because the full rows travel in the stream, each batch's missing rows are
|
|
352
|
+
* inserted as it arrives — nothing is accumulated, and no second source query runs while the
|
|
353
|
+
* source connection is held by the stream — so a table with large columns stays bounded to one
|
|
354
|
+
* batch even when the target is empty. Intended for single-table plans (or plans whose per-table,
|
|
355
|
+
* parent-first insert order is foreign-key safe).
|
|
356
|
+
* @param {string} keyValue - Tenant key selecting the source rows to reconcile.
|
|
357
|
+
* @param {{batchSize?: number}} [options] - Streaming batch size.
|
|
358
|
+
* @returns {Promise<number>} - The number of rows copied into the target.
|
|
359
|
+
*/
|
|
360
|
+
async copyMissingRows(keyValue, {batchSize = DEFAULT_STREAM_BATCH_SIZE} = {}) {
|
|
361
|
+
let copiedCount = 0
|
|
362
|
+
|
|
363
|
+
for await (const {rows, tableName} of this.streamPlanSourceBatches(this.sourceDb, keyValue, {batchSize})) {
|
|
364
|
+
const existingIds = await this.queryExistingIds({db: this.targetDb, ids: rows.map((row) => String(row[this.idColumn])), tableName})
|
|
365
|
+
const missingRows = rows.filter((row) => !existingIds.has(String(row[this.idColumn])))
|
|
366
|
+
|
|
367
|
+
if (missingRows.length > 0) {
|
|
368
|
+
await this.insertTargetRows(new Map([[tableName, missingRows]]))
|
|
369
|
+
copiedCount += missingRows.length
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return copiedCount
|
|
174
374
|
}
|
|
175
375
|
|
|
176
376
|
/**
|
|
177
|
-
* Selects
|
|
178
|
-
*
|
|
377
|
+
* Selects `tableName` rows in `db` whose `columnName` is in `values`, chunked. `selectColumns`
|
|
378
|
+
* bounds the projection and defaults to every column.
|
|
379
|
+
* @param {{columnName: string, db: import("../drivers/base.js").default, selectColumns?: string[], tableName: string, values: string[]}} args - Table, column, projection, database, and values for the chunked lookup.
|
|
179
380
|
* @returns {Promise<Record<string, unknown>[]>} - Rows matching the supplied column values.
|
|
180
381
|
*/
|
|
181
|
-
async queryRowsByColumn({columnName, db, tableName, values}) {
|
|
382
|
+
async queryRowsByColumn({columnName, db, selectColumns, tableName, values}) {
|
|
182
383
|
const normalizedValues = uniqueStrings(values)
|
|
183
384
|
|
|
184
385
|
if (normalizedValues.length <= 0) {
|
|
@@ -188,9 +389,12 @@ export default class DataCopier {
|
|
|
188
389
|
const rows = []
|
|
189
390
|
const quotedTable = db.quoteTable(tableName)
|
|
190
391
|
const quotedColumn = db.quoteColumn(columnName)
|
|
392
|
+
const selectList = selectColumns
|
|
393
|
+
? selectColumns.map((column) => `${quotedTable}.${db.quoteColumn(column)}`).join(", ")
|
|
394
|
+
: `${quotedTable}.*`
|
|
191
395
|
|
|
192
396
|
for (const valuesChunk of chunks(normalizedValues, this.queryChunkSize)) {
|
|
193
|
-
const sql = `SELECT ${
|
|
397
|
+
const sql = `SELECT ${selectList} FROM ${quotedTable} WHERE ${quotedColumn} IN (${this.quotedValuesSql(db, valuesChunk)})`
|
|
194
398
|
|
|
195
399
|
rows.push(...await this.executeQuietQuery(db, sql))
|
|
196
400
|
}
|