Skip to content

Commit a5733aa

Browse files
Merge branch 'master' into rohitptnkr/bulk-delete-fixes
2 parents 1f470b3 + f7e4c30 commit a5733aa

11 files changed

Lines changed: 392 additions & 18 deletions

File tree

common/api/core-backend.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5143,6 +5143,7 @@ export class LocalHub {
51435143
queryLocks(): LocksEntry[];
51445144
// (undocumented)
51455145
queryLockStatus(elementId: Id64String): LockStatus;
5146+
queryNearestCheckpoint(changesetIndex: ChangesetIndex): ChangesetIndex;
51465147
queryPreviousCheckpoint(changesetIndex: ChangesetIndex): ChangesetIndex;
51475148
// (undocumented)
51485149
releaseAllLocks(arg: {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@itwin/core-backend",
5+
"comment": "Allow reversing schema changeset",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@itwin/core-backend"
10+
}

common/config/rush/pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@
113113
"webpack": "^5.97.1"
114114
},
115115
"dependencies": {
116-
"@bentley/imodeljs-native": "5.10.11",
116+
"@bentley/imodeljs-native": "5.10.12",
117117
"@itwin/object-storage-azure": "^3.0.4",
118118
"@azure/storage-blob": "^12.28.0",
119119
"form-data": "^4.0.4",

core/backend/src/LocalHub.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,34 @@ export class LocalHub {
430430
});
431431
}
432432

