@itwin/core-backend 5.14.0-dev.1 → 5.14.0-dev.10

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/lib/cjs/BriefcaseManager.d.ts +6 -0
  3. package/lib/cjs/BriefcaseManager.d.ts.map +1 -1
  4. package/lib/cjs/BriefcaseManager.js +12 -6
  5. package/lib/cjs/BriefcaseManager.js.map +1 -1
  6. package/lib/cjs/ChangesetReader.d.ts.map +1 -1
  7. package/lib/cjs/ChangesetReader.js +1 -0
  8. package/lib/cjs/ChangesetReader.js.map +1 -1
  9. package/lib/cjs/ChangesetReaderTypes.d.ts +2 -1
  10. package/lib/cjs/ChangesetReaderTypes.d.ts.map +1 -1
  11. package/lib/cjs/ChangesetReaderTypes.js.map +1 -1
  12. package/lib/cjs/CloudSqlite.d.ts.map +1 -1
  13. package/lib/cjs/CloudSqlite.js +9 -2
  14. package/lib/cjs/CloudSqlite.js.map +1 -1
  15. package/lib/cjs/ECDb.d.ts.map +1 -1
  16. package/lib/cjs/ECDb.js +1 -0
  17. package/lib/cjs/ECDb.js.map +1 -1
  18. package/lib/cjs/Element.d.ts +1 -1
  19. package/lib/cjs/Element.js +1 -1
  20. package/lib/cjs/Element.js.map +1 -1
  21. package/lib/cjs/Entity.d.ts +3 -3
  22. package/lib/cjs/Entity.js +3 -3
  23. package/lib/cjs/Entity.js.map +1 -1
  24. package/lib/cjs/IModelDb.d.ts +83 -35
  25. package/lib/cjs/IModelDb.d.ts.map +1 -1
  26. package/lib/cjs/IModelDb.js +196 -89
  27. package/lib/cjs/IModelDb.js.map +1 -1
  28. package/lib/cjs/IpcHost.d.ts.map +1 -1
  29. package/lib/cjs/IpcHost.js +51 -17
  30. package/lib/cjs/IpcHost.js.map +1 -1
  31. package/lib/cjs/SchemaSync.d.ts +126 -18
  32. package/lib/cjs/SchemaSync.d.ts.map +1 -1
  33. package/lib/cjs/SchemaSync.js +303 -81
  34. package/lib/cjs/SchemaSync.js.map +1 -1
  35. package/lib/cjs/SynchronousChannel.d.ts +11 -1
  36. package/lib/cjs/SynchronousChannel.d.ts.map +1 -1
  37. package/lib/cjs/SynchronousChannel.js.map +1 -1
  38. package/lib/cjs/TxnManager.d.ts.map +1 -1
  39. package/lib/cjs/TxnManager.js +16 -13
  40. package/lib/cjs/TxnManager.js.map +1 -1
  41. package/lib/cjs/internal/IntegrityCheck.d.ts +57 -0
  42. package/lib/cjs/internal/IntegrityCheck.d.ts.map +1 -1
  43. package/lib/cjs/internal/IntegrityCheck.js.map +1 -1
  44. package/lib/cjs/internal/SchemaSyncReservations.d.ts.map +1 -1
  45. package/lib/cjs/internal/SchemaSyncReservations.js +13 -3
  46. package/lib/cjs/internal/SchemaSyncReservations.js.map +1 -1
  47. package/lib/esm/BriefcaseManager.d.ts +6 -0
  48. package/lib/esm/BriefcaseManager.d.ts.map +1 -1
  49. package/lib/esm/BriefcaseManager.js +12 -6
  50. package/lib/esm/BriefcaseManager.js.map +1 -1
  51. package/lib/esm/ChangesetReader.d.ts.map +1 -1
  52. package/lib/esm/ChangesetReader.js +1 -0
  53. package/lib/esm/ChangesetReader.js.map +1 -1
  54. package/lib/esm/ChangesetReaderTypes.d.ts +2 -1
  55. package/lib/esm/ChangesetReaderTypes.d.ts.map +1 -1
  56. package/lib/esm/ChangesetReaderTypes.js.map +1 -1
  57. package/lib/esm/CloudSqlite.d.ts.map +1 -1
  58. package/lib/esm/CloudSqlite.js +10 -3
  59. package/lib/esm/CloudSqlite.js.map +1 -1
  60. package/lib/esm/ECDb.d.ts.map +1 -1
  61. package/lib/esm/ECDb.js +1 -0
  62. package/lib/esm/ECDb.js.map +1 -1
  63. package/lib/esm/Element.d.ts +1 -1
  64. package/lib/esm/Element.js +1 -1
  65. package/lib/esm/Element.js.map +1 -1
  66. package/lib/esm/Entity.d.ts +3 -3
  67. package/lib/esm/Entity.js +3 -3
  68. package/lib/esm/Entity.js.map +1 -1
  69. package/lib/esm/IModelDb.d.ts +83 -35
  70. package/lib/esm/IModelDb.d.ts.map +1 -1
  71. package/lib/esm/IModelDb.js +196 -89
  72. package/lib/esm/IModelDb.js.map +1 -1
  73. package/lib/esm/IpcHost.d.ts.map +1 -1
  74. package/lib/esm/IpcHost.js +52 -18
  75. package/lib/esm/IpcHost.js.map +1 -1
  76. package/lib/esm/SchemaSync.d.ts +126 -18
  77. package/lib/esm/SchemaSync.d.ts.map +1 -1
  78. package/lib/esm/SchemaSync.js +304 -82
  79. package/lib/esm/SchemaSync.js.map +1 -1
  80. package/lib/esm/SynchronousChannel.d.ts +11 -1
  81. package/lib/esm/SynchronousChannel.d.ts.map +1 -1
  82. package/lib/esm/SynchronousChannel.js.map +1 -1
  83. package/lib/esm/TxnManager.d.ts.map +1 -1
  84. package/lib/esm/TxnManager.js +17 -14
  85. package/lib/esm/TxnManager.js.map +1 -1
  86. package/lib/esm/internal/IntegrityCheck.d.ts +57 -0
  87. package/lib/esm/internal/IntegrityCheck.d.ts.map +1 -1
  88. package/lib/esm/internal/IntegrityCheck.js.map +1 -1
  89. package/lib/esm/internal/SchemaSyncReservations.d.ts.map +1 -1
  90. package/lib/esm/internal/SchemaSyncReservations.js +13 -3
  91. package/lib/esm/internal/SchemaSyncReservations.js.map +1 -1
  92. package/lib/esm/test/annotations/FrameGeometry.test.js +1 -1
  93. package/lib/esm/test/annotations/FrameGeometry.test.js.map +1 -1
  94. package/lib/esm/test/hubaccess/BriefcaseManager.test.js +65 -1
  95. package/lib/esm/test/hubaccess/BriefcaseManager.test.js.map +1 -1
  96. package/lib/esm/test/hubaccess/Rebase.test.js +3 -1
  97. package/lib/esm/test/hubaccess/Rebase.test.js.map +1 -1
  98. package/lib/esm/test/hubaccess/SemanticRebase.test.js +218 -28
  99. package/lib/esm/test/hubaccess/SemanticRebase.test.js.map +1 -1
  100. package/lib/esm/test/hubaccess/SemanticRebaseTestUtils.d.ts +3 -0
  101. package/lib/esm/test/hubaccess/SemanticRebaseTestUtils.d.ts.map +1 -0
  102. package/lib/esm/test/hubaccess/SemanticRebaseTestUtils.js +15 -0
  103. package/lib/esm/test/hubaccess/SemanticRebaseTestUtils.js.map +1 -0
  104. package/lib/esm/test/standalone/ChangesetReader.test.js.map +1 -1
  105. package/lib/esm/test/standalone/SchemaSyncDb.test.js +5 -25
  106. package/lib/esm/test/standalone/SchemaSyncDb.test.js.map +1 -1
  107. package/lib/esm/test/standalone/SchemaSyncElementReservation.test.js +1 -0
  108. package/lib/esm/test/standalone/SchemaSyncElementReservation.test.js.map +1 -1
  109. package/package.json +14 -14
