@itwin/core-backend 5.15.0-dev.13 → 5.15.0-dev.15
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/lib/cjs/BriefcaseManager.d.ts +2 -1
- package/lib/cjs/BriefcaseManager.d.ts.map +1 -1
- package/lib/cjs/BriefcaseManager.js +39 -9
- package/lib/cjs/BriefcaseManager.js.map +1 -1
- package/lib/cjs/ChangeUnifier.d.ts +82 -0
- package/lib/cjs/ChangeUnifier.d.ts.map +1 -0
- package/lib/cjs/ChangeUnifier.js +163 -0
- package/lib/cjs/ChangeUnifier.js.map +1 -0
- package/lib/cjs/ChangesetReader.d.ts +21 -2
- package/lib/cjs/ChangesetReader.d.ts.map +1 -1
- package/lib/cjs/ChangesetReader.js +37 -1
- package/lib/cjs/ChangesetReader.js.map +1 -1
- package/lib/cjs/ECDb.d.ts +1 -0
- package/lib/cjs/ECDb.d.ts.map +1 -1
- package/lib/cjs/ECDb.js +4 -1
- package/lib/cjs/ECDb.js.map +1 -1
- package/lib/cjs/IModelDb.d.ts +6 -1
- package/lib/cjs/IModelDb.d.ts.map +1 -1
- package/lib/cjs/IModelDb.js +16 -5
- package/lib/cjs/IModelDb.js.map +1 -1
- package/lib/cjs/NativeHost.d.ts.map +1 -1
- package/lib/cjs/NativeHost.js +5 -1
- package/lib/cjs/NativeHost.js.map +1 -1
- package/lib/cjs/Relationship.d.ts +28 -0
- package/lib/cjs/Relationship.d.ts.map +1 -1
- package/lib/cjs/Relationship.js +78 -0
- package/lib/cjs/Relationship.js.map +1 -1
- package/lib/cjs/SchemaSync.d.ts.map +1 -1
- package/lib/cjs/SchemaSync.js +1 -0
- package/lib/cjs/SchemaSync.js.map +1 -1
- package/lib/cjs/TxnManager.d.ts +126 -89
- package/lib/cjs/TxnManager.d.ts.map +1 -1
- package/lib/cjs/TxnManager.js +109 -75
- package/lib/cjs/TxnManager.js.map +1 -1
- package/lib/cjs/annotations/LeaderGeometry.d.ts.map +1 -1
- package/lib/cjs/annotations/LeaderGeometry.js +3 -0
- package/lib/cjs/annotations/LeaderGeometry.js.map +1 -1
- package/lib/cjs/core-backend.d.ts +1 -0
- package/lib/cjs/core-backend.d.ts.map +1 -1
- package/lib/cjs/core-backend.js +1 -0
- package/lib/cjs/core-backend.js.map +1 -1
- package/lib/cjs/internal/BriefcaseUtils.d.ts +19 -0
- package/lib/cjs/internal/BriefcaseUtils.d.ts.map +1 -0
- package/lib/cjs/internal/BriefcaseUtils.js +65 -0
- package/lib/cjs/internal/BriefcaseUtils.js.map +1 -0
- package/lib/cjs/internal/QueryMetadataCache.d.ts +44 -0
- package/lib/cjs/internal/QueryMetadataCache.d.ts.map +1 -0
- package/lib/cjs/internal/QueryMetadataCache.js +107 -0
- package/lib/cjs/internal/QueryMetadataCache.js.map +1 -0
- package/lib/cjs/internal/Symbols.d.ts +6 -0
- package/lib/cjs/internal/Symbols.d.ts.map +1 -1
- package/lib/cjs/internal/Symbols.js +7 -1
- package/lib/cjs/internal/Symbols.js.map +1 -1
- package/lib/esm/BriefcaseManager.d.ts +2 -1
- package/lib/esm/BriefcaseManager.d.ts.map +1 -1
- package/lib/esm/BriefcaseManager.js +40 -10
- package/lib/esm/BriefcaseManager.js.map +1 -1
- package/lib/esm/ChangeUnifier.d.ts +82 -0
- package/lib/esm/ChangeUnifier.d.ts.map +1 -0
- package/lib/esm/ChangeUnifier.js +159 -0
- package/lib/esm/ChangeUnifier.js.map +1 -0
- package/lib/esm/ChangesetReader.d.ts +21 -2
- package/lib/esm/ChangesetReader.d.ts.map +1 -1
- package/lib/esm/ChangesetReader.js +38 -2
- package/lib/esm/ChangesetReader.js.map +1 -1
- package/lib/esm/ECDb.d.ts +1 -0
- package/lib/esm/ECDb.d.ts.map +1 -1
- package/lib/esm/ECDb.js +4 -1
- package/lib/esm/ECDb.js.map +1 -1
- package/lib/esm/IModelDb.d.ts +6 -1
- package/lib/esm/IModelDb.d.ts.map +1 -1
- package/lib/esm/IModelDb.js +17 -6
- package/lib/esm/IModelDb.js.map +1 -1
- package/lib/esm/NativeHost.d.ts.map +1 -1
- package/lib/esm/NativeHost.js +5 -1
- package/lib/esm/NativeHost.js.map +1 -1
- package/lib/esm/Relationship.d.ts +28 -0
- package/lib/esm/Relationship.d.ts.map +1 -1
- package/lib/esm/Relationship.js +78 -0
- package/lib/esm/Relationship.js.map +1 -1
- package/lib/esm/SchemaSync.d.ts.map +1 -1
- package/lib/esm/SchemaSync.js +1 -0
- package/lib/esm/SchemaSync.js.map +1 -1
- package/lib/esm/TxnManager.d.ts +126 -89
- package/lib/esm/TxnManager.d.ts.map +1 -1
- package/lib/esm/TxnManager.js +110 -76
- package/lib/esm/TxnManager.js.map +1 -1
- package/lib/esm/annotations/LeaderGeometry.d.ts.map +1 -1
- package/lib/esm/annotations/LeaderGeometry.js +3 -0
- package/lib/esm/annotations/LeaderGeometry.js.map +1 -1
- package/lib/esm/core-backend.d.ts +1 -0
- package/lib/esm/core-backend.d.ts.map +1 -1
- package/lib/esm/core-backend.js +1 -0
- package/lib/esm/core-backend.js.map +1 -1
- package/lib/esm/internal/BriefcaseUtils.d.ts +19 -0
- package/lib/esm/internal/BriefcaseUtils.d.ts.map +1 -0
- package/lib/esm/internal/BriefcaseUtils.js +61 -0
- package/lib/esm/internal/BriefcaseUtils.js.map +1 -0
- package/lib/esm/internal/QueryMetadataCache.d.ts +44 -0
- package/lib/esm/internal/QueryMetadataCache.d.ts.map +1 -0
- package/lib/esm/internal/QueryMetadataCache.js +103 -0
- package/lib/esm/internal/QueryMetadataCache.js.map +1 -0
- package/lib/esm/internal/Symbols.d.ts +6 -0
- package/lib/esm/internal/Symbols.d.ts.map +1 -1
- package/lib/esm/internal/Symbols.js +6 -0
- package/lib/esm/internal/Symbols.js.map +1 -1
- package/lib/esm/test/annotations/LeaderGeometry.test.js +15 -1
- package/lib/esm/test/annotations/LeaderGeometry.test.js.map +1 -1
- package/lib/esm/test/ecdb/ConcurrentQuery.test.js +35 -16
- package/lib/esm/test/ecdb/ConcurrentQuery.test.js.map +1 -1
- package/lib/esm/test/ecdb/QueryMetadataCache.test.d.ts +2 -0
- package/lib/esm/test/ecdb/QueryMetadataCache.test.d.ts.map +1 -0
- package/lib/esm/test/ecdb/QueryMetadataCache.test.js +630 -0
- package/lib/esm/test/ecdb/QueryMetadataCache.test.js.map +1 -0
- package/lib/esm/test/element/RelationshipBatchLookup.test.d.ts +2 -0
- package/lib/esm/test/element/RelationshipBatchLookup.test.d.ts.map +1 -0
- package/lib/esm/test/element/RelationshipBatchLookup.test.js +202 -0
- package/lib/esm/test/element/RelationshipBatchLookup.test.js.map +1 -0
- package/lib/esm/test/hubaccess/BriefcaseManager.test.js +187 -1
- package/lib/esm/test/hubaccess/BriefcaseManager.test.js.map +1 -1
- package/lib/esm/test/standalone/BriefcaseManagerDelete.test.d.ts +2 -0
- package/lib/esm/test/standalone/BriefcaseManagerDelete.test.d.ts.map +1 -0
- package/lib/esm/test/standalone/BriefcaseManagerDelete.test.js +159 -0
- package/lib/esm/test/standalone/BriefcaseManagerDelete.test.js.map +1 -0
- package/lib/esm/test/standalone/ChangeUnifier.test.d.ts +2 -0
- package/lib/esm/test/standalone/ChangeUnifier.test.d.ts.map +1 -0
- package/lib/esm/test/standalone/ChangeUnifier.test.js +732 -0
- package/lib/esm/test/standalone/ChangeUnifier.test.js.map +1 -0
- package/package.json +17 -17
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
53
|
+
/*---------------------------------------------------------------------------------------------
|
|
54
|
+
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
|
|
55
|
+
* See LICENSE.md in the project root for license terms and full copyright notice.
|
|
56
|
+
*--------------------------------------------------------------------------------------------*/
|
|
57
|
+
import { DbResult, Id64, IModelStatus } from "@itwin/core-bentley";
|
|
58
|
+
import { Code, ColorDef, IModel, IModelError, SubCategoryAppearance } from "@itwin/core-common";
|
|
59
|
+
import { assert, expect } from "chai";
|
|
60
|
+
import * as path from "node:path";
|
|
61
|
+
import { DrawingCategory } from "../../Category";
|
|
62
|
+
import { ChangesetReader } from "../../ChangesetReader";
|
|
63
|
+
import { PropertyFilter } from "../../ChangesetReaderTypes";
|
|
64
|
+
import { ChangeUnifier } from "../../ChangeUnifier";
|
|
65
|
+
import { ChannelControl } from "../../core-backend";
|
|
66
|
+
import { EditTxn } from "../../EditTxn";
|
|
67
|
+
import { HubMock } from "../../internal/HubMock";
|
|
68
|
+
import { _appendToNativeUnifier } from "../../internal/Symbols";
|
|
69
|
+
import { IModelNative } from "../../internal/NativePlatform";
|
|
70
|
+
import { ChangeUnifierCache, PartialChangeUnifier } from "../../PartialChangeUnifier";
|
|
71
|
+
import { HubWrappers, IModelTestUtils } from "../IModelTestUtils";
|
|
72
|
+
import { KnownTestLocations } from "../KnownTestLocations";
|
|
73
|
+
import "../TestUtils"; // registers the global mocha before/after hooks that start/stop the backend
|
|
74
|
+
/* eslint-disable @typescript-eslint/naming-convention */ // EC property names are PascalCase
|
|
75
|
+
/** `true` once the native addon implements `ChangeUnifier` (iTwin/itwinjs-core#9761). Until then, tests that need it are skipped. */
|
|
76
|
+
function isNativeChangeUnifierAvailable() {
|
|
77
|
+
return typeof IModelNative.platform.ChangeUnifier === "function";
|
|
78
|
+
}
|
|
79
|
+
function queryTableInfo(db) {
|
|
80
|
+
const tables = new Map();
|
|
81
|
+
db.withSqliteStatement("SELECT Id, ParentTableId, Name, Type, ExclusiveRootClassId FROM ec_Table", (stmt) => {
|
|
82
|
+
while (stmt.step() === DbResult.BE_SQLITE_ROW) {
|
|
83
|
+
tables.set(stmt.getValueId(0), {
|
|
84
|
+
parentId: stmt.isValueNull(1) ? undefined : stmt.getValueId(1),
|
|
85
|
+
name: stmt.getValueString(2),
|
|
86
|
+
isOverflow: stmt.getValueInteger(3) === 3, // DbTable::Type::Overflow
|
|
87
|
+
exclusiveRootClassId: stmt.isValueNull(4) ? undefined : stmt.getValueId(4),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const overflowTables = new Set();
|
|
92
|
+
const rootClassIds = new Map();
|
|
93
|
+
for (const table of tables.values()) {
|
|
94
|
+
if (table.isOverflow)
|
|
95
|
+
overflowTables.add(table.name);
|
|
96
|
+
// Joined and overflow tables reference their parent table; the primary table of the class family has no parent.
|
|
97
|
+
let primary = table;
|
|
98
|
+
while (primary?.parentId !== undefined)
|
|
99
|
+
primary = tables.get(primary.parentId);
|
|
100
|
+
if (primary?.exclusiveRootClassId !== undefined)
|
|
101
|
+
rootClassIds.set(table.name, BigInt(primary.exclusiveRootClassId));
|
|
102
|
+
}
|
|
103
|
+
return { overflowTables, rootClassIds };
|
|
104
|
+
}
|
|
105
|
+
/** The order of [[ChangeUnifier.instances]]: numeric (root class id, ECInstanceId, stage), with "Old" before "New". */
|
|
106
|
+
function sortKeyOf(instance, tableInfo) {
|
|
107
|
+
const table = instance.$meta.tables[0];
|
|
108
|
+
const rootClassId = tableInfo.rootClassIds.get(table);
|
|
109
|
+
assert.isDefined(rootClassId, `root class of table ${table}`);
|
|
110
|
+
return [rootClassId, BigInt(instance.ECInstanceId), BigInt(instance.$meta.stage === "Old" ? 0 : 1)];
|
|
111
|
+
}
|
|
112
|
+
function compareSortKeys(lhs, rhs) {
|
|
113
|
+
for (let i = 0; i < lhs.length; ++i) {
|
|
114
|
+
if (lhs[i] !== rhs[i])
|
|
115
|
+
return lhs[i] < rhs[i] ? -1 : 1;
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
/** The key PartialChangeUnifier merges partial instances by. */
|
|
120
|
+
function partialChangeUnifierKey(instance) {
|
|
121
|
+
return `${instance.$meta.instanceKey}-${instance.$meta.stage}`.toLowerCase();
|
|
122
|
+
}
|
|
123
|
+
function unifyWithPartialChangeUnifier(readers, tableInfo) {
|
|
124
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
125
|
+
try {
|
|
126
|
+
const mainTableRows = new Map();
|
|
127
|
+
const unifier = __addDisposableResource(env_1, new PartialChangeUnifier(ChangeUnifierCache.createInMemoryCache()), false);
|
|
128
|
+
for (const reader of readers) {
|
|
129
|
+
while (reader.step()) {
|
|
130
|
+
if (!reader.isECTable)
|
|
131
|
+
continue;
|
|
132
|
+
if (!tableInfo.overflowTables.has(reader.tableName)) {
|
|
133
|
+
for (const partial of [reader.inserted, reader.deleted]) {
|
|
134
|
+
if (partial === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
const key = partialChangeUnifierKey(partial);
|
|
137
|
+
const rows = mainTableRows.get(key) ?? { ops: new Set(), isIndirect: new Set() };
|
|
138
|
+
rows.ops.add(reader.op);
|
|
139
|
+
rows.isIndirect.add(reader.isIndirectChange);
|
|
140
|
+
mainTableRows.set(key, rows);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
unifier.appendFrom(reader);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const instances = Array.from(unifier.instances);
|
|
147
|
+
instances.sort((lhs, rhs) => compareSortKeys(sortKeyOf(lhs, tableInfo), sortKeyOf(rhs, tableInfo)));
|
|
148
|
+
return { instances, mainTableRows };
|
|
149
|
+
}
|
|
150
|
+
catch (e_1) {
|
|
151
|
+
env_1.error = e_1;
|
|
152
|
+
env_1.hasError = true;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
__disposeResources(env_1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function unifyNatively(readers, args) {
|
|
159
|
+
const env_2 = { stack: [], error: void 0, hasError: false };
|
|
160
|
+
try {
|
|
161
|
+
const unifier = __addDisposableResource(env_2, ChangeUnifier.fromReaders(readers, args), false);
|
|
162
|
+
return Array.from(unifier.instances());
|
|
163
|
+
}
|
|
164
|
+
catch (e_2) {
|
|
165
|
+
env_2.error = e_2;
|
|
166
|
+
env_2.hasError = true;
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
__disposeResources(env_2);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Assert that the instances of [[ChangeUnifier]] equal those of [[PartialChangeUnifier]], except for the documented differences:
|
|
174
|
+
* - ChangeUnifier sorts numerically by (root class id, ECInstanceId, stage).
|
|
175
|
+
* - ChangeUnifier takes `op` and `isIndirectChange` from the rows of main (non-overflow) tables. If only overflow tables
|
|
176
|
+
* contributed, `op` is "Updated" and `isIndirectChange` comes from the first row. PartialChangeUnifier takes both from the first row.
|
|
177
|
+
* @param severalReaders `true` if the instances merge the rows of several readers, in which case the same table or
|
|
178
|
+
* change index can contribute more than once, so `tables` and `changeIndexes` are compared as sets.
|
|
179
|
+
*/
|
|
180
|
+
function assertMatchesPartialChangeUnifier(actual, expected, tableInfo, severalReaders = false) {
|
|
181
|
+
for (let i = 1; i < actual.length; ++i)
|
|
182
|
+
assert.isBelow(compareSortKeys(sortKeyOf(actual[i - 1], tableInfo), sortKeyOf(actual[i], tableInfo)), 0, `instances ${i - 1} and ${i} are not sorted`);
|
|
183
|
+
assert.equal(actual.length, expected.instances.length, "number of instances");
|
|
184
|
+
actual.forEach((actualInstance, i) => {
|
|
185
|
+
const expectedInstance = expected.instances[i];
|
|
186
|
+
const { $meta: actualMeta, ...actualProps } = actualInstance;
|
|
187
|
+
const { $meta: expectedMeta, ...expectedProps } = expectedInstance;
|
|
188
|
+
const what = `${expectedMeta.instanceKey} ${expectedMeta.stage}`;
|
|
189
|
+
assert.deepEqual(actualProps, expectedProps, `${what}: properties`);
|
|
190
|
+
assert.sameMembers(Object.keys(actualMeta), Object.keys(expectedMeta), `${what}: $meta keys`);
|
|
191
|
+
assert.equal(actualMeta.instanceKey, expectedMeta.instanceKey, `${what}: instanceKey`);
|
|
192
|
+
assert.equal(actualMeta.stage, expectedMeta.stage, `${what}: stage`);
|
|
193
|
+
assert.equal(actualMeta.propFilter, expectedMeta.propFilter, `${what}: propFilter`);
|
|
194
|
+
assert.deepEqual(actualMeta.rowOptions, expectedMeta.rowOptions, `${what}: rowOptions`);
|
|
195
|
+
assert.deepEqual(actualMeta.changeFetchedPropNames, expectedMeta.changeFetchedPropNames, `${what}: changeFetchedPropNames`);
|
|
196
|
+
if (severalReaders) {
|
|
197
|
+
assert.sameMembers([...new Set(actualMeta.tables)], [...new Set(expectedMeta.tables)], `${what}: tables`);
|
|
198
|
+
assert.sameMembers([...new Set(actualMeta.changeIndexes)], [...new Set(expectedMeta.changeIndexes)], `${what}: changeIndexes`);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
assert.deepEqual(actualMeta.tables, expectedMeta.tables, `${what}: tables`);
|
|
202
|
+
assert.deepEqual(actualMeta.changeIndexes, expectedMeta.changeIndexes, `${what}: changeIndexes`);
|
|
203
|
+
}
|
|
204
|
+
const mainTableRows = expected.mainTableRows.get(partialChangeUnifierKey(expectedInstance));
|
|
205
|
+
if (mainTableRows === undefined) {
|
|
206
|
+
assert.equal(actualMeta.op, "Updated", `${what}: op of an instance only overflow tables contributed to`);
|
|
207
|
+
assert.equal(actualMeta.isIndirectChange, expectedMeta.isIndirectChange, `${what}: isIndirectChange`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
assert.include([...mainTableRows.ops], actualMeta.op, `${what}: op`);
|
|
211
|
+
assert.include([...mainTableRows.isIndirect], actualMeta.isIndirectChange, `${what}: isIndirectChange`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function startTestTxn(iModel, description) {
|
|
216
|
+
const txn = new EditTxn(iModel, description);
|
|
217
|
+
txn.start();
|
|
218
|
+
return txn;
|
|
219
|
+
}
|
|
220
|
+
async function importSchemaStrings(txn, schemas) {
|
|
221
|
+
if (txn.isActive)
|
|
222
|
+
txn.saveChanges();
|
|
223
|
+
await txn.iModel.importSchemaStrings(schemas);
|
|
224
|
+
}
|
|
225
|
+
/** Wait so that LastMod on bis_Model gets a distinct timestamp in the next changeset. */
|
|
226
|
+
async function waitForDistinctLastMod() {
|
|
227
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
228
|
+
}
|
|
229
|
+
describe("ChangeUnifier", () => {
|
|
230
|
+
const accessToken = "super manager token";
|
|
231
|
+
const wideElementPropCount = 36; // enough to spill properties p34 and p35 into bis_GeometricElement2d_Overflow
|
|
232
|
+
let rwIModel;
|
|
233
|
+
let txn;
|
|
234
|
+
let tableInfo;
|
|
235
|
+
/** Paths of the changesets pushed by the test setup, except the one that imports the schema. */
|
|
236
|
+
const changesets = { inserts: "", updates: "", deletes: "", bulkInserts: "" };
|
|
237
|
+
/** Ids of CuTest:CuElements `e[0]`..`e[4]`, CuTest:CuWideElements `w0` and `w1` and CuTest:CuRefersTo relationships `r0` and `r1`. */
|
|
238
|
+
const ids = { e: [], w0: "", w1: "", r0: "", r1: "" };
|
|
239
|
+
/** Label of e4: a string that starts with the header BeJsValue uses for binary data. */
|
|
240
|
+
const base64LikeLabel = "encoding=base64;SGVsbG8=";
|
|
241
|
+
function db() {
|
|
242
|
+
assert.isDefined(rwIModel);
|
|
243
|
+
return rwIModel;
|
|
244
|
+
}
|
|
245
|
+
function openReader(fileName, options = {}) {
|
|
246
|
+
const reader = ChangesetReader.openFile({ db: db(), fileName, propFilter: options.propFilter, rowOptions: options.rowOptions, invert: options.invert });
|
|
247
|
+
options.configure?.(reader);
|
|
248
|
+
return reader;
|
|
249
|
+
}
|
|
250
|
+
/** Opens each reader when it is requested and disposes it when the next one is requested, as documented for [[ChangeUnifier.fromReaders]]. */
|
|
251
|
+
function* openReaders(fileNames, options = {}) {
|
|
252
|
+
for (const fileName of fileNames) {
|
|
253
|
+
const env_3 = { stack: [], error: void 0, hasError: false };
|
|
254
|
+
try {
|
|
255
|
+
const reader = __addDisposableResource(env_3, openReader(fileName, options), false);
|
|
256
|
+
yield reader;
|
|
257
|
+
}
|
|
258
|
+
catch (e_3) {
|
|
259
|
+
env_3.error = e_3;
|
|
260
|
+
env_3.hasError = true;
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
__disposeResources(env_3);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
before(async function () {
|
|
268
|
+
if (!isNativeChangeUnifierAvailable())
|
|
269
|
+
this.skip();
|
|
270
|
+
HubMock.startup("ChangeUnifier", KnownTestLocations.outputDir);
|
|
271
|
+
const iTwinId = HubMock.iTwinId;
|
|
272
|
+
const iModelId = await HubMock.createNewIModel({ iTwinId, iModelName: "changeUnifier", description: "ChangeUnifier", accessToken });
|
|
273
|
+
const iModel = rwIModel = await HubWrappers.downloadAndOpenBriefcase({ iTwinId, iModelId, accessToken });
|
|
274
|
+
const editTxn = txn = startTestTxn(iModel, "ChangeUnifier setup");
|
|
275
|
+
// Changeset 0: schema, drawing model and category.
|
|
276
|
+
const schema = `<?xml version="1.0" encoding="UTF-8"?>
|
|
277
|
+
<ECSchema schemaName="CuTest" alias="cut" version="01.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1">
|
|
278
|
+
<ECSchemaReference name="BisCore" version="01.00" alias="bis"/>
|
|
279
|
+
<ECEntityClass typeName="CuElement">
|
|
280
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
281
|
+
<ECProperty propertyName="Label" typeName="string"/>
|
|
282
|
+
<ECProperty propertyName="Num" typeName="int"/>
|
|
283
|
+
<ECProperty propertyName="Pt" typeName="point2d"/>
|
|
284
|
+
<ECProperty propertyName="Bin" typeName="binary"/>
|
|
285
|
+
</ECEntityClass>
|
|
286
|
+
<ECEntityClass typeName="CuWideElement">
|
|
287
|
+
<BaseClass>bis:GraphicalElement2d</BaseClass>
|
|
288
|
+
${Array.from({ length: wideElementPropCount }, (_, i) => `<ECProperty propertyName="p${i}" typeName="string"/>`).join("\n ")}
|
|
289
|
+
</ECEntityClass>
|
|
290
|
+
<ECRelationshipClass typeName="CuRefersTo" strength="referencing" modifier="Sealed">
|
|
291
|
+
<BaseClass>bis:ElementRefersToElements</BaseClass>
|
|
292
|
+
<Source multiplicity="(0..*)" roleLabel="refers to" polymorphic="true"><Class class="CuElement"/></Source>
|
|
293
|
+
<Target multiplicity="(0..*)" roleLabel="is referenced by" polymorphic="true"><Class class="CuElement"/></Target>
|
|
294
|
+
</ECRelationshipClass>
|
|
295
|
+
</ECSchema>`;
|
|
296
|
+
await importSchemaStrings(editTxn, [schema]);
|
|
297
|
+
iModel.channels.addAllowedChannel(ChannelControl.sharedChannelName);
|
|
298
|
+
await iModel.locks.acquireLocks({ shared: IModel.dictionaryId });
|
|
299
|
+
const modelCode = Code.createEmpty();
|
|
300
|
+
modelCode.value = "ChangeUnifierDrawing";
|
|
301
|
+
const [, drawingModelId] = IModelTestUtils.createAndInsertDrawingPartitionAndModel(editTxn, modelCode, true);
|
|
302
|
+
const categoryId = DrawingCategory.queryCategoryIdByName(iModel, IModel.dictionaryId, "ChangeUnifierCategory")
|
|
303
|
+
?? DrawingCategory.insert(editTxn, IModel.dictionaryId, "ChangeUnifierCategory", new SubCategoryAppearance({ color: ColorDef.fromString("rgb(0,128,255)").toJSON() }));
|
|
304
|
+
editTxn.saveChanges("setup");
|
|
305
|
+
await iModel.pushChanges({ description: "setup", accessToken });
|
|
306
|
+
const insertElement = (classFullName, props) => editTxn.insertElement({
|
|
307
|
+
classFullName,
|
|
308
|
+
model: drawingModelId,
|
|
309
|
+
category: categoryId,
|
|
310
|
+
code: Code.createEmpty(),
|
|
311
|
+
...props,
|
|
312
|
+
});
|
|
313
|
+
const wideProps = (prefix, count) => Object.fromEntries(Array.from({ length: count }, (_, i) => [`p${i}`, `${prefix}-${i}`]));
|
|
314
|
+
// Changeset 1: insert elements - including properties in the overflow table - and relationships.
|
|
315
|
+
await waitForDistinctLastMod();
|
|
316
|
+
await iModel.locks.acquireLocks({ shared: drawingModelId });
|
|
317
|
+
for (let i = 0; i < 5; ++i)
|
|
318
|
+
ids.e.push(insertElement("CuTest:CuElement", { Label: i === 4 ? base64LikeLabel : `e${i}`, Num: i, Pt: { x: i, y: -i }, Bin: new Uint8Array([i, i + 1, i + 2]) }));
|
|
319
|
+
ids.w0 = insertElement("CuTest:CuWideElement", wideProps("w0", wideElementPropCount));
|
|
320
|
+
ids.w1 = insertElement("CuTest:CuWideElement", wideProps("w1", wideElementPropCount));
|
|
321
|
+
editTxn.saveChanges("insert elements");
|
|
322
|
+
await iModel.locks.acquireLocks({ exclusive: Id64.toIdSet([ids.e[0], ids.e[1], ids.e[3], ids.e[4]]) });
|
|
323
|
+
ids.r0 = editTxn.insertRelationship({ classFullName: "CuTest:CuRefersTo", sourceId: ids.e[0], targetId: ids.e[1] });
|
|
324
|
+
ids.r1 = editTxn.insertRelationship({ classFullName: "CuTest:CuRefersTo", sourceId: ids.e[3], targetId: ids.e[4] });
|
|
325
|
+
editTxn.saveChanges("insert relationships");
|
|
326
|
+
await iModel.pushChanges({ description: "inserts", accessToken });
|
|
327
|
+
// Changeset 2: update elements, including properties in the overflow table.
|
|
328
|
+
await waitForDistinctLastMod();
|
|
329
|
+
await iModel.locks.acquireLocks({ exclusive: Id64.toIdSet([ids.e[0], ids.e[1], ids.w0]) });
|
|
330
|
+
editTxn.updateElement({ ...iModel.elements.getElementProps(ids.e[0]), Label: "e0-updated" });
|
|
331
|
+
editTxn.updateElement({ ...iModel.elements.getElementProps(ids.e[1]), Num: 100, Pt: { x: 10, y: 20 } });
|
|
332
|
+
editTxn.updateElement({ ...iModel.elements.getElementProps(ids.w0), p34: "w0-34-updated", p35: "w0-35-updated" }); // only properties in the overflow table
|
|
333
|
+
editTxn.saveChanges("update elements");
|
|
334
|
+
await iModel.pushChanges({ description: "updates", accessToken });
|
|
335
|
+
// Changeset 3: delete elements - including properties in the overflow table - and a relationship.
|
|
336
|
+
await waitForDistinctLastMod();
|
|
337
|
+
await iModel.locks.acquireLocks({ exclusive: Id64.toIdSet([ids.e[2], ids.w1, ids.e[3], ids.e[4]]) });
|
|
338
|
+
editTxn.deleteRelationship(iModel.relationships.getInstanceProps("CuTest:CuRefersTo", ids.r1));
|
|
339
|
+
editTxn.deleteElement([ids.e[2], ids.w1]);
|
|
340
|
+
editTxn.saveChanges("delete elements");
|
|
341
|
+
await iModel.pushChanges({ description: "deletes", accessToken });
|
|
342
|
+
// Changeset 4: insert enough elements to spill when the memory budget is tiny.
|
|
343
|
+
await waitForDistinctLastMod();
|
|
344
|
+
await iModel.locks.acquireLocks({ shared: drawingModelId });
|
|
345
|
+
for (let i = 0; i < 60; ++i)
|
|
346
|
+
insertElement("CuTest:CuElement", { Label: `bulk${i}`, Num: i, Pt: { x: i, y: i }, Bin: new Uint8Array([i, 255 - i]) });
|
|
347
|
+
editTxn.saveChanges("bulk insert elements");
|
|
348
|
+
await iModel.pushChanges({ description: "bulk inserts", accessToken });
|
|
349
|
+
const downloaded = await HubMock.downloadChangesets({ iModelId, targetDir: path.join(KnownTestLocations.outputDir, iModelId, "changesets") });
|
|
350
|
+
assert.equal(downloaded.length, 5);
|
|
351
|
+
[, changesets.inserts, changesets.updates, changesets.deletes, changesets.bulkInserts] = downloaded.map((changeset) => changeset.pathname);
|
|
352
|
+
tableInfo = queryTableInfo(iModel);
|
|
353
|
+
assert.isTrue(tableInfo.overflowTables.has("bis_GeometricElement2d_Overflow"));
|
|
354
|
+
});
|
|
355
|
+
after(() => {
|
|
356
|
+
txn?.end();
|
|
357
|
+
rwIModel?.close();
|
|
358
|
+
if (rwIModel !== undefined)
|
|
359
|
+
HubMock.shutdown();
|
|
360
|
+
});
|
|
361
|
+
it("matches PartialChangeUnifier for inserts, updates and deletes", () => {
|
|
362
|
+
const propFilters = [PropertyFilter.All, PropertyFilter.BisCoreElement, PropertyFilter.InstanceKey, PropertyFilter.InstanceKeyAndIdentifiers];
|
|
363
|
+
const rowOptionsList = [undefined, { classIdsToClassNames: true }, { abbreviateBlobs: false }];
|
|
364
|
+
for (const fileName of [changesets.inserts, changesets.updates, changesets.deletes]) {
|
|
365
|
+
for (const propFilter of propFilters) {
|
|
366
|
+
for (const rowOptions of rowOptionsList) {
|
|
367
|
+
const options = { propFilter, rowOptions };
|
|
368
|
+
const expected = unifyWithPartialChangeUnifier(openReaders([fileName], options), tableInfo);
|
|
369
|
+
assert.isNotEmpty(expected.instances);
|
|
370
|
+
assertMatchesPartialChangeUnifier(unifyNatively(openReaders([fileName], options)), expected, tableInfo);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
it("merges the rows of all tables of an instance and fills $meta from the reader", () => {
|
|
376
|
+
const rowOptions = { classIdsToClassNames: true };
|
|
377
|
+
const instances = unifyNatively(openReaders([changesets.updates], { propFilter: PropertyFilter.All, rowOptions }));
|
|
378
|
+
const w0Old = instances.find((i) => i.ECInstanceId === ids.w0 && i.$meta.stage === "Old");
|
|
379
|
+
const w0New = instances.find((i) => i.ECInstanceId === ids.w0 && i.$meta.stage === "New");
|
|
380
|
+
expect(w0Old).to.exist;
|
|
381
|
+
expect(w0New).to.exist;
|
|
382
|
+
assert.isBelow(instances.indexOf(w0Old), instances.indexOf(w0New), "Old before New");
|
|
383
|
+
for (const instance of [w0Old, w0New]) {
|
|
384
|
+
assert.equal(instance.ECClassId, "CuTest.CuWideElement");
|
|
385
|
+
assert.equal(instance.$meta.op, "Updated");
|
|
386
|
+
assert.equal(instance.$meta.propFilter, PropertyFilter.All);
|
|
387
|
+
assert.deepEqual(instance.$meta.rowOptions, rowOptions);
|
|
388
|
+
assert.isFalse(instance.$meta.isIndirectChange);
|
|
389
|
+
assert.include(instance.$meta.tables, "bis_Element");
|
|
390
|
+
assert.include(instance.$meta.tables, "bis_GeometricElement2d_Overflow");
|
|
391
|
+
assert.isString(instance.LastMod);
|
|
392
|
+
}
|
|
393
|
+
assert.equal(w0Old.p34, "w0-34");
|
|
394
|
+
assert.equal(w0Old.p35, "w0-35");
|
|
395
|
+
assert.equal(w0New.p34, "w0-34-updated");
|
|
396
|
+
assert.equal(w0New.p35, "w0-35-updated");
|
|
397
|
+
assert.isUndefined(w0New.p0, "unchanged properties are not part of the change");
|
|
398
|
+
const e1New = instances.find((i) => i.ECInstanceId === ids.e[1] && i.$meta.stage === "New");
|
|
399
|
+
expect(e1New).to.exist;
|
|
400
|
+
assert.equal(e1New.Num, 100);
|
|
401
|
+
assert.deepEqual(e1New.Pt, { X: 10, Y: 20 });
|
|
402
|
+
});
|
|
403
|
+
it("honors the options and filters of the readers", () => {
|
|
404
|
+
const cases = [
|
|
405
|
+
{ fileName: changesets.updates, options: { invert: true } },
|
|
406
|
+
{ fileName: changesets.deletes, options: { configure: (reader) => reader.setOpCodeFilters(new Set(["Deleted"])) } },
|
|
407
|
+
{ fileName: changesets.inserts, options: { configure: (reader) => reader.setClassNameFilters(new Set(["CuTest:CuWideElement", "CuTest:CuRefersTo"])) } },
|
|
408
|
+
{ fileName: changesets.updates, options: { configure: (reader) => reader.setTableNameFilters(new Set(["bis_Element"])) } },
|
|
409
|
+
{ fileName: changesets.bulkInserts, options: { configure: (reader) => reader.enableStrictMode() } },
|
|
410
|
+
];
|
|
411
|
+
for (const { fileName, options } of cases) {
|
|
412
|
+
const expected = unifyWithPartialChangeUnifier(openReaders([fileName], options), tableInfo);
|
|
413
|
+
assert.isNotEmpty(expected.instances);
|
|
414
|
+
assertMatchesPartialChangeUnifier(unifyNatively(openReaders([fileName], options)), expected, tableInfo);
|
|
415
|
+
}
|
|
416
|
+
const deleted = unifyNatively(openReaders([changesets.deletes], cases[1].options));
|
|
417
|
+
assert.sameMembers(deleted.map((i) => i.ECInstanceId), [ids.e[2], ids.w1, ids.r1]);
|
|
418
|
+
for (const instance of deleted) {
|
|
419
|
+
assert.equal(instance.$meta.op, "Deleted");
|
|
420
|
+
assert.equal(instance.$meta.stage, "Old");
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
it("reports 'Updated' for instances only overflow tables contributed to", () => {
|
|
424
|
+
const options = { configure: (reader) => reader.setTableNameFilters(new Set(tableInfo.overflowTables)) };
|
|
425
|
+
for (const fileName of [changesets.inserts, changesets.updates, changesets.deletes]) {
|
|
426
|
+
const expected = unifyWithPartialChangeUnifier(openReaders([fileName], options), tableInfo);
|
|
427
|
+
assert.isNotEmpty(expected.instances, "rows from overflow tables");
|
|
428
|
+
const actual = unifyNatively(openReaders([fileName], options));
|
|
429
|
+
assertMatchesPartialChangeUnifier(actual, expected, tableInfo);
|
|
430
|
+
for (const instance of actual)
|
|
431
|
+
assert.equal(instance.$meta.op, "Updated");
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
it("keeps strings that start with the base64 header as strings", () => {
|
|
435
|
+
const instances = unifyNatively(openReaders([changesets.inserts], { rowOptions: { abbreviateBlobs: false } }));
|
|
436
|
+
const e4 = instances.find((instance) => instance.ECInstanceId === ids.e[4]);
|
|
437
|
+
assert.strictEqual(e4?.Label, base64LikeLabel);
|
|
438
|
+
assert.instanceOf(e4?.Bin, Uint8Array);
|
|
439
|
+
});
|
|
440
|
+
it("ends a native batch early once it holds 16 MiB of strings and binary", async () => {
|
|
441
|
+
const iModel = db();
|
|
442
|
+
const { model, category } = iModel.elements.getElementProps(ids.e[0]);
|
|
443
|
+
await iModel.locks.acquireLocks({ shared: model });
|
|
444
|
+
const blob = new Uint8Array(8 * 1024 * 1024);
|
|
445
|
+
try {
|
|
446
|
+
const env_4 = { stack: [], error: void 0, hasError: false };
|
|
447
|
+
try {
|
|
448
|
+
const inserted = [0, 1, 2].map(() => txn.insertElement({ classFullName: "CuTest:CuElement", model, category, code: Code.createEmpty(), Bin: blob }));
|
|
449
|
+
const reader = __addDisposableResource(env_4, ChangesetReader.openInMemoryChanges({ db: iModel, rowOptions: { abbreviateBlobs: false } }), false);
|
|
450
|
+
const nativeUnifier = new IModelNative.platform.ChangeUnifier({});
|
|
451
|
+
try {
|
|
452
|
+
reader[_appendToNativeUnifier](nativeUnifier);
|
|
453
|
+
const batchSizes = [];
|
|
454
|
+
const returnedIds = [];
|
|
455
|
+
for (let batch = nativeUnifier.step(1000); batch.length > 0; batch = nativeUnifier.step(1000)) {
|
|
456
|
+
batchSizes.push(batch.length);
|
|
457
|
+
returnedIds.push(...batch.map((instance) => instance.ECInstanceId));
|
|
458
|
+
}
|
|
459
|
+
assert.isAbove(batchSizes.length, 1, "24 MiB of blobs were returned in one batch");
|
|
460
|
+
assert.includeMembers(returnedIds, inserted);
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
nativeUnifier.close();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
catch (e_4) {
|
|
467
|
+
env_4.error = e_4;
|
|
468
|
+
env_4.hasError = true;
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
__disposeResources(env_4);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
finally {
|
|
475
|
+
txn.abandonChanges();
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
it("keeps only the requested properties", () => {
|
|
479
|
+
const propNames = ["Label", "Num", "Model", "LastMod", "p35"];
|
|
480
|
+
const kept = new Set(["ECInstanceId", "ECClassId", ...propNames]);
|
|
481
|
+
for (const fileName of [changesets.inserts, changesets.updates, changesets.deletes]) {
|
|
482
|
+
const all = unifyNatively(openReaders([fileName]));
|
|
483
|
+
const projected = unifyNatively(openReaders([fileName]), { propNames });
|
|
484
|
+
assert.equal(projected.length, all.length);
|
|
485
|
+
projected.forEach((instance, i) => {
|
|
486
|
+
const { $meta: meta, ...props } = instance;
|
|
487
|
+
const { $meta: allMeta, ...allProps } = all[i];
|
|
488
|
+
assert.deepEqual(props, Object.fromEntries(Object.entries(allProps).filter(([name]) => kept.has(name))));
|
|
489
|
+
assert.isDefined(props.ECInstanceId);
|
|
490
|
+
assert.isDefined(props.ECClassId);
|
|
491
|
+
const { changeFetchedPropNames, ...otherMeta } = meta;
|
|
492
|
+
const { changeFetchedPropNames: allChangeFetchedPropNames, ...allOtherMeta } = allMeta;
|
|
493
|
+
assert.deepEqual(otherMeta, allOtherMeta);
|
|
494
|
+
assert.includeMembers(allChangeFetchedPropNames, changeFetchedPropNames);
|
|
495
|
+
});
|
|
496
|
+
assert.isTrue(projected.some((instance) => instance.Label !== undefined || instance.p35 !== undefined), "some requested properties were kept");
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
it("returns the same instances when it spills to temporary files", () => {
|
|
500
|
+
const options = { rowOptions: { abbreviateBlobs: false } };
|
|
501
|
+
const fileNameLists = [[changesets.bulkInserts], [changesets.updates], [changesets.inserts, changesets.updates, changesets.deletes, changesets.bulkInserts]];
|
|
502
|
+
for (const fileNames of fileNameLists) {
|
|
503
|
+
const inMemory = unifyNatively(openReaders(fileNames, options), { memoryBudgetBytes: 0 });
|
|
504
|
+
assert.isNotEmpty(inMemory);
|
|
505
|
+
assert.deepEqual(unifyNatively(openReaders(fileNames, options)), inMemory, "default memory budget");
|
|
506
|
+
for (const args of [{ memoryBudgetBytes: 1 }, { memoryBudgetBytes: 1, batchSize: 1 }, { memoryBudgetBytes: 1024, batchSize: 7 }, { memoryBudgetBytes: 0, batchSize: 3 }])
|
|
507
|
+
assert.deepEqual(unifyNatively(openReaders(fileNames, options), args), inMemory, JSON.stringify(args));
|
|
508
|
+
const projectedInMemory = unifyNatively(openReaders(fileNames, options), { propNames: ["Label", "Bin"], memoryBudgetBytes: 0 });
|
|
509
|
+
assert.deepEqual(unifyNatively(openReaders(fileNames, options), { propNames: ["Label", "Bin"], memoryBudgetBytes: 1 }), projectedInMemory, "projected");
|
|
510
|
+
}
|
|
511
|
+
const expected = unifyWithPartialChangeUnifier(openReaders([changesets.bulkInserts], options), tableInfo);
|
|
512
|
+
assert.isAtLeast(expected.instances.length, 60);
|
|
513
|
+
assertMatchesPartialChangeUnifier(unifyNatively(openReaders([changesets.bulkInserts], options), { memoryBudgetBytes: 1 }), expected, tableInfo);
|
|
514
|
+
});
|
|
515
|
+
it("merges the rows of several readers", () => {
|
|
516
|
+
const env_5 = { stack: [], error: void 0, hasError: false };
|
|
517
|
+
try {
|
|
518
|
+
const fileNames = [changesets.inserts, changesets.updates, changesets.deletes];
|
|
519
|
+
const expected = unifyWithPartialChangeUnifier(openReaders(fileNames), tableInfo);
|
|
520
|
+
const actual = unifyNatively(openReaders(fileNames));
|
|
521
|
+
assertMatchesPartialChangeUnifier(actual, expected, tableInfo, true);
|
|
522
|
+
// Rows of later readers win.
|
|
523
|
+
const e0New = actual.find((i) => i.ECInstanceId === ids.e[0] && i.$meta.stage === "New");
|
|
524
|
+
assert.equal(e0New?.Label, "e0-updated");
|
|
525
|
+
// An instance inserted by one reader and deleted by another has both stages.
|
|
526
|
+
assert.sameMembers(actual.filter((i) => i.ECInstanceId === ids.e[2]).map((i) => i.$meta.stage), ["Old", "New"]);
|
|
527
|
+
// Readers that are all open while the unifier is created give the same result as readers opened one at a time.
|
|
528
|
+
const inserts = __addDisposableResource(env_5, openReader(changesets.inserts), false);
|
|
529
|
+
const updates = __addDisposableResource(env_5, openReader(changesets.updates), false);
|
|
530
|
+
const deletes = __addDisposableResource(env_5, openReader(changesets.deletes), false);
|
|
531
|
+
assert.deepEqual(unifyNatively([inserts, updates, deletes]), actual);
|
|
532
|
+
}
|
|
533
|
+
catch (e_5) {
|
|
534
|
+
env_5.error = e_5;
|
|
535
|
+
env_5.hasError = true;
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
__disposeResources(env_5);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
it("consumes the readers", () => {
|
|
542
|
+
const env_6 = { stack: [], error: void 0, hasError: false };
|
|
543
|
+
try {
|
|
544
|
+
const reader = __addDisposableResource(env_6, openReader(changesets.inserts), false);
|
|
545
|
+
const unifier = __addDisposableResource(env_6, ChangeUnifier.fromReader(reader), false);
|
|
546
|
+
assert.isFalse(reader.step(), "the unifier read all rows");
|
|
547
|
+
assert.isUndefined(reader.inserted);
|
|
548
|
+
assert.isUndefined(reader.deleted);
|
|
549
|
+
expect(() => reader.setOpCodeFilters(new Set(["Inserted"]))).to.throw(IModelError, "consumed by a ChangeUnifier");
|
|
550
|
+
expect(() => ChangeUnifier.fromReader(reader)).to.throw(IModelError, "already been consumed");
|
|
551
|
+
assert.isNotEmpty(Array.from(unifier.instances()));
|
|
552
|
+
expect(() => reader.close()).to.not.throw();
|
|
553
|
+
}
|
|
554
|
+
catch (e_6) {
|
|
555
|
+
env_6.error = e_6;
|
|
556
|
+
env_6.hasError = true;
|
|
557
|
+
}
|
|
558
|
+
finally {
|
|
559
|
+
__disposeResources(env_6);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
it("rejects readers that cannot be merged", () => {
|
|
563
|
+
{
|
|
564
|
+
const env_7 = { stack: [], error: void 0, hasError: false };
|
|
565
|
+
try {
|
|
566
|
+
const reader = __addDisposableResource(env_7, openReader(changesets.inserts), false);
|
|
567
|
+
assert.isTrue(reader.step());
|
|
568
|
+
expect(() => ChangeUnifier.fromReader(reader)).to.throw(IModelError, "already been stepped");
|
|
569
|
+
assert.isTrue(reader.step(), "the reader was not consumed");
|
|
570
|
+
}
|
|
571
|
+
catch (e_7) {
|
|
572
|
+
env_7.error = e_7;
|
|
573
|
+
env_7.hasError = true;
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
__disposeResources(env_7);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
{
|
|
580
|
+
const env_8 = { stack: [], error: void 0, hasError: false };
|
|
581
|
+
try {
|
|
582
|
+
const reader = __addDisposableResource(env_8, openReader(changesets.inserts), false);
|
|
583
|
+
expect(() => ChangeUnifier.fromReaders([reader, reader])).to.throw(IModelError, "already been consumed");
|
|
584
|
+
}
|
|
585
|
+
catch (e_8) {
|
|
586
|
+
env_8.error = e_8;
|
|
587
|
+
env_8.hasError = true;
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
__disposeResources(env_8);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
{
|
|
594
|
+
const env_9 = { stack: [], error: void 0, hasError: false };
|
|
595
|
+
try {
|
|
596
|
+
const inserts = __addDisposableResource(env_9, openReader(changesets.inserts, { propFilter: PropertyFilter.All }), false);
|
|
597
|
+
const updates = __addDisposableResource(env_9, openReader(changesets.updates, { propFilter: PropertyFilter.InstanceKey }), false);
|
|
598
|
+
expect(() => ChangeUnifier.fromReaders([inserts, updates])).to.throw(IModelError, "same propFilter and rowOptions");
|
|
599
|
+
assert.isFalse(inserts.step(), "readers before the failing one were consumed");
|
|
600
|
+
assert.isTrue(updates.step(), "the failing reader was not consumed");
|
|
601
|
+
}
|
|
602
|
+
catch (e_9) {
|
|
603
|
+
env_9.error = e_9;
|
|
604
|
+
env_9.hasError = true;
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
__disposeResources(env_9);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
{
|
|
611
|
+
const env_10 = { stack: [], error: void 0, hasError: false };
|
|
612
|
+
try {
|
|
613
|
+
const inserts = __addDisposableResource(env_10, openReader(changesets.inserts, { rowOptions: { classIdsToClassNames: true } }), false);
|
|
614
|
+
const updates = __addDisposableResource(env_10, openReader(changesets.updates), false);
|
|
615
|
+
expect(() => ChangeUnifier.fromReaders([inserts, updates])).to.throw(IModelError, "same propFilter and rowOptions");
|
|
616
|
+
}
|
|
617
|
+
catch (e_10) {
|
|
618
|
+
env_10.error = e_10;
|
|
619
|
+
env_10.hasError = true;
|
|
620
|
+
}
|
|
621
|
+
finally {
|
|
622
|
+
__disposeResources(env_10);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
{
|
|
626
|
+
// An options object shared by the readers and changed between them is compared as it was for the first reader.
|
|
627
|
+
const rowOptions = { classIdsToClassNames: true };
|
|
628
|
+
function* readers() {
|
|
629
|
+
const env_12 = { stack: [], error: void 0, hasError: false };
|
|
630
|
+
try {
|
|
631
|
+
const inserts = __addDisposableResource(env_12, openReader(changesets.inserts, { rowOptions }), false);
|
|
632
|
+
yield inserts;
|
|
633
|
+
rowOptions.classIdsToClassNames = false;
|
|
634
|
+
const updates = __addDisposableResource(env_12, openReader(changesets.updates, { rowOptions }), false);
|
|
635
|
+
yield updates;
|
|
636
|
+
}
|
|
637
|
+
catch (e_12) {
|
|
638
|
+
env_12.error = e_12;
|
|
639
|
+
env_12.hasError = true;
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
__disposeResources(env_12);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
expect(() => ChangeUnifier.fromReaders(readers())).to.throw(IModelError, "same propFilter and rowOptions");
|
|
646
|
+
}
|
|
647
|
+
{
|
|
648
|
+
const env_11 = { stack: [], error: void 0, hasError: false };
|
|
649
|
+
try {
|
|
650
|
+
// Omitted row options are equivalent to their defaults.
|
|
651
|
+
const inserts = __addDisposableResource(env_11, openReader(changesets.inserts, { rowOptions: { abbreviateBlobs: true, classIdsToClassNames: false } }), false);
|
|
652
|
+
const updates = __addDisposableResource(env_11, openReader(changesets.updates), false);
|
|
653
|
+
const unifier = __addDisposableResource(env_11, ChangeUnifier.fromReaders([inserts, updates]), false);
|
|
654
|
+
for (const instance of unifier.instances())
|
|
655
|
+
assert.deepEqual(instance.$meta.rowOptions, { abbreviateBlobs: true, classIdsToClassNames: false });
|
|
656
|
+
}
|
|
657
|
+
catch (e_11) {
|
|
658
|
+
env_11.error = e_11;
|
|
659
|
+
env_11.hasError = true;
|
|
660
|
+
}
|
|
661
|
+
finally {
|
|
662
|
+
__disposeResources(env_11);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
it("returns each instance once and can be disposed more than once", () => {
|
|
667
|
+
const env_13 = { stack: [], error: void 0, hasError: false };
|
|
668
|
+
try {
|
|
669
|
+
const all = unifyNatively(openReaders([changesets.bulkInserts]));
|
|
670
|
+
assert.isAtLeast(all.length, 20);
|
|
671
|
+
const unifier = __addDisposableResource(env_13, ChangeUnifier.fromReaders(openReaders([changesets.bulkInserts]), { batchSize: 4 }), false);
|
|
672
|
+
const first = [];
|
|
673
|
+
for (const instance of unifier.instances()) {
|
|
674
|
+
first.push(instance);
|
|
675
|
+
if (first.length === 10)
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
const rest = Array.from(unifier.instances());
|
|
679
|
+
assert.deepEqual([...first, ...rest], all);
|
|
680
|
+
assert.isEmpty(Array.from(unifier.instances()));
|
|
681
|
+
unifier[Symbol.dispose]();
|
|
682
|
+
expect(() => unifier[Symbol.dispose]()).to.not.throw();
|
|
683
|
+
expect(() => unifier.instances().next()).to.throw(IModelError, "disposed");
|
|
684
|
+
}
|
|
685
|
+
catch (e_13) {
|
|
686
|
+
env_13.error = e_13;
|
|
687
|
+
env_13.hasError = true;
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
__disposeResources(env_13);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
it("returns no instances without readers", () => {
|
|
694
|
+
const env_14 = { stack: [], error: void 0, hasError: false };
|
|
695
|
+
try {
|
|
696
|
+
const unifier = __addDisposableResource(env_14, ChangeUnifier.fromReaders([]), false);
|
|
697
|
+
assert.isEmpty(Array.from(unifier.instances()));
|
|
698
|
+
}
|
|
699
|
+
catch (e_14) {
|
|
700
|
+
env_14.error = e_14;
|
|
701
|
+
env_14.hasError = true;
|
|
702
|
+
}
|
|
703
|
+
finally {
|
|
704
|
+
__disposeResources(env_14);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
});
|
|
708
|
+
describe("ChangeUnifier arguments", () => {
|
|
709
|
+
it("are validated before any reader is requested", () => {
|
|
710
|
+
const invalidArgs = [
|
|
711
|
+
{ batchSize: 0 },
|
|
712
|
+
{ batchSize: -1 },
|
|
713
|
+
{ batchSize: 1.5 },
|
|
714
|
+
{ batchSize: Number.NaN },
|
|
715
|
+
{ memoryBudgetBytes: -1 },
|
|
716
|
+
{ memoryBudgetBytes: 0.5 },
|
|
717
|
+
{ memoryBudgetBytes: Number.POSITIVE_INFINITY },
|
|
718
|
+
];
|
|
719
|
+
for (const args of invalidArgs) {
|
|
720
|
+
let requested = false;
|
|
721
|
+
const readers = {
|
|
722
|
+
[Symbol.iterator]: () => {
|
|
723
|
+
requested = true;
|
|
724
|
+
return [][Symbol.iterator]();
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
expect(() => ChangeUnifier.fromReaders(readers, args), JSON.stringify(args)).to.throw(IModelError, "ChangeUnifier:").with.property("errorNumber", IModelStatus.BadArg);
|
|
728
|
+
assert.isFalse(requested, JSON.stringify(args));
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
//# sourceMappingURL=ChangeUnifier.test.js.map
|