Skip to content
18 changes: 18 additions & 0 deletions common/api/core-backend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,18 @@ export class ChangedElementsDb implements Disposable {
processChangesetsAndRoll(accessToken: AccessToken, briefcase: IModelDb, options: ProcessChangesetOptions): Promise<DbResult>;
}

// @beta
export interface ChangeElementModelProps {
id: Id64String;
modelId: Id64String;
}

// @beta
export interface ChangeElementParentProps {
id: Id64String;
parentId: Id64String;
}

// @beta
export interface ChangeFormatArgs {
includeNullColumns?: true;
Expand Down Expand Up @@ -2734,6 +2746,8 @@ export interface EditableWorkspaceDb extends WorkspaceDb {
export class EditTxn {
constructor(iModel: IModelDb, description: string);
abandonChanges(): void;
changeElementModel(props: ChangeElementModelProps): void;
changeElementParent(props: ChangeElementParentProps): void;
deleteAspect(aspectInstanceIds: Id64Arg): void;
deleteDefinitionElements(definitionElementIds: Id64Array): Id64Set;
deleteElement(ids: Id64Arg): void;
Expand Down Expand Up @@ -4219,6 +4233,10 @@ export namespace IModelDb {
readonly [_instanceKeyCache]: InstanceKeyLRUCache;
// @internal
constructor(_iModel: IModelDb);
// @beta @deprecated
changeElementModel(props: ChangeElementModelProps): void;
// @beta @deprecated
changeElementParent(props: ChangeElementParentProps): void;
createElement<T extends Element_2>(elProps: ElementProps): T;
// @deprecated
deleteAspect(aspectInstanceIds: Id64Arg): void;
Expand Down
14 changes: 14 additions & 0 deletions common/api/core-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3011,6 +3011,20 @@ export interface ElementAspectProps extends EntityProps {
element: RelatedElementProps;
}

// @beta
export namespace ElementError {
const // (undocumented)
scope = "itwin-element";
export function isError(error: unknown, key?: Key): error is ITwinError;
// (undocumented)
export type Key =
/** The element's model type does not match the expected model type for the operation */
"model-type-mismatch" |
/** Invalid arguments were provided to an element operation */
"invalid-arguments";
export function throwError(key: Key, message: string): never;
}

// @beta
export namespace ElementGeometry {
export function appendGeometryParams(geomParams: GeometryParams, entries: ElementGeometryDataEntry[], worldToLocal?: Transform): boolean;
Expand Down
2 changes: 2 additions & 0 deletions common/api/summary/core-backend.exports.csv
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ beta;interface;ChangeCache
beta;interface;ChangedECInstance
deprecated;interface;ChangedECInstance
internal;class;ChangedElementsDb
beta;interface;ChangeElementModelProps
beta;interface;ChangeElementParentProps
beta;interface;ChangeFormatArgs
beta;interface;ChangeInstance
beta;interface;ChangeInstanceKey
Expand Down
1 change: 1 addition & 0 deletions common/api/summary/core-common.exports.csv
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ public;type;ElementAlignedBox2d
public;type;ElementAlignedBox3d
public;interface;ElementAspectProps
preview;interface;ElementAspectProps
beta;namespace;ElementError
beta;namespace;ElementGeometry
public;interface;ElementGeometryBuilderParams
public;interface;ElementGeometryBuilderParamsForPart
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Add EditTxn.changeElementParent and EditTxn.changeElementModel; add ElementError namespace.",
"type": "none",
"packageName": "@itwin/core-backend"
}
],
"packageName": "@itwin/core-backend",
"email": "khanaffan@users.noreply.github.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Add EditTxn.changeElementParent and EditTxn.changeElementModel; add ElementError namespace.",
"type": "none",
"packageName": "@itwin/core-common"
}
],
"packageName": "@itwin/core-common",
"email": "khanaffan@users.noreply.github.com"
}
130 changes: 127 additions & 3 deletions core/backend/src/EditTxn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
*/

