Skip to content

Commit d3a75b3

Browse files
Merge branch 'master' into io-schema-localization
2 parents 3ed110b + a59280d commit d3a75b3

55 files changed

Lines changed: 453 additions & 63 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

common/api/core-backend.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5141,6 +5141,7 @@ export class LocalHub {
51415141
queryLocks(): LocksEntry[];
51425142
// (undocumented)
51435143
queryLockStatus(elementId: Id64String): LockStatus;
5144+
queryNearestCheckpoint(changesetIndex: ChangesetIndex): ChangesetIndex;
51445145
queryPreviousCheckpoint(changesetIndex: ChangesetIndex): ChangesetIndex;
51455146
// (undocumented)
51465147
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/azure-pipelines/jobs/version-bump.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,22 @@ jobs:
216216
displayName: rush audit
217217

218218
- ${{ if or(eq(parameters.BumpType, 'minor'), eq(parameters.BumpType, 'patch')) }}:
219+
- bash: |
220+
# Keep this aligned with the version policy used by the release version bump steps below.
221+
versionPolicyName=prerelease-monorepo-lockStep
222+
currentVersion=$(jq -r --arg policyName "$versionPolicyName" '.[] | select(.policyName == $policyName) | .version' common/config/rush/version-policies.json)
223+
if [ -z "$currentVersion" ] || [ "$currentVersion" = "null" ]; then
224+
echo "Failed to find version policy '$versionPolicyName' in common/config/rush/version-policies.json" >&2
225+
exit 1
226+
fi
227+
# Deprecation comments require a released minor/patch version like 5.10.0, not a prerelease suffix like 5.10.0-dev.10.
228+
# For non-prerelease versions, this parameter expansion leaves the version unchanged.
229+
deprecationVersion=${currentVersion%%-*}
230+
sed -i "s/\(addVersion: \"\)[^\"]*/\1$deprecationVersion/" common/config/eslint/eslint.config.deprecation-policy.js
231+
echo "Using deprecation addVersion=$deprecationVersion from version policy $versionPolicyName"
232+
echo "##vso[task.setvariable variable=deprecationVersion]$deprecationVersion"
233+
displayName: Sync deprecation policy addVersion to current version
234+
219235
- bash: node common/scripts/install-run-rush.js lint-deprecation
220236
displayName: rush lint-deprecation
221237

@@ -236,7 +252,7 @@ jobs:
236252
displayName: Determine whether ESLint rule made any changes
237253
238254
- bash: |
239-
git commit -m "Apply deprecation date rule"
255+
git commit -m "Apply deprecation date rule for v$(deprecationVersion)"
240256
displayName: Commit deprecation comment changes to release branch
241257
condition: eq(variables['deprecationCommentChangesMade'], 'true')
242258

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.

common/config/rush/version-policies.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
"policyName": "prerelease-monorepo-lockStep",
44
"definitionName": "lockStepVersion",
5-
"version": "5.10.0-dev.10",
5+
"version": "5.10.0-dev.11",
66
"nextBump": "prerelease"
77
}
88
]

core/backend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@itwin/core-backend",
3-
"version": "5.10.0-dev.10",
3+
"version": "5.10.0-dev.11",
44
"description": "iTwin.js backend components",
55
"main": "lib/cjs/core-backend.js",
66
"module": "lib/esm/core-backend.js",
@@ -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)