velocious 1.0.581 → 1.0.583
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/README.md +19 -3
- package/build/configuration-types.js +2 -0
- package/build/database/drivers/base.js +209 -13
- package/build/database/query/preloader/belongs-to.js +32 -31
- package/build/database/query/preloader/has-many.js +50 -26
- package/build/database/query/preloader/has-one.js +20 -12
- package/build/database/query/query-data.js +23 -8
- package/build/database/query/with-count.js +47 -11
- package/build/environment-handlers/node/cli/commands/test.js +21 -2
- package/build/frontend-model-controller.js +131 -28
- package/build/src/configuration-types.d.ts +10 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +3 -1
- package/build/src/database/drivers/base.d.ts +71 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +178 -17
- package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
- package/build/src/database/query/preloader/belongs-to.js +33 -31
- package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-many.js +48 -28
- package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-one.js +21 -13
- package/build/src/database/query/query-data.js +21 -8
- package/build/src/database/query/with-count.js +41 -12
- package/build/src/environment-handlers/node/cli/commands/test.d.ts +6 -0
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +22 -3
- package/build/src/frontend-model-controller.d.ts +40 -0
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +116 -28
- package/build/src/testing/test-filter-parser.d.ts +5 -0
- package/build/src/testing/test-filter-parser.d.ts.map +1 -1
- package/build/src/testing/test-filter-parser.js +19 -2
- package/build/src/testing/test-suite-splitter.d.ts +16 -1
- package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
- package/build/src/testing/test-suite-splitter.js +38 -3
- package/build/testing/test-filter-parser.js +21 -1
- package/build/testing/test-suite-splitter.js +44 -2
- package/package.json +4 -1
- package/src/configuration-types.js +2 -0
- package/src/database/drivers/base.js +209 -13
- package/src/database/query/preloader/belongs-to.js +32 -31
- package/src/database/query/preloader/has-many.js +50 -26
- package/src/database/query/preloader/has-one.js +20 -12
- package/src/database/query/query-data.js +23 -8
- package/src/database/query/with-count.js +47 -11
- package/src/environment-handlers/node/cli/commands/test.js +21 -2
- package/src/frontend-model-controller.js +131 -28
- package/src/testing/test-filter-parser.js +21 -1
- package/src/testing/test-suite-splitter.js +44 -2
package/README.md
CHANGED
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
* Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
|
|
24
24
|
* Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
|
|
25
25
|
* Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, and linting (see [docs/factories.md](docs/factories.md))
|
|
26
|
-
* Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters
|
|
27
|
-
* Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
|
|
26
|
+
* Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters, safe batching of structurally identical aggregates, and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
|
|
27
|
+
* Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order and automatic IN-list chunking for large parent sets, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
|
|
28
28
|
* Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
|
|
29
29
|
* Translated model attributes with current-locale relationship sorting (see [docs/translations.md](docs/translations.md))
|
|
30
30
|
* Cross-process broadcast bus for `broadcastToChannel` via `velocious beacon`, including background job runner processes (see [docs/beacon.md](docs/beacon.md))
|
|
@@ -290,10 +290,26 @@ npx velocious test --groups=4 --group-number=2
|
|
|
290
290
|
|
|
291
291
|
# Combine with tags
|
|
292
292
|
npx velocious test --groups=3 --group-number=1 --tag fast
|
|
293
|
+
|
|
294
|
+
# Prefer recorded file durations when balancing groups
|
|
295
|
+
npx velocious test --groups=4 --group-number=1 --timing-manifest=tmp/test-timings.json
|
|
293
296
|
```
|
|
294
297
|
|
|
295
298
|
Files are distributed using a greedy load-balancing algorithm. Each file is
|
|
296
|
-
weighted by
|
|
299
|
+
primarily weighted by a positive finite duration from the optional timing
|
|
300
|
+
manifest. Manifest keys are normalized project-relative test paths using `/`
|
|
301
|
+
separators:
|
|
302
|
+
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"spec/system/sign-in-spec.js": 42.7,
|
|
306
|
+
"spec/controller/accounts-spec.js": 8.1
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Files absent from the manifest, unknown files, invalid duration values, and
|
|
311
|
+
unreadable or malformed JSON all fall back to the existing deterministic
|
|
312
|
+
heuristic. The heuristic weights files by spec directory (`system/` = 20, `frontend-models/` = 10,
|
|
297
313
|
`controller/` = 3, default = 1) with a 2x multiplier for `.browser-spec.js`
|
|
298
314
|
files. The heaviest files are assigned first to the group with the least
|
|
299
315
|
accumulated weight, producing balanced wall-clock times across groups.
|
|
@@ -75,6 +75,8 @@
|
|
|
75
75
|
* @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so a whole structure SQL dump loads in one round-trip via `StructureSqlLoader`. Off by default; ordinary queries otherwise reject stacked statements.
|
|
76
76
|
* @property {number} [maxRowsPerInsert] - Maximum rows per `INSERT ... VALUES (...), (...), ...` statement generated by `Record.insertMultiple`. Defaults to 500.
|
|
77
77
|
* @property {number} [maxInsertSqlBytes] - Maximum serialized SQL size, in bytes, for a single `INSERT ... VALUES (...), (...), ...` statement. Defaults to 1 MiB (1048576).
|
|
78
|
+
* @property {number} [maxInClauseValues] - Maximum values in a single `IN (...)` cohort used by preloads, association counts, and queryData aggregates. Defaults to 999.
|
|
79
|
+
* @property {number} [maxQuerySqlBytes] - Maximum serialized SQL size, in bytes, for a single cohort query used by preloads, association counts, and queryData aggregates. Defaults to 1 MiB (1048576).
|
|
78
80
|
* @property {string} [password] - Password for the database user.
|
|
79
81
|
* @property {number} [port] - Database port.
|
|
80
82
|
* @property {string} [primaryKeyType] - Default type for implicit migration primary keys and references. Defaults to `uuid`.
|
|
@@ -118,6 +118,14 @@
|
|
|
118
118
|
* @property {string[]} updateColumns - Columns to update on conflict.
|
|
119
119
|
*/
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* SqlTokenResult type.
|
|
123
|
+
* @typedef {object} SqlTokenResult
|
|
124
|
+
* @property {boolean} incomplete - Whether the scan hit its bound before finishing trivia/token parsing.
|
|
125
|
+
* @property {string | undefined} token - Lowercased token when parsing completed; undefined when no token was found.
|
|
126
|
+
* @property {number} index - Index immediately after the parsed token or trivia.
|
|
127
|
+
*/
|
|
128
|
+
|
|
121
129
|
import BacktraceCleaner from "../../utils/backtrace-cleaner.js"
|
|
122
130
|
import { getDatabaseAnnotations } from "../annotations.js"
|
|
123
131
|
import { formatDateForDatabase } from "../datetime-storage.js"
|
|
@@ -134,6 +142,11 @@ import TableForeignKey from "../table-data/table-foreign-key.js"
|
|
|
134
142
|
import wait from "awaitery/build/wait.js"
|
|
135
143
|
import {optionalPositiveInteger} from "typanic"
|
|
136
144
|
|
|
145
|
+
/** Maximum characters inspected when building the debug SQL preview. */
|
|
146
|
+
const SQL_PREVIEW_SCAN_LIMIT = 4096
|
|
147
|
+
/** Maximum characters inspected when deciding whether a statement invalidates schema metadata. */
|
|
148
|
+
const SCHEMA_INVALIDATION_SCAN_LIMIT = 8192
|
|
149
|
+
|
|
137
150
|
/**
|
|
138
151
|
* Marks a callback failure that happened after the owning transaction was durably committed.
|
|
139
152
|
* The public transaction boundary unwraps it before deadlock classification.
|
|
@@ -793,10 +806,82 @@ export default class VelociousDatabaseDriversBase {
|
|
|
793
806
|
return optionalPositiveInteger(this.getArgs().maxInsertSqlBytes, "maxInsertSqlBytes") ?? 1048576
|
|
794
807
|
}
|
|
795
808
|
|
|
809
|
+
/**
|
|
810
|
+
* Maximum values in a single `IN (...)` cohort used by preloads, association
|
|
811
|
+
* counts, and queryData aggregates. The default stays under SQLite's default
|
|
812
|
+
* `MAX_VARIABLE_NUMBER` compile-time limit.
|
|
813
|
+
*
|
|
814
|
+
* Override via `maxInClauseValues` in the database configuration.
|
|
815
|
+
* @returns {number} - Maximum values per IN clause cohort.
|
|
816
|
+
*/
|
|
817
|
+
maxInClauseValues() {
|
|
818
|
+
return optionalPositiveInteger(this.getArgs().maxInClauseValues, "maxInClauseValues") ?? 999
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Maximum serialized SQL size, in bytes, for a single cohort query used by
|
|
823
|
+
* preloads, association counts, and queryData aggregates. Cohort chunking
|
|
824
|
+
* stops when the next value would push the generated string over this threshold.
|
|
825
|
+
*
|
|
826
|
+
* Override via `maxQuerySqlBytes` in the database configuration.
|
|
827
|
+
* @returns {number} - Maximum bytes per cohort query.
|
|
828
|
+
*/
|
|
829
|
+
maxQuerySqlBytes() {
|
|
830
|
+
return optionalPositiveInteger(this.getArgs().maxQuerySqlBytes, "maxQuerySqlBytes") ?? 1048576
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Splits `values` into cohort chunks that stay within both `maxCount` and
|
|
835
|
+
* `maxBytes` while preserving order.
|
|
836
|
+
*
|
|
837
|
+
* A chunk always contains at least one value, even if that single value exceeds
|
|
838
|
+
* the byte limit, so progress is guaranteed.
|
|
839
|
+
* @template T
|
|
840
|
+
* @param {Array<T>} values - Values to chunk.
|
|
841
|
+
* @param {(values: Array<T>) => string} buildSql - Function that builds the full SQL for a candidate chunk.
|
|
842
|
+
* @param {{maxCount?: number, maxBytes?: number}} [options] - Chunking bounds.
|
|
843
|
+
* @returns {Array<Array<T>>} - Value cohorts.
|
|
844
|
+
*/
|
|
845
|
+
chunkValues(values, buildSql, {maxCount = this.maxInClauseValues(), maxBytes = this.maxQuerySqlBytes()} = {}) {
|
|
846
|
+
if (values.length === 0) return []
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Chunks.
|
|
850
|
+
* @type {Array<Array<T>>} */
|
|
851
|
+
const chunks = []
|
|
852
|
+
/**
|
|
853
|
+
* Current chunk.
|
|
854
|
+
* @type {Array<T>} */
|
|
855
|
+
let currentChunk = []
|
|
856
|
+
|
|
857
|
+
for (const value of values) {
|
|
858
|
+
const candidate = [...currentChunk, value]
|
|
859
|
+
const candidateBytes = Buffer.byteLength(buildSql(candidate), "utf8")
|
|
860
|
+
|
|
861
|
+
if (currentChunk.length > 0 && (candidate.length > maxCount || candidateBytes > maxBytes)) {
|
|
862
|
+
chunks.push(currentChunk)
|
|
863
|
+
currentChunk = [value]
|
|
864
|
+
} else {
|
|
865
|
+
currentChunk = candidate
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (currentChunk.length > 0) {
|
|
870
|
+
chunks.push(currentChunk)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return chunks
|
|
874
|
+
}
|
|
875
|
+
|
|
796
876
|
/**
|
|
797
877
|
* Splits `rows` into chunks that stay within both {@link maxRowsPerInsert}
|
|
798
878
|
* and {@link maxInsertSqlBytes} while preserving order.
|
|
799
879
|
*
|
|
880
|
+
* Byte accounting is incremental: `buildSql` is called once with `[]` to
|
|
881
|
+
* measure the statement prefix and once per row with `[row]` to measure the
|
|
882
|
+
* row's values tuple. This keeps chunking linear in the number of rows
|
|
883
|
+
* instead of rebuilding the full multi-row SQL for every candidate.
|
|
884
|
+
*
|
|
800
885
|
* A chunk always contains at least one row, even if that single row exceeds
|
|
801
886
|
* the byte limit, so progress is guaranteed.
|
|
802
887
|
* @param {Array<Array<ReturnType<typeof JSON.parse>>>} rows - Rows to insert.
|
|
@@ -811,6 +896,9 @@ export default class VelociousDatabaseDriversBase {
|
|
|
811
896
|
const prefix = `${emptySql} VALUES `
|
|
812
897
|
const baseByteLength = Buffer.byteLength(prefix, "utf8")
|
|
813
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Current chunk.
|
|
901
|
+
* @type {Array<Array<ReturnType<typeof JSON.parse>>>} */
|
|
814
902
|
let currentChunk = []
|
|
815
903
|
let currentBytes = 0
|
|
816
904
|
|
|
@@ -1604,13 +1692,25 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1604
1692
|
}
|
|
1605
1693
|
}
|
|
1606
1694
|
|
|
1695
|
+
/**
|
|
1696
|
+
* Returns a bounded prefix of `sql` for lightweight diagnostic scanning.
|
|
1697
|
+
* @param {string} sql - SQL string.
|
|
1698
|
+
* @param {number} limit - Maximum code units to inspect.
|
|
1699
|
+
* @returns {string} - Prefix of `sql`.
|
|
1700
|
+
*/
|
|
1701
|
+
_diagnosticSqlPrefix(sql, limit) {
|
|
1702
|
+
return sql.length <= limit ? sql : sql.slice(0, limit)
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1607
1705
|
/**
|
|
1608
1706
|
* Runs debug sql preview.
|
|
1609
1707
|
* @param {string} sql - SQL to preview.
|
|
1610
1708
|
* @returns {string} - Normalized truncated SQL preview for diagnostics.
|
|
1611
1709
|
*/
|
|
1612
1710
|
_debugSqlPreview(sql) {
|
|
1613
|
-
|
|
1711
|
+
const prefix = this._diagnosticSqlPrefix(sql, SQL_PREVIEW_SCAN_LIMIT)
|
|
1712
|
+
|
|
1713
|
+
return prefix
|
|
1614
1714
|
.replace(/\s+/g, " ")
|
|
1615
1715
|
.trim()
|
|
1616
1716
|
.slice(0, 500)
|
|
@@ -1664,25 +1764,121 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1664
1764
|
.replace(/"/g, "'")
|
|
1665
1765
|
}
|
|
1666
1766
|
|
|
1767
|
+
/**
|
|
1768
|
+
* Reads the next SQL token starting at `startIndex`, skipping leading trivia
|
|
1769
|
+
* (BOM, whitespace, block comments, line comments). If the scan cannot finish
|
|
1770
|
+
* skipping trivia before `limit`, the result is marked incomplete so callers
|
|
1771
|
+
* can conservatively treat the statement as schema-invalidating.
|
|
1772
|
+
* @param {string} sql - SQL string.
|
|
1773
|
+
* @param {number} startIndex - Index to start scanning.
|
|
1774
|
+
* @param {number} limit - Maximum absolute index to scan while skipping leading trivia.
|
|
1775
|
+
* @returns {SqlTokenResult} - Token result.
|
|
1776
|
+
*/
|
|
1777
|
+
_readSqlToken(sql, startIndex, limit) {
|
|
1778
|
+
let i = startIndex
|
|
1779
|
+
const len = sql.length
|
|
1780
|
+
|
|
1781
|
+
while (i < len && i < limit) {
|
|
1782
|
+
const char = sql[i]
|
|
1783
|
+
|
|
1784
|
+
if (char === "\ufeff" || /\s/.test(char)) {
|
|
1785
|
+
i++
|
|
1786
|
+
continue
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (char === "/" && sql[i + 1] === "*") {
|
|
1790
|
+
const close = sql.indexOf("*/", i + 2)
|
|
1791
|
+
|
|
1792
|
+
if (close === -1 || close + 2 > limit) {
|
|
1793
|
+
return {incomplete: true, index: i, token: undefined}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
i = close + 2
|
|
1797
|
+
continue
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if (char === "-" && sql[i + 1] === "-") {
|
|
1801
|
+
const newline = sql.indexOf("\n", i + 2)
|
|
1802
|
+
|
|
1803
|
+
if (newline === -1) {
|
|
1804
|
+
return {incomplete: false, index: len, token: undefined}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
if (newline + 1 > limit) {
|
|
1808
|
+
return {incomplete: true, index: i, token: undefined}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
i = newline + 1
|
|
1812
|
+
continue
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
let token = ""
|
|
1816
|
+
|
|
1817
|
+
while (i < len) {
|
|
1818
|
+
const c = sql[i]
|
|
1819
|
+
|
|
1820
|
+
if (/\s/.test(c) || c === "\ufeff") break
|
|
1821
|
+
if (c === "/" && sql[i + 1] === "*") break
|
|
1822
|
+
if (c === "-" && sql[i + 1] === "-") break
|
|
1823
|
+
|
|
1824
|
+
token += c
|
|
1825
|
+
i++
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
return {incomplete: false, token: token.toLowerCase(), index: i}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
if (i >= len) {
|
|
1832
|
+
return {incomplete: false, index: len, token: undefined}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
return {incomplete: true, index: i, token: undefined}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1667
1838
|
/**
|
|
1668
1839
|
* Runs schema cache invalidating sql.
|
|
1669
1840
|
* @param {string} sql - SQL string.
|
|
1670
1841
|
* @returns {boolean} - Whether the SQL should invalidate schema metadata.
|
|
1671
1842
|
*/
|
|
1672
1843
|
_schemaCacheInvalidatingSql(sql) {
|
|
1673
|
-
const
|
|
1674
|
-
.trim()
|
|
1675
|
-
.replace(/^\ufeff/, "")
|
|
1676
|
-
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
1677
|
-
.replace(/--[^\n]*(\n|$)/g, " ")
|
|
1678
|
-
.replace(/\s+/g, " ")
|
|
1679
|
-
.toLowerCase()
|
|
1844
|
+
const first = this._readSqlToken(sql, 0, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1680
1845
|
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
if (
|
|
1846
|
+
if (first.incomplete) return true
|
|
1847
|
+
|
|
1848
|
+
const firstToken = first.token
|
|
1849
|
+
|
|
1850
|
+
if (!firstToken) return false
|
|
1851
|
+
if (/^(create|alter|drop|rename)$/.test(firstToken)) return true
|
|
1852
|
+
|
|
1853
|
+
if (firstToken === "comment") {
|
|
1854
|
+
const next = this._readSqlToken(sql, first.index, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1855
|
+
|
|
1856
|
+
return next.incomplete || next.token === "on"
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
if (firstToken === "exec" || firstToken === "execute") {
|
|
1860
|
+
const next = this._readSqlToken(sql, first.index, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1861
|
+
|
|
1862
|
+
return next.incomplete || next.token === "sp_rename"
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (firstToken === "if") {
|
|
1866
|
+
let index = first.index
|
|
1867
|
+
|
|
1868
|
+
while (true) {
|
|
1869
|
+
const result = this._readSqlToken(sql, index, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1870
|
+
|
|
1871
|
+
if (result.incomplete) return true
|
|
1872
|
+
if (!result.token) return false
|
|
1873
|
+
if (result.token === "begin") {
|
|
1874
|
+
const ddlResult = this._readSqlToken(sql, result.index, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1875
|
+
|
|
1876
|
+
return ddlResult.incomplete || /^(create|alter|drop|rename)$/.test(ddlResult.token || "")
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
index = result.index
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1686
1882
|
|
|
1687
1883
|
return false
|
|
1688
1884
|
}
|
|
@@ -87,25 +87,27 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
|
|
|
87
87
|
if (foreignKeyValues.size > 0) {
|
|
88
88
|
await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration(), modelsToLoad[0])
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const whereArgs = {}
|
|
90
|
+
// Build the query once with scope and selection, then clone it per cohort so
|
|
91
|
+
// the IN-list size stays within driver limits without rebuilding shared state.
|
|
92
|
+
let baseQuery = preloadQueryForModel(modelsToLoad, targetModelClass)
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
baseQuery = this.relationship.applyScope(baseQuery)
|
|
95
|
+
baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [primaryKey]})
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const driver = baseQuery.driver
|
|
98
|
+
const cohorts = driver.chunkValues([...foreignKeyValues], (chunk) => baseQuery.clone().where({[primaryKey]: chunk}).toSql())
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
for (const cohort of cohorts) {
|
|
101
|
+
const cohortQuery = baseQuery.clone().where({[primaryKey]: cohort})
|
|
102
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
targetModels.push(...foundTargetModels)
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
for (const targetModel of foundTargetModels) {
|
|
107
|
+
const primaryKeyValue = /** @type {string | number} */ (targetModel.readColumn(primaryKey))
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
targetModelsById[primaryKeyValue] = targetModel
|
|
110
|
+
}
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -205,33 +207,32 @@ export default class VelociousDatabaseQueryPreloaderBelongsTo {
|
|
|
205
207
|
|
|
206
208
|
await ensureModelClassInitialized(targetModelClass, configuration, this.models[0])
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
* Where args.
|
|
210
|
-
* @type {Record<string, string | number | Array<string | number>>} */
|
|
211
|
-
const whereArgs = {}
|
|
210
|
+
let baseQuery = preloadQueryForModel(this.models, targetModelClass)
|
|
212
211
|
|
|
213
|
-
|
|
212
|
+
baseQuery = this.relationship.applyScope(baseQuery)
|
|
213
|
+
baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [primaryKey]})
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
const driver = baseQuery.driver
|
|
216
|
+
const cohorts = driver.chunkValues([...foreignKeyValuesByType[targetType]], (chunk) => baseQuery.clone().where({[primaryKey]: chunk}).toSql())
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
query = this.selection.applyToQuery({query, targetModelClass, mappingColumns: [primaryKey]})
|
|
218
|
+
targetModelsByTypeAndId[targetType] = {}
|
|
219
219
|
|
|
220
|
-
const
|
|
220
|
+
for (const cohort of cohorts) {
|
|
221
|
+
const cohortQuery = baseQuery.clone().where({[primaryKey]: cohort})
|
|
222
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
221
223
|
|
|
222
|
-
|
|
224
|
+
targetModels.push(...foundTargetModels)
|
|
223
225
|
|
|
224
|
-
|
|
226
|
+
const className = targetModelClass.getModelName()
|
|
225
227
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
if (!targetModelsByClassName[className]) targetModelsByClassName[className] = []
|
|
229
|
+
targetModelsByClassName[className].push(...foundTargetModels)
|
|
228
230
|
|
|
229
|
-
|
|
231
|
+
for (const targetModel of foundTargetModels) {
|
|
232
|
+
const primaryKeyValue = /** @type {string | number} */ (targetModel.readColumn(primaryKey))
|
|
230
233
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
targetModelsByTypeAndId[targetType][primaryKeyValue] = targetModel
|
|
234
|
+
targetModelsByTypeAndId[targetType][primaryKeyValue] = targetModel
|
|
235
|
+
}
|
|
235
236
|
}
|
|
236
237
|
}
|
|
237
238
|
|
|
@@ -155,10 +155,11 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
|
|
|
155
155
|
modelsByPrimaryKeyValue[primaryKeyValue].push(model)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
// Step 1: Query the through table to build parent→target ID mapping
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
// Step 1: Query the through table to build parent→target ID mapping.
|
|
159
|
+
// Chunk the parent PK cohort so the through query's IN-list stays bounded.
|
|
160
|
+
const throughBaseQuery = preloadQueryForModel(modelsToLoad, throughModelClass)
|
|
161
|
+
const throughDriver = throughBaseQuery.driver
|
|
162
|
+
const throughCohorts = throughDriver.chunkValues([...modelsPrimaryKeyValues], (chunk) => throughBaseQuery.clone().where({[throughForeignKey]: chunk}).toSql())
|
|
162
163
|
|
|
163
164
|
/**
|
|
164
165
|
* Parent to target ids.
|
|
@@ -170,28 +171,43 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
|
|
|
170
171
|
* @type {Set<string | number>} */
|
|
171
172
|
const allTargetIds = new Set()
|
|
172
173
|
|
|
173
|
-
for (const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
174
|
+
for (const cohort of throughCohorts) {
|
|
175
|
+
const throughQuery = throughBaseQuery.clone().where({[throughForeignKey]: cohort})
|
|
176
|
+
const throughModels = await throughQuery.toArray()
|
|
176
177
|
|
|
177
|
-
|
|
178
|
+
for (const throughModel of throughModels) {
|
|
179
|
+
const parentId = /** @type {string | number} */ (throughModel.readColumn(throughForeignKey))
|
|
180
|
+
const throughId = /** @type {string | number} */ (throughModel.readColumn(throughModelClass.primaryKey()))
|
|
178
181
|
|
|
179
|
-
|
|
180
|
-
|
|
182
|
+
if (!(parentId in parentToTargetIds)) parentToTargetIds[parentId] = []
|
|
183
|
+
|
|
184
|
+
parentToTargetIds[parentId].push(throughId)
|
|
185
|
+
allTargetIds.add(throughId)
|
|
186
|
+
}
|
|
181
187
|
}
|
|
182
188
|
|
|
183
|
-
// Step 2: Load target models by the foreign key that points to the through table
|
|
189
|
+
// Step 2: Load target models by the foreign key that points to the through table.
|
|
190
|
+
// Chunk the target ID cohort so the target query's IN-list stays bounded.
|
|
184
191
|
/**
|
|
185
192
|
* Target models.
|
|
186
193
|
* @type {import("../../record/index.js").default[]} */
|
|
187
194
|
let targetModels = []
|
|
188
195
|
|
|
189
196
|
if (allTargetIds.size > 0) {
|
|
190
|
-
let
|
|
197
|
+
let targetBaseQuery = preloadQueryForModel(modelsToLoad, targetModelClass)
|
|
198
|
+
|
|
199
|
+
targetBaseQuery = this.relationship.applyScope(targetBaseQuery)
|
|
200
|
+
targetBaseQuery = this.selection.applyToQuery({query: targetBaseQuery, targetModelClass, mappingColumns: [targetForeignKey]})
|
|
201
|
+
|
|
202
|
+
const targetDriver = targetBaseQuery.driver
|
|
203
|
+
const targetCohorts = targetDriver.chunkValues([...allTargetIds], (chunk) => targetBaseQuery.clone().where({[targetForeignKey]: chunk}).toSql())
|
|
191
204
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
for (const cohort of targetCohorts) {
|
|
206
|
+
const cohortQuery = targetBaseQuery.clone().where({[targetForeignKey]: cohort})
|
|
207
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
208
|
+
|
|
209
|
+
targetModels.push(...foundTargetModels)
|
|
210
|
+
}
|
|
195
211
|
}
|
|
196
212
|
|
|
197
213
|
// Step 3: Index target models by their foreign key (maps to through model ID)
|
|
@@ -285,27 +301,35 @@ export default class VelociousDatabaseQueryPreloaderHasMany {
|
|
|
285
301
|
modelsByPrimaryKeyValue[primaryKeyValue].push(model)
|
|
286
302
|
}
|
|
287
303
|
|
|
288
|
-
|
|
289
|
-
* Where args.
|
|
290
|
-
* @type {Record<string, string | number | Array<string | number>>} */
|
|
291
|
-
const whereArgs = {}
|
|
304
|
+
await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration(), modelsToLoad[0])
|
|
292
305
|
|
|
293
|
-
|
|
306
|
+
// Build the query once with the polymorphic type constant (when present),
|
|
307
|
+
// relationship scope, and selection. The parent ID IN-list is cloned per cohort
|
|
308
|
+
// so the generated SQL stays within driver limits.
|
|
309
|
+
let baseQuery = preloadQueryForModel(modelsToLoad, targetModelClass)
|
|
294
310
|
|
|
295
311
|
if (this.relationship.getPolymorphic()) {
|
|
296
312
|
const typeColumn = this.relationship.getPolymorphicTypeColumn()
|
|
297
313
|
|
|
298
|
-
|
|
314
|
+
baseQuery = baseQuery.where({[typeColumn]: this.relationship.getModelClass().getModelName()})
|
|
299
315
|
}
|
|
300
316
|
|
|
301
|
-
|
|
317
|
+
baseQuery = this.relationship.applyScope(baseQuery)
|
|
318
|
+
baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [foreignKey]})
|
|
302
319
|
|
|
303
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Target models.
|
|
322
|
+
* @type {import("../../record/index.js").default[]} */
|
|
323
|
+
const targetModels = []
|
|
324
|
+
const driver = baseQuery.driver
|
|
325
|
+
const cohorts = driver.chunkValues([...modelsPrimaryKeyValues], (chunk) => baseQuery.clone().where({[foreignKey]: chunk}).toSql())
|
|
304
326
|
|
|
305
|
-
|
|
306
|
-
|
|
327
|
+
for (const cohort of cohorts) {
|
|
328
|
+
const cohortQuery = baseQuery.clone().where({[foreignKey]: cohort})
|
|
329
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
307
330
|
|
|
308
|
-
|
|
331
|
+
targetModels.push(...foundTargetModels)
|
|
332
|
+
}
|
|
309
333
|
|
|
310
334
|
for (const targetModel of targetModels) {
|
|
311
335
|
const foreignKeyValue = /** @type {string | number} */ (targetModel.readColumn(foreignKey))
|
|
@@ -72,28 +72,36 @@ export default class VelociousDatabaseQueryPreloaderHasOne {
|
|
|
72
72
|
|
|
73
73
|
if (modelsPrimaryKeyValues.size == 0) return satisfiedTargets
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
* Where args.
|
|
77
|
-
* @type {Record<string, string | number | Array<string | number>>} */
|
|
78
|
-
const whereArgs = {}
|
|
75
|
+
await ensureModelClassInitialized(targetModelClass, this.relationship.getConfiguration(), this.models[0])
|
|
79
76
|
|
|
80
|
-
|
|
77
|
+
// Load target models to be preloaded on the given models.
|
|
78
|
+
// Build the query once with the polymorphic type constant (when present),
|
|
79
|
+
// relationship scope, and selection. The parent ID IN-list is cloned per cohort
|
|
80
|
+
// so the generated SQL stays within driver limits.
|
|
81
|
+
let baseQuery = preloadQueryForModel(this.models, targetModelClass)
|
|
81
82
|
|
|
82
83
|
if (this.relationship.getPolymorphic()) {
|
|
83
84
|
const typeColumn = this.relationship.getPolymorphicTypeColumn()
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
baseQuery = baseQuery.where({[typeColumn]: this.relationship.getModelClass().getModelName()})
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
baseQuery = this.relationship.applyScope(baseQuery)
|
|
90
|
+
baseQuery = this.selection.applyToQuery({query: baseQuery, targetModelClass, mappingColumns: [foreignKey]})
|
|
89
91
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Target models.
|
|
94
|
+
* @type {import("../../record/index.js").default[]} */
|
|
95
|
+
const targetModels = []
|
|
96
|
+
const driver = baseQuery.driver
|
|
97
|
+
const cohorts = driver.chunkValues([...modelsPrimaryKeyValues], (chunk) => baseQuery.clone().where({[foreignKey]: chunk}).toSql())
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
for (const cohort of cohorts) {
|
|
100
|
+
const cohortQuery = baseQuery.clone().where({[foreignKey]: cohort})
|
|
101
|
+
const foundTargetModels = await cohortQuery.toArray()
|
|
95
102
|
|
|
96
|
-
|
|
103
|
+
targetModels.push(...foundTargetModels)
|
|
104
|
+
}
|
|
97
105
|
|
|
98
106
|
for (const targetModel of targetModels) {
|
|
99
107
|
const foreignKeyValue = /** @type {string | number} */ (targetModel.readColumn(foreignKey))
|
|
@@ -227,7 +227,7 @@ export async function runQueryData({rootModelClass, rootModels, entries}) {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
for (const {query} of queryGroups) {
|
|
230
|
-
await
|
|
230
|
+
await executeChunkedEntryQuery({primaryKey, query, rootIds, rootModels})
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
@@ -267,13 +267,6 @@ function prepareEntry({entry, entryIndex, primaryKey, rootIds, rootModelClass, s
|
|
|
267
267
|
const rootTable = rootModelClass.tableName()
|
|
268
268
|
const rootPkSql = `${driver.quoteTable(rootTable)}.${driver.quoteColumn(primaryKey)}`
|
|
269
269
|
|
|
270
|
-
/**
|
|
271
|
-
* Root where.
|
|
272
|
-
* @type {Record<string, ReturnType<typeof JSON.parse>>} */
|
|
273
|
-
const rootWhere = {}
|
|
274
|
-
rootWhere[primaryKey] = rootIds
|
|
275
|
-
query.where(rootWhere)
|
|
276
|
-
|
|
277
270
|
const joinDescriptor = buildNestedJoinDescriptor(entry.chain)
|
|
278
271
|
|
|
279
272
|
if (joinDescriptor !== true) {
|
|
@@ -370,3 +363,25 @@ async function executeEntryQuery({primaryKey, query, rootModels}) {
|
|
|
370
363
|
}
|
|
371
364
|
}
|
|
372
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Executes one compatible queryData group in cohorts so the root ID IN-list
|
|
369
|
+
* stays within driver limits, attaching each selected alias to the matching
|
|
370
|
+
* root record.
|
|
371
|
+
* @param {object} args - Options.
|
|
372
|
+
* @param {string} args.primaryKey - Root model primary key column.
|
|
373
|
+
* @param {import("./model-class-query.js").default} args.query - Prepared grouped query.
|
|
374
|
+
* @param {Array<string | number>} args.rootIds - Root primary-key values.
|
|
375
|
+
* @param {import("../record/index.js").default[]} args.rootModels - Loaded root records.
|
|
376
|
+
* @returns {Promise<void>}
|
|
377
|
+
*/
|
|
378
|
+
async function executeChunkedEntryQuery({primaryKey, query, rootIds, rootModels}) {
|
|
379
|
+
const driver = query.driver
|
|
380
|
+
const cohorts = driver.chunkValues(rootIds, (chunk) => query.clone().where({[primaryKey]: chunk}).toSql())
|
|
381
|
+
|
|
382
|
+
for (const cohort of cohorts) {
|
|
383
|
+
const cohortQuery = query.clone().where({[primaryKey]: cohort})
|
|
384
|
+
|
|
385
|
+
await executeEntryQuery({primaryKey, query: cohortQuery, rootModels})
|
|
386
|
+
}
|
|
387
|
+
}
|