import { DbResult, Id64, Id64Arg, Id64Array, Id64Set, Id64String, IModelStatus, ITwinError, OpenMode } from "@itwin/core-bentley";
import { EcefLocation, EcefLocationProps, EditTxnError, ElementAspectProps, ElementProps, FilePropertyProps, IModelError, ModelProps, RelationshipProps, SaveChangesArgs } from "@itwin/core-common";
import { EcefLocation, EcefLocationProps, EditTxnError, ElementAspectProps, ElementError, ElementProps, FilePropertyProps, IModelError, ModelProps, RelationshipProps, SaveChangesArgs } from "@itwin/core-common";
import { Range3d, Range3dProps } from "@itwin/core-geometry";
import type { CloudSqlite } from "./CloudSqlite";
import type { ImplicitWriteEnforcement } from "./IModelHost";
import type { IModelDb, InsertElementOptions, UpdateModelOptions } from "./IModelDb";
import type { ChangeElementModelProps, ChangeElementParentProps, IModelDb, InsertElementOptions, UpdateModelOptions } from "./IModelDb";
import type { SettingsContainer } from "./workspace/Settings";
import { _activeTxn, _cache, _instanceKeyCache, _nativeDb } from "./internal/Symbols";
import { _activeTxn, _cache, _instanceKeyCache, _nativeDb, _verifyChannel } from "./internal/Symbols";

/** Options for bulk deleting elements from an iModelDb.
* @beta
Expand Down Expand Up @@ -273,6 +273,130 @@ export class EditTxn {
});
}

/** Change the parent of an element within its model.
*
* The new parent must be in the same model as the element. Reparenting across models is not
* allowed; to move an element into a different model use [[changeElementModel]] instead.
* Only the target element is reparented — its children and their model membership are unaffected.
*
* **Blocked cases** (will throw):
* - The new parent is in a different model than the element.
* - Element has a `ParentElement`-scoped code (code uniqueness is tied to the parent; use delete+insert instead).
*
* **Allowed cases**:
* - Element has a `Repository`-scoped code (unique across entire iModel — unaffected by the parent change).
* - Element has a `RelatedElement`-scoped code (scope element is independent of the parent).
* - Element has a `Model`-scoped code (the model does not change, so the code remains valid).
* - Element has no meaningful code (empty code).
*
* Channel verification is performed on the element's model.
* Lock enforcement: requires an exclusive lock on the element, and a shared lock on the new parent.
* @param props The reparent parameters: element id and new parent id.
* @throws EditTxnError if this EditTxn is not active.
* @throws [[ITwinError]] if the operation fails.
* @beta
*/
public changeElementParent(props: ChangeElementParentProps): void {
this.verifyWriteable();
const iModel = this.iModel;

// Lock enforcement: exclusive lock on the element being reparented, shared lock on the new parent.
iModel.locks.checkExclusiveLock(props.id, "element", "changeParent");
iModel.locks.checkSharedLock(props.parentId, "parent", "changeParent");

// The new parent must be in the same model as the element. Reparenting across models is not
// allowed here — use changeElementModel to move an element into a different model. Check this up
// front so consumers get a clear error instead of the addon's lower-level "wrong model" status.
const sourceModelId = iModel.elements.getElementProps({ id: props.id }).model;
const parentModelId = iModel.elements.getElementProps({ id: props.parentId }).model;
if (sourceModelId !== parentModelId)
ElementError.throwError("invalid-arguments", `cannot reparent element '${props.id}' to a parent in a different model ('${parentModelId}' != '${sourceModelId}'); use changeElementModel to move an element to a different model`);

// Channel verification on the element's model.
iModel.channels[_verifyChannel](sourceModelId);

// Invalidate caches for the element being reparented.
iModel.elements[_cache].delete({ id: props.id });
iModel.elements[_instanceKeyCache].deleteById(props.id);

try {
iModel[_nativeDb].changeElementParent({ id: props.id, parentId: props.parentId });
} catch (err: any) {
err.message = `Error changing element parent [${err.message}], id: ${props.id}, parentId: ${props.parentId}`;
err.metadata = { props };
throw err;
}

// The model is unchanged and descendants are not moved, so only the reparented element's cache is stale.
iModel.elements[_cache].delete({ id: props.id });
iModel.elements[_instanceKeyCache].deleteById(props.id);
}