433+
/**
434+
* Find the checkpoint nearest to `changesetIndex` (searching both backward and forward).
435+
* When the previous and next checkpoints are equidistant, the newer (next) checkpoint is preferred,
436+
* which enables the tip-then-reverse download path in [[V2CheckpointManager]].
437+
*/
438+
public queryNearestCheckpoint(changesetIndex: ChangesetIndex): ChangesetIndex {
439+
const prev = this.queryPreviousCheckpoint(changesetIndex); // always ≥ 0; returns 0 when none found
440+
441+
const next: ChangesetIndex | undefined = this.db.withSqliteStatement(
442+
"SELECT min(csIndex) FROM checkpoints WHERE csIndex >= ?",
443+
(stmt) => {
444+
stmt.bindInteger(1, changesetIndex);
445+
stmt.step();
446+
const val = stmt.getValue(0);
447+
return val.isNull ? undefined : val.getInteger();
448+
}
449+
);
450+
451+
if (next === undefined) return prev; // no checkpoint at or after requested index
452+
if (next === prev) return prev; // exact match
453+
454+
// Prefer the newer (next) checkpoint when equidistant so that CheckpointManager
455+
// will reverse from a tip checkpoint rather than apply from an old seed.
456+
const distPrev = changesetIndex - prev;
457+
const distNext = next - changesetIndex;
458+
return distNext <= distPrev ? next : prev;
459+
}
460+
433461
/** "download" a checkpoint */
434462
public downloadCheckpoint(arg: { changeset: ChangesetIndexOrId, targetFile: LocalFileName }) {
435463
const index = this.getIndexFromChangeset(arg.changeset);

core/backend/src/internal/HubMock.ts

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,40 @@ import { CheckpointProps, DownloadRequest, MockCheckpoint, ProgressFunction, Pro
1717
import { IModelHost } from "../IModelHost";
1818
import { IModelJsFs } from "../IModelJsFs";
1919
import { LocalHub } from "../LocalHub";
20-
import { TokenArg } from "../IModelDb";
21-
import { _getHubAccess, _mockCheckpoint, _setHubAccess } from "./Symbols";
20+
import { SnapshotDb, TokenArg } from "../IModelDb";
21+
import { _getHubAccess, _mockCheckpoint, _nativeDb, _setHubAccess } from "./Symbols";
2222
import { BriefcaseManager } from "../BriefcaseManager";
23-
import * as path from "path";
2423

2524
function wasStarted(val: string | undefined): asserts val is string {
2625
if (undefined === val)
2726
throw new Error("Call HubMock.startup first");
2827
}
2928

30-
function doDownload(args: { iModelId: string, changeset: ChangesetIndexOrId, targetFile: string }) {
29+
// Used by mockAttach: copies the nearest *prior* checkpoint (never newer) so the
30+
// returned file is already at or before the requested version.
31+
function doDownloadPrior(args: { iModelId: string, changeset: ChangesetIndexOrId, targetFile: string }) {
3132
HubMock.findLocalHub(args.iModelId).downloadCheckpoint(args);
3233
}
34+
35+
// Used by mockDownload: copies the *nearest* checkpoint (forward or backward).
36+
// When the nearest is newer than the requested version, CheckpointManager.updateToRequestedVersion
37+
// will reverse it to the requested version via BriefcaseManager.pullAndApplyChangesets.
38+
function doDownloadNearest(args: { iModelId: string, changeset: ChangesetIndexOrId, targetFile: string }) {
39+
const hub = HubMock.findLocalHub(args.iModelId);
40+
const requestedIndex = hub.getIndexFromChangeset(args.changeset);
41+
const nearest = hub.queryNearestCheckpoint(requestedIndex);
42+
IModelJsFs.copySync(join(hub.checkpointDir, hub.checkpointNameFromIndex(nearest)), args.targetFile);
43+
}
44+
3345
const mockCheckpoint: MockCheckpoint = {
3446
mockAttach: (checkpoint: CheckpointProps) => {
35-
const targetFile = path.join(BriefcaseManager.getBriefcaseBasePath(checkpoint.iModelId), `${checkpoint.changeset.index}.bim`);
36-
doDownload({ ...checkpoint, targetFile })
47+
const targetFile = join(BriefcaseManager.getBriefcaseBasePath(checkpoint.iModelId), `${checkpoint.changeset.index}.bim`);
48+
doDownloadPrior({ ...checkpoint, targetFile });
3749
return targetFile;
3850
},
3951

4052
mockDownload: (request: DownloadRequest) => {
41-
doDownload({ ...request.checkpoint, targetFile: request.localFile });
53+
doDownloadNearest({ ...request.checkpoint, targetFile: request.localFile });
4254
}
4355
};
4456

@@ -74,11 +86,22 @@ const mockCheckpoint: MockCheckpoint = {
7486
*
7587
* @internal
7688
*/
89+
/** Options for [[HubMock.startup]]. @internal */
90+
export interface HubMockStartupOptions {
91+
/**
92+
* When `true`, every successful [[HubMock.pushChangeset]] call automatically uploads a V1 checkpoint
93+
* (the current briefcase `.bim` file) for the tip changeset index into [[LocalHub]].
94+
* This lets tests download a tip checkpoint and reverse changesets from it.
95+
*/
96+
createTipCheckpointOnPush?: boolean;
97+
}
98+
7799
export class HubMock {
78100
private static mockRoot: LocalDirName | undefined;
79101
private static hubs = new Map<string, LocalHub>();
80102
private static _saveHubAccess: BackendHubAccess | undefined;
81103
private static _iTwinId: GuidString | undefined;
104+
private static _createTipCheckpointOnPush = false;
82105

83106
/** Determine whether a test us currently being run under HubMock */
84107
public static get isValid() { return undefined !== this.mockRoot; }
@@ -92,7 +115,7 @@ export class HubMock {
92115
* @param mockName a unique name (e.g. "MyTest") for this HubMock to disambiguate tests when more than one is simultaneously active.
93116
* It is used to create a private directory used by the HubMock for a test. That directory is removed when [[shutdown]] is called.
94117
*/
95-
public static startup(mockName: LocalDirName, outputDir: string) {
118+
public static startup(mockName: LocalDirName, outputDir: string, options?: HubMockStartupOptions) {
96119
if (this.isValid)
97120
throw new Error("Either a previous test did not call HubMock.shutdown() properly, or more than one test is simultaneously attempting to use HubMock, which is not allowed");
98121

@@ -104,6 +127,7 @@ export class HubMock {
104127

105128
IModelHost[_setHubAccess](this);
106129
HubMock._iTwinId = Guid.createValue(); // all iModels for this test get the same "iTwinId"
130+
this._createTipCheckpointOnPush = options?.createTipCheckpointOnPush ?? false;
107131

108132
V2CheckpointManager[_mockCheckpoint] = mockCheckpoint;
109133
}
@@ -116,6 +140,7 @@ export class HubMock {
116140
return;
117141

118142
V2CheckpointManager[_mockCheckpoint] = undefined;
143+
this._createTipCheckpointOnPush = false;
119144

120145
HubMock._iTwinId = undefined;
121146
for (const hub of this.hubs)
@@ -232,7 +257,46 @@ export class HubMock {
232257
}
233258

234259
public static async pushChangeset(arg: IModelIdArg & { changesetProps: ChangesetFileProps }): Promise<ChangesetIndex> {
235-
return this.findLocalHub(arg.iModelId).addChangeset(arg.changesetProps);
260+
const csIndex = this.findLocalHub(arg.iModelId).addChangeset(arg.changesetProps);
261+
if (this._createTipCheckpointOnPush)
262+
await this.createTipCheckpoint(arg.iModelId);
263+
return csIndex;
264+
}
265+
266+
/**
267+
* Build and upload a V1 checkpoint at the current tip changeset of the given iModel.
268+
*
269+
* The checkpoint is constructed by copying the nearest prior checkpoint from [[LocalHub]] as a
270+
* starting base, then applying all subsequent changesets forward to the latest index via
271+
* [[BriefcaseManager.pullAndApplyChangesets]]. The result is registered in [[LocalHub]] so that
272+
* [[V2CheckpointManager]] (mock path) can serve it to consumers.
273+
*
274+
* When [[HubMockStartupOptions.createTipCheckpointOnPush]] is `true` this is called automatically
275+
* after every successful [[pushChangeset]]. Tests can also call it explicitly to create a single
276+
* checkpoint at the tip after all changesets have been pushed.
277+
*/
278+
public static async createTipCheckpoint(iModelId: GuidString): Promise<void> {
279+
const hub = this.findLocalHub(iModelId);
280+
const csIndex = hub.latestChangesetIndex;
281+
// Find the nearest checkpoint that precedes the new tip and use it as the base.
282+
const prevIndex = hub.queryPreviousCheckpoint(csIndex);
283+
const prevCheckpointFile = join(hub.checkpointDir, hub.checkpointNameFromIndex(prevIndex));
284+
const tempFile = join(hub.rootDir, `checkpoint-building-${csIndex}.bim`);
285+
IModelJsFs.copySync(prevCheckpointFile, tempFile);
286+
try {
287+
const db = SnapshotDb.openForApplyChangesets(tempFile);
288+
try {
289+
await BriefcaseManager.pullAndApplyChangesets(db, { accessToken: "", toIndex: csIndex });
290+
db[_nativeDb].saveChanges();
291+
} finally {
292+
db.close();
293+
}
294+
295+
hub.uploadCheckpoint({ changesetIndex: csIndex, localFile: tempFile });
296+
} finally {
297+
if (IModelJsFs.existsSync(tempFile))
298+
IModelJsFs.removeSync(tempFile);
299+
}
236300
}
237301

238302
public static async queryV2Checkpoint(arg: CheckpointProps): Promise<V2CheckpointAccessProps | undefined> {

0 commit comments

Comments
 (0)