Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow attach/detach db to IModelDb connection #7530

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions common/api/core-backend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1787,11 +1787,13 @@ export class ECDb implements IDisposable {
get [_nativeDb](): IModelJsNative.ECDb;
constructor();
abandonChanges(): void;
attachDb(fileName: string, alias: string): void;
// @internal
clearStatementCache(): void;
closeDb(): void;
createDb(pathName: string): void;
createQueryReader(ecsql: string, params?: QueryBinder, config?: QueryOptions): ECSqlReader;
detachDb(alias: string): void;
dispose(): void;
// @internal
getCachedStatementCount(): number;
Expand Down Expand Up @@ -3139,6 +3141,7 @@ export abstract class IModelDb extends IModel {
});
abandonChanges(): void;
acquireSchemaLock(): Promise<void>;
attachDb(fileName: string, alias: string): void;
// @internal
protected beforeClose(): void;
// @internal
Expand Down Expand Up @@ -3171,6 +3174,7 @@ export abstract class IModelDb extends IModel {
deleteFileProperty(prop: FilePropertyProps): void;
// @beta
deleteSettingDictionary(name: string): void;
detachDb(alias: string): void;
// @beta
elementGeometryCacheOperation(requestProps: ElementGeometryCacheOperationRequestProps): BentleyStatus;
// @beta
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-backend",
"comment": "Allow attach/detach db",
"type": "none"
}
],
"packageName": "@itwin/core-backend"
}
17 changes: 16 additions & 1 deletion core/backend/src/ECDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,22 @@ export class ECDb implements IDisposable {
this._nativeDb.dispose();
this._nativeDb = undefined;
}

/**
* Attach a ECDb file to this connection and load and register its schemas.
* @param fileName ECDb file name
* @param alias identifier for the attached file. This identifer is used a tablespace executing ECSQL queries.
*/
public attachDb(fileName: string, alias: string): void {
aruniverse marked this conversation as resolved.
Show resolved Hide resolved
this[_nativeDb].attachDb(fileName, alias);
}
/**
* Detach the attached file from this connection. The attached file is closed and its schemas are unregistered.
* @param alias identifer that was used to attach the file
*/
public detachDb(alias: string): void {
this.clearStatementCache();
this[_nativeDb].detachDb(alias);
}
/** Create an ECDb
* @param pathName The path to the ECDb file to create.
* @throws [IModelError]($common) if the operation failed.
Expand Down
17 changes: 16 additions & 1 deletion core/backend/src/IModelDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,22 @@ export abstract class IModelDb extends IModel {
});
}
}

/**
* Attach a iModel file to this connection and load and register its schemas.
* @param fileName IModel file name
* @param alias identifier for the attached file. This identifer is used a tablespace executing ECSQL queries.
*/
public attachDb(fileName: string, alias: string): void {
this[_nativeDb].attachDb(fileName, alias);
}
/**
* Detach the attached file from this connection. The attached file is closed and its schemas are unregistered.
* @param alias identifer that was used to attach the file
*/
public detachDb(alias: string): void {
this.clearCaches();
this[_nativeDb].detachDb(alias);
}
/** Close this IModel, if it is currently open, and save changes if it was opened in ReadWrite mode. */
public close(): void {
if (!this.isOpen)
Expand Down
203 changes: 203 additions & 0 deletions core/backend/src/test/ecdb/ECDb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DbResult, Id64, Id64String, Logger, using } from "@itwin/core-bentley";
import { ECDb, ECDbOpenMode, ECSqlInsertResult, ECSqlStatement, IModelJsFs, SqliteStatement, SqliteValue, SqliteValueType } from "../../core-backend";
import { KnownTestLocations } from "../KnownTestLocations";
import { ECDbTestHelper } from "./ECDbTestHelper";
import { QueryOptionsBuilder } from "@itwin/core-common";