/** Change the model of a root element, making it a root element in the new model.
*
* The element must not have a parent; reparent it first with [[changeElementParent]] if needed.
* Only the target element is moved — its children remain in their current model.
*
* **Blocked cases** (will throw):
* - Element has a parent (only root elements can be moved between models).
* - Element has a `Model`-scoped code (code uniqueness is tied to the source model; use delete+insert instead).
* - Element has a `ParentElement`-scoped code (use delete+insert instead).
*
* **Allowed cases**:
* - Element has a `Repository`-scoped code (unique across entire iModel — unaffected by the model change).
* - Element has a `RelatedElement`-scoped code (scope element is independent of the model).
* - Element has no meaningful code (empty code).
*
* The source and target models must be of the same class (classFullName must match exactly).
* Channel verification is performed on both the source and target models.
* Lock enforcement: requires an exclusive lock on the element, and a shared lock on the target model.
* @param props The model change parameters: element id and target model id.
* @throws EditTxnError if this EditTxn is not active.
* @throws [[ITwinError]] if the operation fails.
* @beta
*/
public changeElementModel(props: ChangeElementModelProps): void {
this.verifyWriteable();
const iModel = this.iModel;

// Lock enforcement: exclusive lock on element
iModel.locks.checkExclusiveLock(props.id, "element", "changeModel");

// Resolve the source model
const sourceModelId = iModel.elements.getElementProps({ id: props.id }).model;

// Channel verification on the source model
iModel.channels[_verifyChannel](sourceModelId);

// Model type check: source and target models must be the same class
const sourceModel = iModel.models.getModel(sourceModelId);
const targetModel = iModel.models.getModel(props.modelId);
if (sourceModel.classFullName !== targetModel.classFullName)
ElementError.throwError("model-type-mismatch", `cannot move element from model of type '${sourceModel.classFullName}' to model of type '${targetModel.classFullName}'`);

// Shared lock on target model
iModel.locks.checkSharedLock(props.modelId, "model", "changeModel");

// Channel verification on the target model
iModel.channels[_verifyChannel](props.modelId);

// Invalidate caches
iModel.elements[_cache].delete({ id: props.id });
iModel.elements[_instanceKeyCache].deleteById(props.id);

try {
iModel[_nativeDb].changeElementModel({ id: props.id, modelId: props.modelId });
} catch (err: any) {
err.message = `Error changing element model [${err.message}], id: ${props.id}, modelId: ${props.modelId}`;
err.metadata = { props };
throw err;
}

// Only the moved element changes model; descendants are not moved, so only its cache is stale.
iModel.elements[_cache].delete({ id: props.id });
iModel.elements[_instanceKeyCache].deleteById(props.id);
}

