Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
ff1def0
Add support for schema_view_fragment pragma to return binary blobs of…
rschili Jun 20, 2026
b0d3904
Add schema_token support to PRAGMA checksum for cheap schema-identity…
rschili Jun 23, 2026
7628fec
Change primitiveType from uint8_t to uint16_t in WriteEnumTable for f…
rschili Jun 23, 2026
bfe4019
Refactor SchemaViewWriter to use SafeU8 and SafeU16 for type safety a…
rschili Jun 23, 2026
3236785
Merge branch 'main' into rschili/schema-view-fragment
rschili Jun 26, 2026
5bc7cd3
Refactor schema view handling and improve documentation in ECSqlPragm…
rschili Jun 26, 2026
2d7edf5
Update schema view fragment handling to accept duplicate IDs and reje…
rschili Jun 26, 2026
4204b06
Return proper error message if schema token pragma call fails
rschili Jun 26, 2026
0f80f4b
Merge branch 'main' into rschili/schema-view-fragment
rschili Jun 30, 2026
fdc68c2
Enhance comments in SafeU8 function to clarify validation approach an…
rschili Jun 30, 2026
2587792
Merge branch 'rschili/schema-view-fragment' of https://github.com/iTw…
rschili Jun 30, 2026
85fa670
Update PragmaSchemaViewFragment to accept schema names instead of IDs…
rschili Jul 3, 2026
bd429b1
Merge branch 'main' into rschili/schema-view-fragment
rschili Jul 20, 2026
855dd85
Merge branch 'main' into rschili/schema-view-fragment
rschili Jul 20, 2026
f8f719d
Merge branch 'main' into rschili/schema-view-fragment
rschili Jul 23, 2026
35e1e86
Merge branch 'main' into rschili/schema-view-fragment
rschili Jul 23, 2026
7ad44a4
Merge branch 'main' into rschili/schema-view-fragment
rschili Jul 24, 2026
0cd13dd
Refactor comments for clarity and conciseness in QueryHelper, SHA3Hel…
rschili Jul 25, 2026
c9decf7
Merge branch 'main' into rschili/schema-view-fragment
rschili Jul 27, 2026
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
6 changes: 4 additions & 2 deletions iModelCore/ECDb/ECDb/ConcurrentQueryManagerImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1163,8 +1163,10 @@ std::string QueryHelper::FormatQuery(const char* query) {
// @bsimethod
//---------------------------------------------------------------------------------------
void QueryHelper::BindLimits(ECSqlStatement& stmt, QueryLimit const& limit) {
const auto idxCount = stmt.GetParameterIndex(LIMIT_VAR_COUNT);
const auto idxOffset = stmt.GetParameterIndex(LIMIT_VAR_OFFSET);
// A PRAGMA (or any statement FormatQuery left unwrapped) has no LIMIT/OFFSET parameters.
// TryGetParameterIndex probes for them without logging a "No parameter index found" error each time.
const auto idxCount = stmt.TryGetParameterIndex(LIMIT_VAR_COUNT);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@khanaffan are you okay with this change?
The previous code was logging a warning because pragma going through concurrent query does not have those two parameters.
This change makes it so we don't log those warnings anymore.
Unrelated change, sneaking it in because it's small and insignificant.

const auto idxOffset = stmt.TryGetParameterIndex(LIMIT_VAR_OFFSET);
if (idxCount < 0 || idxOffset < 0)
return; // PRAGMA or other statement without LIMIT/OFFSET parameters
stmt.BindInt64(idxCount, limit.GetCount());
Expand Down
1 change: 1 addition & 0 deletions iModelCore/ECDb/ECDb/ECDbImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ void ECDb::Impl::RegisterECSqlPragmas() const
GetPragmaManager().Register(PragmaECSqlVersion::Create());
GetPragmaManager().Register(PragmaSqliteSql::Create());
GetPragmaManager().Register(PragmaSchemaView::Create());
GetPragmaManager().Register(PragmaSchemaViewFragment::Create());
}

//--------------------------------------------------------------------------------------
Expand Down
226 changes: 194 additions & 32 deletions iModelCore/ECDb/ECDb/ECSql/ECSqlPragmas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ DbResult PragmaChecksum::Read(PragmaManager::RowSet& rowSet, ECDbCR ecdb, Pragma
const Utf8String kECDbSchema = "ecdb_schema";
const Utf8String kECDbMap = "ecdb_map";
const Utf8String kSQLiteSchema = "sqlite_schema";
const Utf8String kSchemaToken = "schema_token";

if (kECDbSchema.EqualsIAscii(val.GetString())) {
Utf8String sha3;
Expand Down Expand Up @@ -87,13 +88,31 @@ DbResult PragmaChecksum::Read(PragmaManager::RowSet& rowSet, ECDbCR ecdb, Pragma
rowSet = std::move(result);
return BE_SQLITE_OK;
}
if (kSchemaToken.EqualsIAscii(val.GetString())) {
Utf8String sha3;
if (SHA3Helper::ComputeHash(sha3, ecdb, SHA3Helper::SourceType::ECDB_SCHEMA_TOKEN, "main", SHA3Helper::HashSize::SHA3_256) != BE_SQLITE_OK) {
ecdb.GetImpl().Issues().Report(
IssueSeverity::Error,
IssueCategory::BusinessProperties,
IssueType::ECSQL,
ECDbIssueId::ECDb_0593,
"Unable to compute schema token.");

rowSet = std::move(result);
return BE_SQLITE_ERROR;
}
auto row = result->AppendRow();
row.appendValue() = sha3;
rowSet = std::move(result);
return BE_SQLITE_OK;
}

ecdb.GetImpl().Issues().ReportV(
IssueSeverity::Error,
IssueCategory::BusinessProperties,
IssueType::ECSQL,
ECDbIssueId::ECDb_0596,
"Unable checksum val '%s'. Valid values are ecdb_schema|ecdb_map|sqlite_schema", val.GetString().c_str());
"Unable checksum val '%s'. Valid values are ecdb_schema|ecdb_map|sqlite_schema|schema_token", val.GetString().c_str());

rowSet = std::move(result);
return BE_SQLITE_ERROR;
Expand Down Expand Up @@ -403,6 +422,8 @@ DbResult SHA3Helper::ComputeHash(Utf8StringR hash, DbCR db, SourceType type, Utf
}, dbAlias, hashSize, skipTableThatDoesNotExists);
} else if (type == SourceType::SQLITE_SCHEMA) {
rc = ComputeSQLiteSchemaHash(hash, db, dbAlias, hashSize);
} else if (type == SourceType::ECDB_SCHEMA_TOKEN) {
rc = ComputeSchemaTokenHash(hash, db, dbAlias, hashSize);
}
if (rc != BE_SQLITE_OK) {
return rc;
Expand All @@ -412,6 +433,34 @@ DbResult SHA3Helper::ComputeHash(Utf8StringR hash, DbCR db, SourceType type, Utf
return rc;
}

//---------------------------------------------------------------------------------------
// Cheap schema-identity hash: hashes only the name and version of every schema (one row per
// schema in ec_Schema), NOT their contents. Backs PRAGMA checksum(schema_token) and the schemaToken
// column of schema_view / schema_view_fragment. Ordered by Name for a session-stable digest.
// Limitation: a same-version content change (which ECDb only allows for dynamic schemas) does
// not change this hash.
// Note: If we ever need to make this track content, we can add a fingerprint column to ec_Schema in a
// profile update.
// @bsimethod
//+---------------+---------------+---------------+---------------+---------------+------
DbResult SHA3Helper::ComputeSchemaTokenHash(Utf8String& hash, DbCR db, Utf8CP dbAlias, HashSize hashSize) {
Statement stmt;
auto rc = stmt.Prepare(db, SqlPrintfString(
"select hex(sha3_query(\"select Name,VersionDigit1,VersionDigit2,VersionDigit3 from [%s].ec_Schema order by Name\", %d))", dbAlias, (int)hashSize));

if (rc != BE_SQLITE_OK) {
return rc;
}

rc = stmt.Step();
if (rc != BE_SQLITE_ROW) {
return rc;
}

hash = stmt.GetValueText(0);
return BE_SQLITE_OK;
}

//---------------------------------------------------------------------------------------
// @bsimethod
//+---------------+---------------+---------------+---------------+---------------+------
Expand Down Expand Up @@ -958,6 +1007,54 @@ DbResult PragmaCheckECSqlWriteValues::Write(PragmaManager::RowSet& rowSet, ECDbC
//=======================================================================================
// PragmaSchemaView
//=======================================================================================
namespace {
// Emits the standard schema_view result rowSet (format/formatVersion/data/schemaToken) from a
// completed SchemaViewWriter blob. Reused by both PRAGMA schema_view and schema_view_fragment
DbResult BuildSchemaViewResult(PragmaManager::RowSet& rowSet, ECDbCR ecdb, uint8_t requestedVersion, bvector<Byte> const& output) {
// Profile 4.0.0.1 predates the EC3.2 Units/Formats migration (introduced in 4.0.0.2, ~2018).
// The writer queries no tables/columns added in 4.0.0.2+, so the pragma succeeds, but
// KindOfQuantity persistence/presentation strings are still in legacy FUS format rather
// than the alias-qualified EC3.2 form.
if (ecdb.GetECDbProfileVersion() < ProfileVersion(4, 0, 0, 2)) {
ECDbLogger::Get().warningv(
"PRAGMA schema_view: ECDb profile %s predates the EC3.2 Units/Formats migration. "
"KindOfQuantity persistence and presentation strings use the legacy FUS format. "
"Upgrade the ECDb profile to 4.0.0.2 or later for EC3.2-formatted strings.",
ecdb.GetECDbProfileVersion().ToString().c_str());
}

auto result = std::make_unique<StaticPragmaResult>(ecdb);
result->AppendProperty("format", PRIMITIVETYPE_String);
result->AppendProperty("formatVersion", PRIMITIVETYPE_Integer);
result->AppendProperty("data", PRIMITIVETYPE_String);
result->AppendProperty("schemaToken", PRIMITIVETYPE_String);
result->FreezeSchemaChanges();

// schemaToken is the cheap schema-identity hash (ec_Schema names + versions), identical to
// PRAGMA checksum(schema_token), so a fragment and the manifest it was planned from share one key.
Utf8String schemaToken;
if (SHA3Helper::ComputeHash(schemaToken, ecdb, SHA3Helper::SourceType::ECDB_SCHEMA_TOKEN, "main", SHA3Helper::HashSize::SHA3_256) != BE_SQLITE_OK) {
ecdb.GetImpl().Issues().Report(
IssueSeverity::Error,
IssueCategory::BusinessProperties,
IssueType::ECSQL,
ECDbIssueId::ECDb_0593,
"Unable to compute schema token.");
rowSet = std::make_unique<StaticPragmaResult>(ecdb);
rowSet->FreezeSchemaChanges();
return BE_SQLITE_ERROR;
}
auto row = result->AppendRow();
row.appendValue() = "binary";
row.appendValue() = (int64_t)requestedVersion;
row.appendValue().SetBinary(output.data(), output.size());
row.appendValue() = schemaToken.c_str();

rowSet = std::move(result);
return BE_SQLITE_OK;
}
}

//---------------------------------------------------------------------------------------
// @bsimethod
//---------------------------------------------------------------------------------------
Expand All @@ -982,52 +1079,117 @@ DbResult PragmaSchemaView::Read(PragmaManager::RowSet& rowSet, ECDbCR ecdb, Prag
return BE_SQLITE_ERROR;
}

auto result = std::make_unique<StaticPragmaResult>(ecdb);
result->AppendProperty("format", PRIMITIVETYPE_String);
result->AppendProperty("formatVersion", PRIMITIVETYPE_Integer);
result->AppendProperty("data", PRIMITIVETYPE_String);
result->AppendProperty("schemaToken", PRIMITIVETYPE_String);
result->FreezeSchemaChanges();

// Profile 4.0.0.1 predates the EC3.2 Units/Formats migration (introduced in 4.0.0.2, ~2018).
// The writer queries no tables/columns added in 4.0.0.2+, so the pragma succeeds, but
// KindOfQuantity persistence/presentation strings are still in legacy FUS format rather
// than the alias-qualified EC3.2 form. See SchemaViewBinaryFormat.md "ECDb Profile Compatibility".
if (ecdb.GetECDbProfileVersion() < ProfileVersion(4, 0, 0, 2)) {
ECDbLogger::Get().warningv(
"PRAGMA schema_view: ECDb profile %s predates the EC3.2 Units/Formats migration. "
"KindOfQuantity persistence and presentation strings will be returned in legacy FUS format. "
"Upgrade the ECDb profile to 4.0.0.2 or later for EC3.2-formatted strings.",
ecdb.GetECDbProfileVersion().ToString().c_str());
}

// Build the binary blob (v1: property definition dedup).
// Currently only one format version exists. When adding v2+, this must route to
// the appropriate writer based on requestedVersion instead of always using v1.
SchemaViewWriter writer;
auto writeResult = writer.WriteAllSchemas(ecdb);
if (writeResult != BE_SQLITE_OK)
return writeResult;
auto const& output = writer.GetOutput();

// Compute schema token for cache invalidation
Utf8String schemaToken;
SHA3Helper::ComputeHash(schemaToken, ecdb, SHA3Helper::SourceType::ECDB_SCHEMA, "main", SHA3Helper::HashSize::SHA3_256);
return BuildSchemaViewResult(rowSet, ecdb, requestedVersion, writer.GetOutput());
}

auto row = result->AppendRow();
row.appendValue() = "binary";
row.appendValue() = (int64_t)requestedVersion;
row.appendValue().SetBinary(output.data(), output.size());
row.appendValue() = schemaToken.c_str();
//---------------------------------------------------------------------------------------
// @bsimethod
//---------------------------------------------------------------------------------------
DbResult PragmaSchemaView::Write(PragmaManager::RowSet& rowSet, ECDbCR ecdb, PragmaVal const&, PragmaManager::OptionsMap const& options) {
ecdb.GetImpl().Issues().ReportV(IssueSeverity::Error, IssueCategory::BusinessProperties, IssueType::ECSQL, ECDbIssueId::ECDb_0552, "PRAGMA %s is readonly.", GetName().c_str());
rowSet = std::make_unique<StaticPragmaResult>(ecdb);
rowSet->FreezeSchemaChanges();
return BE_SQLITE_READONLY;
}

rowSet = std::move(result);
return BE_SQLITE_OK;
//=======================================================================================
// PragmaSchemaViewFragment
//=======================================================================================
namespace {
// True only for a non-empty run of ASCII decimal digits.
bool IsAllDigits(Utf8StringCR s) {
if (s.empty())
return false;
for (char c : s) {
if (c < '0' || c > '9')
return false;
}
return true;
}
}

//---------------------------------------------------------------------------------------
// @bsimethod
//---------------------------------------------------------------------------------------
DbResult PragmaSchemaView::Write(PragmaManager::RowSet& rowSet, ECDbCR ecdb, PragmaVal const&, PragmaManager::OptionsMap const& options) {
DbResult PragmaSchemaViewFragment::Read(PragmaManager::RowSet& rowSet, ECDbCR ecdb, PragmaVal const& val, PragmaManager::OptionsMap const& options) {
auto reportError = [&](Utf8StringCR message) -> DbResult {
ecdb.GetImpl().Issues().ReportV(IssueSeverity::Error, IssueCategory::BusinessProperties, IssueType::ECSQL, ECDbIssueId::ECDb_0601,
"PRAGMA %s: %s", GetName().c_str(), message.c_str());
rowSet = std::make_unique<StaticPragmaResult>(ecdb);
rowSet->FreezeSchemaChanges();
return BE_SQLITE_ERROR;
};

if (!val.IsString())
return reportError("expects a single string argument: a comma-separated list of schema names, optionally prefixed with a 'v<N>;' format-version token.");

Utf8String arg(val.GetString().c_str());

// Optional leading 'v<N>;' format-version token. It rides inside the single string argument
// because the pragma grammar allows only one. Schema names are ECNames, so a ';' always means a
// version prefix.
uint8_t requestedVersion = CURRENT_FORMAT_VERSION;
Utf8String nameList = arg;
size_t const semicolon = arg.find(';');
if (semicolon != Utf8String::npos) {
Utf8String versionToken = arg.substr(0, semicolon);
nameList = arg.substr(semicolon + 1);
if (versionToken.size() < 2 || (versionToken[0] != 'v' && versionToken[0] != 'V'))
return reportError("malformed version prefix; expected 'v<N>;' (for example 'v1;').");
Utf8String digits = versionToken.substr(1);
if (!IsAllDigits(digits))
return reportError("malformed version prefix; expected 'v<N>;' with N a positive integer.");
int64_t v = (int64_t)strtoll(digits.c_str(), nullptr, 10);
if (v < 1 || v > CURRENT_FORMAT_VERSION)
return reportError(Utf8PrintfString("unsupported format version %" PRId64 ". Supported versions: 1-%d.", v, (int)CURRENT_FORMAT_VERSION));
requestedVersion = (uint8_t)v;
}

// Resolve each name in the comma-separated list to its ec_Schema id. Names are ECNames, so ','
// can never occur in one. A malformed or unknown name fails the pragma - no partial fragment.
// Duplicate names are de-duplicated rather than rejected.
bvector<Utf8String> tokens;
BeStringUtilities::Split(nameList.c_str(), ",", tokens);
std::unordered_set<int64_t> schemaIds;
Statement nameStmt;
if (BE_SQLITE_OK != nameStmt.Prepare(ecdb, "SELECT Id FROM [main].[ec_Schema] WHERE Name=? COLLATE NOCASE"))
return reportError("failed to prepare the schema name lookup.");
for (Utf8StringR token : tokens) {
token.Trim();
if (!ECN::ECNameValidation::IsValidName(token.c_str()))
return reportError(Utf8PrintfString("invalid schema name '%s'; names must be valid ECNames.", token.c_str()));
nameStmt.Reset();
nameStmt.ClearBindings();
nameStmt.BindText(1, token.c_str(), Statement::MakeCopy::Yes);
if (BE_SQLITE_ROW != nameStmt.Step())
return reportError(Utf8PrintfString("schema '%s' does not exist in this connection.", token.c_str()));
schemaIds.insert(nameStmt.GetValueInt64(0));
}
if (schemaIds.empty())
return reportError("the schema name list is empty.");

SchemaViewWriter writer;
DbResult writeResult = writer.WriteSchemas(ecdb, schemaIds);
if (writeResult == BE_SQLITE_NOTFOUND) // cannot happen (ids were just resolved), kept as defense in depth
return reportError("one or more requested schemas do not exist in this connection.");
if (writeResult != BE_SQLITE_OK)
return writeResult;

return BuildSchemaViewResult(rowSet, ecdb, requestedVersion, writer.GetOutput());
}

//---------------------------------------------------------------------------------------
// @bsimethod
//---------------------------------------------------------------------------------------
DbResult PragmaSchemaViewFragment::Write(PragmaManager::RowSet& rowSet, ECDbCR ecdb, PragmaVal const&, PragmaManager::OptionsMap const& options) {
ecdb.GetImpl().Issues().ReportV(IssueSeverity::Error, IssueCategory::BusinessProperties, IssueType::ECSQL, ECDbIssueId::ECDb_0552, "PRAGMA %s is readonly.", GetName().c_str());
rowSet = std::make_unique<StaticPragmaResult>(ecdb);
rowSet->FreezeSchemaChanges();
Expand Down
25 changes: 24 additions & 1 deletion iModelCore/ECDb/ECDb/ECSql/ECSqlPragmas.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ struct PragmaSqliteSql : PragmaManager::GlobalHandler {
// @bsiclass PragmaChecksum
//+===============+===============+===============+===============+===============+======
struct PragmaChecksum : PragmaManager::GlobalHandler {
PragmaChecksum():GlobalHandler("checksum", "checksum([ec_schema|ec_map|db_schema]) return sha1 checksum for data."){}
PragmaChecksum():GlobalHandler("checksum", "checksum([ecdb_schema|ecdb_map|sqlite_schema|schema_token]) return sha3 checksum for data. 'schema_token' is a cheap schema-identity hash (names+versions only) for SchemaView cache invalidation."){}
~PragmaChecksum(){}
virtual DbResult Read(PragmaManager::RowSet&, ECDbCR, PragmaVal const&, PragmaManager::OptionsMap const&) override;
virtual DbResult Write(PragmaManager::RowSet&, ECDbCR, PragmaVal const&, PragmaManager::OptionsMap const&) override;
Expand Down Expand Up @@ -172,6 +172,27 @@ struct PragmaSchemaView : PragmaManager::GlobalHandler {
static std::unique_ptr<PragmaManager::Handler> Create () { return std::make_unique<PragmaSchemaView>(); }
};

//=======================================================================================
// @bsiclass PragmaSchemaViewFragment
// Returns a chosen subset of schemas as a single binary blob, in the same format as
// schema_view, for incremental SchemaView loading.
// The single string argument is a comma-separated list of schema names, optionally
// prefixed with a 'v<N>;' format-version token. Names are ECNames, so ',' and ';' can never
// occur in one. The name list must be dependency-closed (the caller computes and includes
// the references; the pragma does not expand references).
// Usage: PRAGMA schema_view_fragment('BisCore,Generic') -- latest format version
// PRAGMA schema_view_fragment('v1;BisCore,Generic') -- explicit format version 1
// Result: single row, same columns as schema_view: format, formatVersion, data, schemaToken.
//+===============+===============+===============+===============+===============+======
struct PragmaSchemaViewFragment : PragmaManager::GlobalHandler {
static constexpr uint8_t CURRENT_FORMAT_VERSION = 1;
PragmaSchemaViewFragment():GlobalHandler("schema_view_fragment", "schema_view_fragment('[v<N>;]name,name,...') - returns the given subset of schemas as a base64-encoded binary blob string for incremental loading. Optional leading 'v<N>;' selects the format version (default: latest)."){}
~PragmaSchemaViewFragment(){}
virtual DbResult Read(PragmaManager::RowSet&, ECDbCR, PragmaVal const&, PragmaManager::OptionsMap const&) override;
virtual DbResult Write(PragmaManager::RowSet&, ECDbCR, PragmaVal const&, PragmaManager::OptionsMap const&) override;
static std::unique_ptr<PragmaManager::Handler> Create () { return std::make_unique<PragmaSchemaViewFragment>(); }
};

//=======================================================================================
// @bsiclass
//+===============+===============+===============+===============+===============+======
Expand All @@ -186,13 +207,15 @@ struct SHA3Helper final {
ECDB_SCHEMA,
ECDB_MAP,
SQLITE_SCHEMA,
ECDB_SCHEMA_TOKEN, // schema identity only (ec_Schema name + version), cheap
};


private:
static bool TableExists(DbCR, Utf8CP, Utf8CP);
static DbResult ComputeHash(Utf8String&, DbCR, std::vector<std::string> const &, Utf8CP, HashSize, bool);
static DbResult ComputeSQLiteSchemaHash(Utf8String&, DbCR, Utf8CP, HashSize);
static DbResult ComputeSchemaTokenHash(Utf8String&, DbCR, Utf8CP, HashSize);
public:
static DbResult ComputeHash(Utf8StringR, DbCR, SourceType, Utf8CP dbAlias = "main", HashSize hashSize = HashSize::SHA3_256);
};
Expand Down
Loading