@@ -317,7 +317,9 @@ export class IModelDb extends IModel {
317
317
  db[_nativeDb].abandonChanges();
318
318
  db.close();
319
319
  }
320
- catch { }
320
+ catch {
321
+ return;
322
+ }
321
323
  });
322
324
  });
323
325
  }
@@ -383,12 +385,20 @@ export class IModelDb extends IModel {
383
385
  this._codeService = undefined;
384
386
  this[_nativeDb].closeFile();
385
387
  }
388
+ /**
389
+ * Save any unsaved schema changes in this iModel.
390
+ * @internal
391
+ */
386
392
  saveSchemaChanges(args) {
387
393
  if (!this[_nativeDb].hasUnsavedChanges())
388
394
  return;
389
395
  const saveArgs = typeof args === "string" ? { description: args } : args;
390
396
  saveArgs === undefined ? this[_nativeDb].saveChanges() : this[_nativeDb].saveChanges(JSON.stringify(saveArgs));
391
397
  }
398
+ /**
399
+ * Abandon any unsaved schema changes in this iModel.
400
+ * @internal
401
+ */
392
402
  abandonSchemaChanges() {
393
403
  if (!this[_nativeDb].hasUnsavedChanges())
394
404
  return;
@@ -431,24 +441,30 @@ export class IModelDb extends IModel {
431
441
  }
432
442
  /**
433
443
  * Performs integrity checks on this iModel.
434
- * Types of integrity checks that can be performed are:
435
444
  *
436
- * Default Check:
437
- * - Quick Check: Runs all integrity checks below and returns whether each check passed or failed, without detailed results.
445
+ * The quick check runs the ten checks below other than Missing Child Rows. It reports pass/fail for each check
446
+ * based on the first problem found, without returning problem details. The specific checks return all problem rows.
447
+ * Selecting both quickCheck and specific checks repeats the selected work in both modes.
448
+ *
449
+ * The checks are:
450
+ * - Data Columns Check: Checks for missing nonvirtual physical columns in mapped data tables. It does not compare column types.
451
+ * - EC Profile Check: Checks expected EC profile tables, indexes, and iModel triggers for missing or mismatching SQL definitions. It does not check be_* tables.
452
+ * - Navigation Class Ids Check: Checks that each non-null persisted RelECClassId is the declared relationship class or one of its derived classes.
453
+ * - Navigation Ids Check: Checks that a navigation property's non-null .Id (the referenced instance's ECInstanceId) resolves to a row in the first relationship constraint class in the navigation direction, including derived classes.
454
+ * - Linktable Foreign Key Class Ids Check: Checks that SourceECClassId and TargetECClassId have matching meta.ECClassDef entries. This checks class existence only, not endpoint-class validity.
455
+ * - Linktable Foreign Key Ids Check: Checks SourceECInstanceId and TargetECInstanceId for null or an unresolved row in the first endpoint constraint class, including derived classes.
456
+ * - Class Ids Check: Checks persisted ECClassId values for a corresponding class definition. This checks class existence, not whether the class belongs in that table.
457
+ * - Data Schema Check: Checks for missing mapped data tables and indexes. Results report object names and types, not DDL differences.
458
+ * - Schema Load Check: Checks whether the schema manager can load each schema recorded in the database. A problem reports the schema name without a reason.
459
+ * - Missing Child Rows Check: Checks whether each existing bis_Element row has all required mapped child-table rows.
460
+ * - Diverged Property Maps Check: Checks whether an inherited property maps to different columns for a base and derived class under the same physical table root.
438
461
  *
439
- * Specific Checks:
440
- * - Data Columns Check: Checks if all the required columns exist in data tables. Issues are returned as a list of those tables/columns.
441
- * - EC Profile Check: Checks if the profile table, indexes, and triggers are present. Does not check be_* tables. Issues are returned as a list of tables/indexes/triggers which were not found or have different DDL.
442
- * - Navigation Class Ids Check: Checks if RelClassId of a Navigation property is a valid ECClassId. It does not check the value to match the relationship class.
443
- * - Navigation Ids Check: Checks if Id of a Navigation property matches a valid row primary class.
444
- * - Linktable Foreign Key Class Ids Check: Checks if SourceECClassId or TargetECClassId of a link table matches a valid ECClassId.
445
- * - Linktable Foreign Key Ids Check: Checks if SourceECInstanceId or TargetECInstanceId of a link table matches a valid row in primary class.
446
- * - Class Ids Check: Checks persisted ECClassId in all data tables and makes sure they are valid.
447
- * - Data Schema Check: Checks if all the required data tables and indexes exist for mapped classes. Issues are returned as a list of tables/columns which were not found or have different DDL.
448
- * - Schema Load Check: Checks if all schemas can be loaded into memory.
449
- * - Missing Child Rows Check: Checks if all child rows have a corresponding parent row.
462
+ * If no options are provided, or if quickCheck is false and no specific check is enabled, a quick check is performed by default.
463
+ * The checks are read-only and use the current primary connection. They have no callbacks, and the native operation
464
+ * blocks the backend while each check runs.
450
465
  *
451
- * @param options Options specifying which integrity checks to perform. If no options are provided or all options are false, a quick check will be performed by default.
466
+ * @see [ECSQL integrity checks]($docs/learning/ECSqlReference/Pragmas.md#pragma-integrity_check-experimental)
467
+ * @param options Options specifying which integrity checks to perform.
452
468
  * @returns An array of integrity check results.
453
469
  * @throws [[IModelError]] if the iModel is not open.
454
470
  * @beta
@@ -935,7 +951,7 @@ export class IModelDb extends IModel {
935
951
  async dropSchemas(schemaNames) {
936
952
  if (schemaNames.length === 0)
937
953
  return;
938
- if (this[_nativeDb].schemaSyncEnabled())
954
+ if (SchemaSync.isEnabled(this))
939
955
  throw new IModelError(DbResult.BE_SQLITE_ERROR, "Cannot drop schemas when schema sync is enabled");
940
956
  if (this[_nativeDb].hasUnsavedChanges())
941
957
  throw new IModelError(ChangeSetStatus.HasUncommittedChanges, "Cannot drop schemas with unsaved changes");
@@ -1021,26 +1037,34 @@ export class IModelDb extends IModel {
1021
1037
  this.cleanupSnapshot(context.resources);
1022
1038
  }
1023
1039
  }
1024
- /** Shared implementation for importing schemas from file or string. */
1025
- async importSchemasInternal(schemas, options, nativeImportOp) {
1026
- // BriefcaseDb-specific validation checks
1027
- if (this.isBriefcaseDb()) {
1028
- if (this.txns.rebaser.isRebasing) {
1029
- throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas while rebasing");
1030
- }
1031
- if (this.txns.isIndirectChanges) {
1032
- throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas while in an indirect change scope");
1033
- }
1034
- // Additional checks when semantic rebase is enabled
1035
- if (IModelHost.useSemanticRebase) {
1036
- if (this[_nativeDb].hasUnsavedChanges()) {
1037
- throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas with unsaved changes when useSemanticRebase flag is on");
1038
- }
1039
- if (this[_nativeDb].schemaSyncEnabled()) {
1040
- throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas when schema sync is enabled and also useSemanticRebase flag is on");
1041
- }
1040
+ /** Refuse a schema import this briefcase is in no state to run. Shared by [[importSchemas]] and
1041
+ * [[BriefcaseDb.upgradeSchemas]], which reach the native importer by different routes.
1042
+ * @internal
1043
+ */
1044
+ assertCanImportSchemas() {
1045
+ if (!this.isBriefcaseDb())
1046
+ return;
1047
+ if (this.txns.rebaser.isRebasing) {
1048
+ throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas while rebasing");
1049
+ }
1050
+ if (this.txns.isIndirectChanges) {
1051
+ throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas while in an indirect change scope");
1052
+ }
1053
+ // Additional checks when semantic rebase is enabled
1054
+ if (IModelHost.useSemanticRebase) {
1055
+ if (this[_nativeDb].hasUnsavedChanges()) {
1056
+ throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas with unsaved changes when useSemanticRebase flag is on");
1057
+ }
1058
+ if (SchemaSync.isEnabled(this)) {
1059
+ throw new IModelError(IModelStatus.BadRequest, "Cannot import schemas when schema sync is enabled and also useSemanticRebase flag is on");
1042
1060
  }
1043
1061
  }
1062
+ }
1063
+ /** Shared implementation for importing schemas from file or string.
1064
+ * @internal
1065
+ */
1066
+ async importSchemasInternal(schemas, options, nativeImportOp) {
1067
+ this.assertCanImportSchemas();
1044
1068
  if (options?.channelUpgrade) {
1045
1069
  const channelUpgrade = options.channelUpgrade;
1046
1070
  try {
@@ -1058,30 +1082,52 @@ export class IModelDb extends IModel {
1058
1082
  if (options?.schemaImportCallbacks?.preSchemaImportCallback)
1059
1083
  preSchemaImportCallbackResult = await this.preSchemaImportCallback(options.schemaImportCallbacks, { iModel: this, data: options.data, schemaData: schemas });
1060
1084
  const maybeCustomNativeContext = options?.ecSchemaXmlContext?.nativeContext;
1061
- if (this[_nativeDb].schemaSyncEnabled()) {
1062
- await SchemaSync.withLockedAccess(this, { openMode: OpenMode.Readonly, operationName: "schema sync" }, async (syncAccess) => {
1063
- const schemaSyncDbUri = syncAccess.getUri();
1064
- this.saveSchemaChanges();
1065
- try {
1066
- nativeImportOp(schemas, { schemaLockHeld: false, ecSchemaXmlContext: maybeCustomNativeContext, schemaSyncDbUri });
1067
- }
1068
- catch (outerErr) {
1069
- if (DbResult.BE_SQLITE_ERROR_DataTransformRequired === outerErr.errorNumber) {
1070
- this.abandonSchemaChanges();
1071
- if (this[_nativeDb].getITwinId() !== Guid.empty)
1072
- await this.acquireSchemaLock();
1073
- try {
1074
- nativeImportOp(schemas, { schemaLockHeld: true, ecSchemaXmlContext: maybeCustomNativeContext, schemaSyncDbUri });
1075
- }
1076
- catch (innerErr) {
1077
- throw new IModelError(innerErr.errorNumber, innerErr.message);
1078
- }
1085
+ if (SchemaSync.isEnabled(this)) {
1086
+ // The shared lock lets concurrent updates through while blocking anyone taking the exclusive lock for an upgrade.
1087
+ if (this[_nativeDb].getITwinId() !== Guid.empty)
1088
+ await this.locks.acquireLocks({ shared: IModel.repositoryModelId });
1089
+ // The native import adopts rows from the attached SchemaSyncDb. Attach and detach commit
1090
+ // implicitly, so abandoning changes cannot undo the adoption. A later failure must roll the
1091
+ // briefcase back before those ec_ rows can be pushed.
1092
+ let txnBeforeAdopt;
1093
+ // Set once the caller's own work is committed. Getting a token or the container write lock happens
1094
+ // before that, and abandonSchemaChanges discards every unsaved change rather than only this import's,
1095
+ // so abandoning on a failure from that window would throw away work the import never touched.
1096
+ let callerChangesSaved = false;
1097
+ try {
1098
+ await SchemaSync.withLockedAccess(this, { openMode: OpenMode.Readonly, operationName: "schema sync" }, async (syncAccess) => {
1099
+ const schemaSyncDbUri = syncAccess.getUri();
1100
+ this.saveSchemaChanges();
1101
+ callerChangesSaved = true;
1102
+ // After saveSchemaChanges, so rolling back cannot reach the caller's own work.
1103
+ if (this.isBriefcaseDb())
1104
+ txnBeforeAdopt = this.txns.getCurrentTxnId();
1105
+ try {
1106
+ nativeImportOp(schemas, { schemaLockHeld: false, ecSchemaXmlContext: maybeCustomNativeContext, schemaSyncDbUri });
1079
1107
  }
1080
- else {
1081
- throw new IModelError(outerErr.errorNumber, outerErr.message);
1108
+ catch (err) {
1109
+ if (SchemaSync.requiresUpgrade(err))
1110
+ throw new IModelError(err.errorNumber, `${err.message} - this schema change needs the upgrade path. Use BriefcaseDb.upgradeSchemas, which takes the exclusive schema lock and pushes the result.`);
1111
+ throw new IModelError(err.errorNumber, err.message);
1082
1112
  }
1113
+ });
1114
+ }
1115
+ catch (err) {
1116
+ if (callerChangesSaved)
1117
+ this.abandonSchemaChanges();
1118
+ if (this.isBriefcaseDb() && undefined !== txnBeforeAdopt && this.txns.getCurrentTxnId() !== txnBeforeAdopt) {
1119
+ // Saving the adopt's txn started a new undo session, because schema changes are kept out of the
1120
+ // user's undo stack, so this reverses across that boundary. The changes themselves reverse like
1121
+ // any other - a rebase does it to every local txn on every pull. It does not reverse DDL: a table
1122
+ // or column the adopt created stays behind with no ec_ row describing it, which is the state a
1123
+ // briefcase already tolerates, and the next successful import finds the table up to date and
1124
+ // carries on. Cancel rather than reverse, so the rows cannot come back through undo either.
1125
+ const status = this[_nativeDb].cancelTo(txnBeforeAdopt, true);
1126
+ if (IModelStatus.Success !== status)
1127
+ Logger.logError(loggerCategory, `Failed to roll back the adopted schema rows after a failed import: ${IModelStatus[status] ?? status}`);
1083
1128
  }
1084
- });
1129
+ throw err;
1130
+ }
1085
1131
  }
1086
1132
  else {
1087
1133
  const nativeImportOptions = {
@@ -2846,7 +2892,9 @@ function processSchemaWriteStatus(status) {
2846
2892
  try {
2847
2893
  props.push(imodel.elements.getElementProps(id));
2848
2894
  }
2849
- catch { }
2895
+ catch {
2896
+ return;
2897
+ }
2850
2898
  });
2851
2899
  return props;
2852
2900
  }
@@ -2869,7 +2917,9 @@ function processSchemaWriteStatus(status) {
2869
2917
  break;
2870
2918
  }
2871
2919
  }
2872
- catch { }
2920
+ catch {
2921
+ continue;
2922
+ }
2873
2923
  }
2874
2924
  return finished;
2875
2925
  }
@@ -3079,9 +3129,6 @@ export class BriefcaseDb extends IModelDb {
3079
3129
  get isBriefcase() { return true; }
3080
3130
  /* the BriefcaseId of the briefcase opened with this BriefcaseDb */
3081
3131
  briefcaseId;
3082
- _skipSyncSchemasOnPullAndPush;
3083
- /** @internal */
3084
- get skipSyncSchemasOnPullAndPush() { return this._skipSyncSchemasOnPullAndPush ?? false; }
3085
3132
  /**
3086
3133
  * Event raised just before a BriefcaseDb is opened. Supplies the arguments that will be used to open the BriefcaseDb.
3087
3134
  * Throw an exception to stop the open.
@@ -3164,6 +3211,8 @@ export class BriefcaseDb extends IModelDb {
3164
3211
  }
3165
3212
  constructor(args) {
3166
3213
  super({ ...args, changeset: args.nativeDb.getCurrentChangeset() });
3214
+ // Native invokes this private callback dynamically through the object registered by IModelDb.
3215
+ void this.onChangesetConflict;
3167
3216
  this._openMode = args.openMode;
3168
3217
  this.briefcaseId = args.briefcaseId;
3169
3218
  this.makeLockControl();
@@ -3176,26 +3225,33 @@ export class BriefcaseDb extends IModelDb {
3176
3225
  wasChanges = nativeDb.hasPendingTxns();
3177
3226
  nativeDb.closeFile();
3178
3227
  };
3179
- const isSchemaSyncEnabled = await withBriefcaseDb(briefcase, async (db) => {
3180
- await SchemaSync.pull(db);
3181
- return db[_nativeDb].schemaSyncEnabled();
3182
- });
3183
- if (isSchemaSyncEnabled) {
3228
+ const push = async () => {
3229
+ if (wasChanges)
3230
+ await withBriefcaseDb(briefcase, async (db) => db.pushChanges({ ...briefcase, description, retainLocks: true }));
3231
+ };
3232
+ // Asked of the closed file, so an iModel without schema sync never gets opened for this.
3233
+ if (SchemaSync.isEnabled(briefcase)) {
3234
+ await withBriefcaseDb(briefcase, async (db) => SchemaSync.updateDbSchema(db));
3184
3235
  await SchemaSync.withLockedAccess(briefcase, { openMode: OpenMode.Readonly, operationName: "schema sync" }, async (syncAccess) => {
3185
3236
  const schemaSyncDbUri = syncAccess.getUri();
3186
3237
  executeUpgrade();
3238
+ // Overwriting advances the SchemaSync data version, so a no-op upgrade must not call it.
3239
+ if (!wasChanges)
3240
+ return;
3187
3241
  await withBriefcaseDb(briefcase, async (db) => {
3188
- db[_nativeDb].schemaSyncPush(schemaSyncDbUri);
3242
+ db[_nativeDb].schemaSyncOverwrite(schemaSyncDbUri);
3189
3243
  db[_nativeDb].saveChanges();
3190
3244
  });
3191
- syncAccess.synchronizeWithCloud();
3245
+ // Publish the sync db before the changeset while retaining both locks.
3246
+ syncAccess.closeDb();
3247
+ await syncAccess.container.uploadChanges();
3248
+ await push();
3192
3249
  });
3193
3250
  }
3194
3251
  else {
3195
3252
  executeUpgrade();
3253
+ await push();
3196
3254
  }
3197
- if (wasChanges)
3198
- await withBriefcaseDb(briefcase, async (db) => db.pushChanges({ ...briefcase, description, retainLocks: true }));
3199
3255
  }
3200
3256
  /** Upgrades the schemas in the iModel based on the current version of the software. Follows a sequence of operations -
3201
3257
  * * Acquires a schema lock to prevent other users from making any other changes while upgrade is happening
@@ -3215,6 +3271,22 @@ export class BriefcaseDb extends IModelDb {
3215
3271
  // - push changes
3216
3272
  // - release schema lock
3217
3273
  // good thing computers are fast. Fortunately upgrading should be rare (and the push time will dominate anyway.) Don't try to optimize any of this away.
3274
+ // schemaSyncOverwrite can discard unpushed imports, so hold the exclusive lock across both upgrades.
3275
+ if (SchemaSync.isEnabled(briefcase)) {
3276
+ try {
3277
+ await withBriefcaseDb(briefcase, async (db) => {
3278
+ if (!db.locks.isServerBased)
3279
+ throw new IModelError(DbResult.BE_SQLITE_ERROR, "Cannot upgrade schemas with SchemaSync without server-based locking");
3280
+ await db.acquireSchemaLock();
3281
+ });
3282
+ await this.doUpgrade(briefcase, { profile: ProfileOptions.Upgrade, schemaLockHeld: true }, "Upgraded profile");
3283
+ await this.doUpgrade(briefcase, { domain: DomainOptions.Upgrade, schemaLockHeld: true }, "Upgraded domain schemas");
3284
+ }
3285
+ finally {
3286
+ await withBriefcaseDb(briefcase, async (db) => db.locks[_releaseAllLocks]());
3287
+ }
3288
+ return;
3289
+ }
3218
3290
  try {
3219
3291
  await this.doUpgrade(briefcase, { profile: ProfileOptions.Upgrade }, "Upgraded profile");
3220
3292
  }
@@ -3515,8 +3587,7 @@ export class BriefcaseDb extends IModelDb {
3515
3587
  async pullChanges(arg) {
3516
3588
  await this.executeWritable(async () => {
3517
3589
  await BriefcaseManager.pullAndApplyChangesets(this, arg ?? {});
3518
- if (!this.skipSyncSchemasOnPullAndPush)
3519
- await SchemaSync.pull(this);
3590
+ SchemaSync.updateDbSchema(this);
3520
3591
  this.initializeIModelDb("pullMerge");
3521
3592
  });
3522
3593
  // If this pull enabled or disabled SchemaSync for this briefcase, its reservations must now be re-initialized
@@ -3601,24 +3672,10 @@ export class BriefcaseDb extends IModelDb {
3601
3672
  if (nativeDb.hasPendingTxns()) {
3602
3673
  throw new IModelError(ChangeSetStatus.HasLocalChanges, "Cannot revert with pending txns");
3603
3674
  }
3604
- const skipSchemaSyncPull = async (func) => {
3605
- if (nativeDb.schemaSyncEnabled()) {
3606
- this._skipSyncSchemasOnPullAndPush = true;
3607
- try {
3608
- return await func();
3609
- }
3610
- finally {
3611
- this._skipSyncSchemasOnPullAndPush = undefined;
3612
- }
3613
- }
3614
- else {
3615
- return func();
3616
- }
3617
- };
3618
3675
  this.clearCaches();
3619
- await skipSchemaSyncPull(async () => this.pullChanges({ ...arg, toIndex: undefined }));
3676
+ await this.pullChanges({ ...arg, toIndex: undefined });
3620
3677
  await this.acquireSchemaLock();
3621
- if (nativeDb.schemaSyncEnabled()) {
3678
+ if (SchemaSync.isEnabled(this)) {
3622
3679
  arg.skipSchemaChanges = true;
3623
3680
  }
3624
3681
  // The native side enables file-based txns during revert. Restore the original setting afterward.
@@ -3639,7 +3696,7 @@ export class BriefcaseDb extends IModelDb {
3639
3696
  pushRetryDelay: arg.pushRetryDelay,
3640
3697
  retainLocks: arg.retainLocks,
3641
3698
  };
3642
- await skipSchemaSyncPull(async () => this.pushChanges(pushArgs));
3699
+ await this.pushChanges(pushArgs);
3643
3700
  this.clearCaches();
3644
3701
  }
3645
3702
  catch (err) {
@@ -3716,6 +3773,56 @@ export class BriefcaseDb extends IModelDb {
3716
3773
  this.txns._onChangesPushed(this.changeset);
3717
3774
  BriefcaseManager.deleteRebaseFolders(this);
3718
3775
  }
3776
+ /** Import schemas while allowing changes that move or delete data.
3777
+ *
3778
+ * Takes the exclusive schema lock, requires the briefcase to be at the timeline tip, imports, updates the sync db, and pushes the result.
3779
+ * @note The briefcase must have no local changes.
3780
+ * @see [[BriefcaseDb.upgradeSchemas]] (static) for upgrading the software's profile and domain schemas.
3781
+ * @alpha
3782
+ */
3783
+ async upgradeSchemas(schemaFileNames, arg) {
3784
+ return this.upgradeSchemasInternal(schemaFileNames, arg, (schemas, importOptions) => this[_nativeDb].importSchemas(schemas, importOptions));
3785
+ }
3786
+ /** The [[importSchemaStrings]] counterpart of [[upgradeSchemas]].
3787
+ * @alpha
3788
+ */
3789
+ async upgradeSchemaStrings(serializedXmlSchemas, arg) {
3790
+ return this.upgradeSchemasInternal(serializedXmlSchemas, arg, (schemas, importOptions) => this[_nativeDb].importXmlSchemas(schemas, importOptions));
3791
+ }
3792
+ async upgradeSchemasInternal(schemas, arg, nativeImportOp) {
3793
+ if (schemas.length === 0)
3794
+ return;
3795
+ this.assertCanImportSchemas();
3796
+ if (this[_nativeDb].hasUnsavedChanges() || this.txns.hasLocalChanges)
3797
+ throw new IModelError(ChangeSetStatus.HasLocalChanges, "Cannot upgrade schemas while there are local changes");
3798
+ if (SchemaSync.isEnabled(this) && !this.locks.isServerBased)
3799
+ throw new IModelError(DbResult.BE_SQLITE_ERROR, "Cannot upgrade schemas with SchemaSync without server-based locking");
3800
+ await this.acquireSchemaLock();
3801
+ await this.pullChanges({ accessToken: arg.accessToken });
3802
+ if (!SchemaSync.isEnabled(this)) {
3803
+ await this.importSchemasInternal(schemas, { ecSchemaXmlContext: arg.ecSchemaXmlContext }, nativeImportOp);
3804
+ await this.pushChanges(arg);
3805
+ return;
3806
+ }
3807
+ await SchemaSync.withLockedAccess(this, { openMode: OpenMode.Readonly, operationName: "schema upgrade" }, async (syncAccess) => {
3808
+ this.saveSchemaChanges();
3809
+ try {
3810
+ nativeImportOp(schemas, {
3811
+ schemaLockHeld: true,
3812
+ ecSchemaXmlContext: arg.ecSchemaXmlContext?.nativeContext,
3813
+ schemaSyncDbUri: syncAccess.getUri(),
3814
+ });
3815
+ }
3816
+ catch (err) {
3817
+ this.abandonSchemaChanges();
3818
+ throw new IModelError(err.errorNumber, err.message);
3819
+ }
3820
+ this.clearCaches();
3821
+ syncAccess.closeDb();
3822
+ await syncAccess.container.uploadChanges();
3823
+ await this.pushChanges(arg);
3824
+ });
3825
+ }
3719
3826
  close(options) {
3720
3827
  if (this.isBriefcase && this.isOpen && !this.isReadonly && this.txns.rebaser.inProgress()) {
3721
3828
  this[_nativeDb].abandonChanges();