/**
* Delete multiple elements from the iModel.
* @param ids The ids of the elements to delete. All ids must be well-formed and valid [[Id64String]]s.
Expand Down
69 changes: 68 additions & 1 deletion core/backend/src/IModelDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ import type { BlobContainer } from "./BlobContainerService";
import { createNoOpLockControl } from "./internal/NoLocks";
import { IModelDbFonts } from "./IModelDbFonts";
import { createIModelDbFonts } from "./internal/IModelDbFontsImpl";
import { _activeTxn, _cache, _close, _hubAccess, _implicitTxn, _instanceKeyCache, _nativeDb, _releaseAllLocks, _resetIModelDb } from "./internal/Symbols";
import { _activeTxn, _cache, _close, _hubAccess, _implicitTxn, _instanceKeyCache, _nativeDb, _releaseAllLocks, _resetIModelDb, _verifyChannel } from "./internal/Symbols";
Comment thread
aruniverse marked this conversation as resolved.
Outdated
import { ECSpecVersion, ECVersion, SchemaContext, SchemaJsonLocater, SchemaView } from "@itwin/ecschema-metadata";
import { SchemaMap } from "./Schema";
import { ElementLRUCache, InstanceKeyLRUCache } from "./internal/ElementLRUCache";
Expand Down Expand Up @@ -142,6 +142,53 @@ export interface InsertElementOptions {
forceUseId?: boolean;
}

/** Options for [[EditTxn.changeElementParent]].
* Changes the parent of an element within its model. The new parent must be in the same model as the
* element; reparenting across models is not allowed (use [[EditTxn.changeElementModel]] for that).
* Only the target element is reparented — its children and their model membership are unaffected.
*
* **Blocked cases** (will throw):
Comment thread
hl662 marked this conversation as resolved.
Outdated
* - The new parent is in a different model than the element.
* - Element has a `ParentElement`-scoped code (code uniqueness is tied to parent; use delete+insert instead).
*
* **Allowed cases**:
* - Element has a `Repository`-scoped code (unique across entire iModel — unaffected by the parent change).
* - Element has a `RelatedElement`-scoped code (scope element is independent of parent).
* - Element has a `Model`-scoped code (the model does not change, so the code remains valid).
* - Element has no meaningful code (empty code).
*
* @beta
*/
export interface ChangeElementParentProps {
/** The Id of the element to reparent. */
id: Id64String;
/** The Id of the new parent element. Must be in the same model as the element. */
parentId: Id64String;
}

/** Options for [[EditTxn.changeElementModel]].
* Changes the model of a root element (one with no parent), making it a root element in the new model.
* Only the target element is moved — its children remain in their current model.
*
* **Blocked cases** (will throw):
* - Element has a parent (only root elements can be moved between models; reparent first with [[EditTxn.changeElementParent]]).
* - Element has a `Model`-scoped code (code uniqueness is tied to source model; use delete+insert instead).
* - Element has a `ParentElement`-scoped code (use delete+insert instead).
*
* **Allowed cases**:
* - Element has a `Repository`-scoped code (unique across entire iModel — unaffected by model change).
* - Element has a `RelatedElement`-scoped code (scope element is independent of model).
* - Element has no meaningful code (empty code).
*
* @beta
*/
export interface ChangeElementModelProps {
/** The Id of the element to move. Must be a root element (no parent). */
id: Id64String;
/** The Id of the target model. The element becomes a root element (no parent) in this model. */
modelId: Id64String;
}

/** Options supplied to [[IModelDb.clearCaches]].
* @beta
*/
Expand Down Expand Up @@ -3205,6 +3252,26 @@ export namespace IModelDb {
public deleteAspect(aspectInstanceIds: Id64Arg): void {
this._iModel[_implicitTxn].deleteAspect(aspectInstanceIds);
}

/** Change the parent of an element.
* @param props The properties specifying the element to reparent and its new parent.
* @throws [[IModelError]] if unable to change the element's parent.
* @beta
* @deprecated in 5.9.0 - will not be removed until after 2026-08-04. Use EditTxn.changeElementParent instead, within an explicit EditTxn scope (or via withEditTxn). See EditTxn documentation for migration help.
Comment thread
aruniverse marked this conversation as resolved.
Outdated
*/
public changeElementParent(props: ChangeElementParentProps): void {
this._iModel[_implicitTxn].changeElementParent(props);
}

/** Change the model of an element.
* @param props The properties specifying the element to move and its new model.
* @throws [[IModelError]] if unable to change the element's model.
* @beta
* @deprecated in 5.9.0 - will not be removed until after 2026-08-04. Use EditTxn.changeElementModel instead, within an explicit EditTxn scope (or via withEditTxn). See EditTxn documentation for migration help.
Comment thread
aruniverse marked this conversation as resolved.
Outdated
*/
public changeElementModel(props: ChangeElementModelProps): void {
this._iModel[_implicitTxn].changeElementModel(props);
}
}

/** The collection of views in an [[IModelDb]].
Expand Down
Loading
Loading