Skip to content

Commit

Permalink
Fix aspect queries when class name is reserved SQLite keyword (#123)
Browse files Browse the repository at this point in the history
This PR fixes a problem where aspect class has SQLite reserved keyword
as its name. If EC class name has SQLite reserved keyword as its name,
the class name must be quoted `SELECT * FROM
schema.[SQLiteReservedKeyword]`
  • Loading branch information
mindaugasdirg authored Sep 26, 2023
1 parent 0e45745 commit dc46202
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Fix aspect queries when class name is reserved SQLite keyword",
"packageName": "@itwin/imodel-transformer",
"email": "Mindaugas.Dirgincius@bentley.com",
"dependentChangeType": "patch"
}
15 changes: 8 additions & 7 deletions packages/transformer/src/DetachedExportElementAspectsStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ export class DetachedExportElementAspectsStrategy extends ExportElementAspectsSt
}

private async *queryAspects<T extends ElementAspect>(baseElementAspectClassFullName: string) {
const aspectClassNameIdMap = new Map<string, Id64String>();
const aspectClassNameIdMap = new Map<Id64String, { schemaName: string, className: string }>();

const optimizesAspectClassesSql = `
SELECT c.ECInstanceId as classId, (ec_className(c.ECInstanceId, 's:c')) as className
SELECT c.ECInstanceId as classId, (ec_className(c.ECInstanceId, 's')) as schemaName, (ec_className(c.ECInstanceId, 'c')) as className
FROM ECDbMeta.ClassHasAllBaseClasses r
JOIN ECDbMeta.ECClassDef c ON c.ECInstanceId = r.SourceECInstanceId
WHERE r.TargetECInstanceId = ec_classId(:baseClassName)
Expand All @@ -77,20 +77,21 @@ export class DetachedExportElementAspectsStrategy extends ExportElementAspectsSt
const aspectClassesAsyncQueryReader = ensureECSqlReaderIsAsyncIterableIterator(aspectClassesQueryReader);
for await (const rowProxy of aspectClassesAsyncQueryReader) {
const row = rowProxy.toRow();
aspectClassNameIdMap.set(row.className, row.classId);
aspectClassNameIdMap.set(row.classId, { schemaName: row.schemaName, className: row.className });
}

for (const [className, classId] of aspectClassNameIdMap) {
if(this.excludedElementAspectClassFullNames.has(className))
for (const [classId, { schemaName, className }] of aspectClassNameIdMap) {
const classFullName = `${schemaName}:${className}`;
if(this.excludedElementAspectClassFullNames.has(classFullName))
continue;

const getAspectPropsSql = `SELECT * FROM ${className} WHERE ECClassId = :classId ORDER BY Element.Id`;
const getAspectPropsSql = `SELECT * FROM [${schemaName}]:[${className}] WHERE ECClassId = :classId ORDER BY Element.Id`;
const aspectQueryReader = this.sourceDb.createQueryReader(getAspectPropsSql, new QueryBinder().bindId("classId", classId), { rowFormat: QueryRowFormat.UseJsPropertyNames });
const aspectAsyncQueryReader = ensureECSqlReaderIsAsyncIterableIterator(aspectQueryReader);
let firstDone = false;
for await (const rowProxy of aspectAsyncQueryReader) {
const row = rowProxy.toRow();
const aspectProps: ElementAspectProps = { ...row, classFullName: className, className: undefined }; // add in property required by EntityProps
const aspectProps: ElementAspectProps = { ...row, classFullName, className: undefined }; // add in property required by EntityProps
if (!firstDone) {
firstDone = true;
}
Expand Down
58 changes: 58 additions & 0 deletions packages/transformer/src/test/standalone/IModelTransformer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2565,6 +2565,64 @@ describe("IModelTransformer", () => {
});
});

it("should transform all aspects when detachedAspectProcessing is turned on and schema name and aspect class name has SQLite reserved keyword", async () => {
// arrange
// prepare source
const sourceDbFile: string = IModelTransformerTestUtils.prepareOutputFile("IModelTransformer", "DetachedAspectProcessingWithReservedSQLiteKeyword.bim");
const sourceDb = SnapshotDb.createEmpty(sourceDbFile, { rootSubject: { name: "DetachedAspectProcessingWithReservedSQLiteKeyword" } });
const elements = [
Subject.insert(sourceDb, IModel.rootSubjectId, "Subject1"),
Subject.insert(sourceDb, IModel.rootSubjectId, "Subject2"),
];
const customSchema = `<?xml version="1.0" encoding="UTF-8"?>
<ECSchema schemaName="SELECT" alias="cs" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1" description="Custom schema to test aspect class which has SQLite reserved keyword as its name">
<ECSchemaReference name="BisCore" version="01.00.04" alias="bis"/>
<ECEntityClass typeName="JOIN" modifier="Sealed" description="Aspect class with SQLite reserved keyword">
<BaseClass>bis:ElementMultiAspect</BaseClass>
</ECEntityClass>
</ECSchema>
`;
await sourceDb.importSchemaStrings([customSchema]);

// 10 aspects in total (5 per element)
elements.forEach((element) => {
for (let i = 0; i < 5; ++i) {
const aspectProps: ElementAspectProps = {
classFullName: "SELECT:JOIN",
element: new ElementOwnsMultiAspects(element),
};

sourceDb.elements.insertAspect(aspectProps);
}
});

sourceDb.saveChanges();

// create target iModel
const targetDbFile: string = IModelTransformerTestUtils.prepareOutputFile("IModelTransformer", "DetachedAspectProcessingWithReservedSQLiteKeyword-Target.bim");
const targetDb = StandaloneDb.createEmpty(targetDbFile, { rootSubject: { name: "DetachedAspectProcessingWithReservedSQLiteKeyword-Target" } });

const exporter = new IModelExporter(sourceDb, DetachedExportElementAspectsStrategy);
const transformer = new IModelTransformer(exporter, targetDb, {
includeSourceProvenance: true,
});

// act
await transformer.processAll();
targetDb.saveChanges();

// assert
const elementIds = targetDb.queryEntityIds({ from: Subject.classFullName });
elementIds.forEach((elementId) => {
if (elementId === IModel.rootSubjectId) {
return;
}
const targetAspects = targetDb.elements.getAspects(elementId, ExternalSourceAspect.classFullName);
const sourceAspects = sourceDb.elements.getAspects(elementId, ExternalSourceAspect.classFullName);
expect(targetAspects.length).to.be.equal(sourceAspects.length + 1); // +1 because provenance aspect was added
});
});

it("should remap textures in target iModel", async function () {
const atleastInItjs4x = Semver.gte(coreBackendPkgJson.version, "4.0.0");
if (!atleastInItjs4x)
Expand Down

0 comments on commit dc46202

Please sign in to comment.