describe("ECDb", () => {
const outDir = KnownTestLocations.outputDir;
Expand Down Expand Up @@ -58,7 +59,209 @@ describe("ECDb", () => {
assert.doesNotThrow(() => ecdb.openDb(ecdbPath, ECDbOpenMode.FileUpgrade));
});
});
it("attach/detach newer profile version", async () => {
const fileName1 = "source_file.ecdb";
const ecdbPath1: string = path.join(outDir, fileName1);
using(ECDbTestHelper.createECDb(outDir, fileName1,
`<ECSchema schemaName="Test" alias="test" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1">
<ECEntityClass typeName="Person" modifier="Sealed">
<ECProperty propertyName="Name" typeName="string"/>
<ECProperty propertyName="Age" typeName="int"/>
</ECEntityClass>
</ECSchema>`), (testECDb: ECDb) => {
assert.isTrue(testECDb.isOpen);
testECDb.withPreparedStatement("INSERT INTO test.Person(Name,Age) VALUES('Mary', 45)", (stmt: ECSqlStatement) => {
const res: ECSqlInsertResult = stmt.stepForInsert();
assert.equal(res.status, DbResult.BE_SQLITE_DONE);
assert.isDefined(res.id);
assert.isTrue(Id64.isValidId64(res.id!));
return res.id!;
});

// override profile version to 55.0.0 which is currently not supported
testECDb.withSqliteStatement(`
UPDATE be_Prop SET
StrData = '{"major":55,"minor":0,"sub1":0,"sub2":0}'
WHERE Namespace = 'ec_Db' AND Name = 'SchemaVersion'`,
(stmt: SqliteStatement) => { stmt.step(); });
testECDb.saveChanges();
});
const runDbListPragmaUsingStatement = (ecdb: ECDb) => {
return ecdb.withPreparedStatement("PRAGMA db_list", (stmt: ECSqlStatement) => {
const result: { alias: string, filename: string, profile: string }[] = [];
while (stmt.step() === DbResult.BE_SQLITE_ROW) {
result.push(stmt.getRow());
}
return result;
});
}
const runDbListPragmaCCQ = async (ecdb: ECDb) => {
const reader = ecdb.createQueryReader("PRAGMA db_list");
const result: { alias: string, filename: string, profile: string }[] = [];
while (await reader.step()) {
result.push(reader.current.toRow());
}
return result;
}
using(ECDbTestHelper.createECDb(outDir, "file2.ecdb"), (testECDb: ECDb) => {
// following call will not fail but unknow ECDb profile will cause it to be attach as SQLite.
testECDb.attachDb(ecdbPath1, "source");
expect(() => testECDb.withPreparedStatement("SELECT Name, Age FROM source.test.Person", () => { })).to.throw("ECClass 'source.test.Person' does not exist or could not be loaded.");
expect(runDbListPragmaUsingStatement(testECDb)).deep.equals([
{
sno: 0,
alias: "main",
fileName: path.join(outDir, "file2.ecdb"),
profile: "ECDb"
},
{
sno: 1,
alias: "source",
fileName: path.join(outDir, "source_file.ecdb"),
profile: "SQLite"
}
]);
testECDb.detachDb("source");
expect(runDbListPragmaUsingStatement(testECDb)).deep.equals([
{
sno: 0,
alias: "main",
fileName: path.join(outDir, "file2.ecdb"),
profile: "ECDb"
},
]);
expect(() => testECDb.withPreparedStatement("SELECT Name, Age FROM source.test.Person", () => { })).to.throw("ECClass 'source.test.Person' does not exist or could not be loaded.");
});
await using(ECDbTestHelper.createECDb(outDir, "file4.ecdb"), async (testECDb: ECDb) => {
testECDb.attachDb(ecdbPath1, "source");
const reader1 = testECDb.createQueryReader("SELECT Name, Age FROM source.test.Person");
let expectThrow = false;
try {
await reader1.step();
} catch (err) {
if (err instanceof Error) {
assert.equal(err.message, "ECClass 'source.test.Person' does not exist or could not be loaded.");
expectThrow = true;
}
}
assert.isTrue(expectThrow);
expect(await runDbListPragmaCCQ(testECDb)).deep.equals([
{
sno: 0,
alias: "main",
fileName: path.join(outDir, "file4.ecdb"),
profile: "ECDb"
},
{
sno: 1,
alias: "source",
fileName: path.join(outDir, "source_file.ecdb"),
profile: "SQLite"
}
]);
testECDb.detachDb("source");
expect(await runDbListPragmaCCQ(testECDb)).deep.equals([
{
sno: 0,
alias: "main",
fileName: path.join(outDir, "file4.ecdb"),
profile: "ECDb"
},
]);
});
});
it("attach/detach file & db_list pragma", async () => {
const fileName1 = "source_file.ecdb";
const ecdbPath1: string = path.join(outDir, fileName1);
using(ECDbTestHelper.createECDb(outDir, fileName1,
`<ECSchema schemaName="Test" alias="test" version="01.00.00" xmlns="http://www.bentley.com/schemas/Bentley.ECXML.3.1">
<ECEntityClass typeName="Person" modifier="Sealed">
<ECProperty propertyName="Name" typeName="string"/>
<ECProperty propertyName="Age" typeName="int"/>
</ECEntityClass>
</ECSchema>`), (testECDb: ECDb) => {
assert.isTrue(testECDb.isOpen);
testECDb.withPreparedStatement("INSERT INTO test.Person(Name,Age) VALUES('Mary', 45)", (stmt: ECSqlStatement) => {
const res: ECSqlInsertResult = stmt.stepForInsert();
assert.equal(res.status, DbResult.BE_SQLITE_DONE);
assert.isDefined(res.id);
assert.isTrue(Id64.isValidId64(res.id!));
return res.id!;
});
testECDb.saveChanges();
});
const runDbListPragma = (ecdb: ECDb) => {
return ecdb.withPreparedStatement("PRAGMA db_list", (stmt: ECSqlStatement) => {
const result: { alias: string, filename: string, profile: string }[] = [];
while (stmt.step() === DbResult.BE_SQLITE_ROW) {
result.push(stmt.getRow());
}
return result;
});
}
using(ECDbTestHelper.createECDb(outDir, "file2.ecdb"), (testECDb: ECDb) => {
testECDb.attachDb(ecdbPath1, "source");
testECDb.withPreparedStatement("SELECT Name, Age FROM source.test.Person", (stmt: ECSqlStatement) => {
assert.equal(stmt.step(), DbResult.BE_SQLITE_ROW);
const row = stmt.getRow();
assert.equal(row.name, "Mary");
assert.equal(row.age, 45);
});
expect(runDbListPragma(testECDb)).deep.equals([
{
sno: 0,
alias: "main",
fileName: path.join(outDir, "file2.ecdb"),
profile: "ECDb"
},
{
sno: 1,
alias: "source",
fileName: path.join(outDir, "source_file.ecdb"),
profile: "ECDb"
}
]);
testECDb.detachDb("source");
expect(runDbListPragma(testECDb)).deep.equals([
{
sno: 0,
alias: "main",
fileName: path.join(outDir, "file2.ecdb"),
profile: "ECDb"
},
]);
expect(() => testECDb.withPreparedStatement("SELECT Name, Age FROM source.test.Person", () => { })).to.throw("ECClass 'source.test.Person' does not exist or could not be loaded.");
});
await using(ECDbTestHelper.createECDb(outDir, "file3.ecdb"), async (testECDb: ECDb) => {
testECDb.attachDb(ecdbPath1, "source");
const reader = testECDb.createQueryReader("SELECT Name, Age FROM source.test.Person", undefined, new QueryOptionsBuilder().setUsePrimaryConnection(true).getOptions());
assert.equal(await reader.step(), true);
assert.equal(reader.current.name, "Mary");
assert.equal(reader.current.age, 45);
testECDb.detachDb("source");
});

await using(ECDbTestHelper.createECDb(outDir, "file4.ecdb"), async (testECDb: ECDb) => {
testECDb.attachDb(ecdbPath1, "source");
const reader = testECDb.createQueryReader("SELECT Name, Age FROM source.test.Person");
assert.equal(await reader.step(), true);
assert.equal(reader.current.name, "Mary");
assert.equal(reader.current.age, 45);
testECDb.detachDb("source");
const reader1 = testECDb.createQueryReader("SELECT Name, Age FROM source.test.Person");
let expectThrow = false;
try {
await reader1.step();
} catch (err) {
if (err instanceof Error) {
assert.equal(err.message, "ECClass 'source.test.Person' does not exist or could not be loaded.");
expectThrow = true;
}
}
assert.isTrue(expectThrow);
});

});
it("should be able to import a schema", () => {
const fileName = "schemaimport.ecdb";
const ecdbPath: string = path.join(outDir, fileName);
Expand Down
Loading
Loading