DependenciesTable.cpp (24284B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "DependenciesTable.h" 5 #include <winget/SQLiteStatementBuilder.h> 6 #include "winget\DependenciesGraph.h" 7 #include "Microsoft/Schema/1_0/OneToOneTable.h" 8 #include "Microsoft/Schema/1_0/IdTable.h" 9 #include "Microsoft/Schema/1_0/ManifestTable.h" 10 #include "Microsoft/Schema/1_0/VersionTable.h" 11 #include "Microsoft/Schema/1_0/Interface.h" 12 #include "Microsoft/Schema/1_0/ChannelTable.h" 13 14 namespace AppInstaller::Repository::Microsoft::Schema::V1_4 15 { 16 using namespace AppInstaller; 17 using namespace std::string_view_literals; 18 using namespace SQLite::Builder; 19 using namespace Schema::V1_0; 20 using QCol = SQLite::Builder::QualifiedColumn; 21 22 static constexpr std::string_view s_DependenciesTable_Table_Name = "dependencies"sv; 23 static constexpr std::string_view s_DependenciesTable_Index_Name = "dependencies_pkindex"sv; 24 static constexpr std::string_view s_DependenciesTable_Manifest_Column_Name = "manifest"sv; 25 static constexpr std::string_view s_DependenciesTable_MinVersion_Column_Name = "min_version"sv; 26 static constexpr std::string_view s_DependenciesTable_PackageId_Column_Name = "package_id"; 27 28 namespace 29 { 30 struct DependencyTableRow 31 { 32 SQLite::rowid_t m_packageRowId; 33 SQLite::rowid_t m_manifestRowId; 34 35 // Ideally this should be version row id, the version string is more needed than row id, 36 // this prevents converting back and forth between version row id and version string. 37 std::optional<Utility::NormalizedString> m_version; 38 39 bool operator <(const DependencyTableRow& rhs) const 40 { 41 auto lhsVersion = m_version.has_value() ? m_version.value() : ""; 42 auto rhsVersion = rhs.m_version.has_value() ? rhs.m_version.value() : ""; 43 return std::tie(m_packageRowId, m_manifestRowId, lhsVersion) < std::tie(rhs.m_packageRowId, rhs.m_manifestRowId, rhsVersion); 44 } 45 }; 46 47 void ThrowOnMissingPackageNodes(std::vector<Manifest::Dependency>& missingPackageNodes) 48 { 49 if (!missingPackageNodes.empty()) 50 { 51 std::string missingPackages{ missingPackageNodes.begin()->Id()}; 52 std::for_each( 53 missingPackageNodes.begin() + 1, 54 missingPackageNodes.end(), 55 [&](auto& dep) { missingPackages.append(", " + dep.Id()); }); 56 THROW_HR_MSG(APPINSTALLER_CLI_ERROR_MISSING_PACKAGE, "Missing packages: %hs", missingPackages.c_str()); 57 } 58 } 59 60 std::set<DependencyTableRow> GetAndLinkDependencies( 61 SQLite::Connection& connection, 62 const Manifest::Manifest& manifest, 63 SQLite::rowid_t manifestRowId, 64 Manifest::DependencyType dependencyType) 65 { 66 std::set<DependencyTableRow> dependencies; 67 std::vector<Manifest::Dependency> missingPackageNodes; 68 69 for (const auto& installer : manifest.Installers) 70 { 71 installer.Dependencies.ApplyToType(dependencyType, [&](Manifest::Dependency dependency) 72 { 73 auto packageRowId = IdTable::SelectIdByValue(connection, dependency.Id(), true); 74 std::optional<Utility::NormalizedString> version; 75 76 if (!packageRowId.has_value()) 77 { 78 missingPackageNodes.emplace_back(dependency); 79 return; 80 } 81 82 if (dependency.MinVersion.has_value()) 83 { 84 version = dependency.MinVersion.value().ToString(); 85 } 86 87 dependencies.emplace(DependencyTableRow{ packageRowId.value(), manifestRowId, version }); 88 }); 89 } 90 91 ThrowOnMissingPackageNodes(missingPackageNodes); 92 93 return dependencies; 94 } 95 96 bool RemoveDependenciesByRowIds(SQLite::Connection& connection, std::vector<DependencyTableRow> dependencyTableRows) 97 { 98 using namespace SQLite::Builder; 99 bool tableUpdated = false; 100 if (dependencyTableRows.empty()) 101 { 102 return tableUpdated; 103 } 104 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, std::string{ s_DependenciesTable_Table_Name } + "remove_dependencies_by_rowid"); 105 106 SQLite::Builder::StatementBuilder builder; 107 builder 108 .DeleteFrom(s_DependenciesTable_Table_Name) 109 .Where(s_DependenciesTable_PackageId_Column_Name).Equals(Unbound) 110 .And(s_DependenciesTable_Manifest_Column_Name).Equals(Unbound); 111 112 113 SQLite::Statement deleteStmt = builder.Prepare(connection); 114 for (auto row : dependencyTableRows) 115 { 116 deleteStmt.Reset(); 117 deleteStmt.Bind(1, row.m_packageRowId); 118 deleteStmt.Bind(2, row.m_manifestRowId); 119 deleteStmt.Execute(); 120 tableUpdated = true; 121 } 122 123 savepoint.Commit(); 124 return tableUpdated; 125 } 126 127 bool InsertManifestDependencies( 128 SQLite::Connection& connection, 129 std::set<DependencyTableRow>& dependenciesTableRows) 130 { 131 using namespace SQLite::Builder; 132 using namespace Schema::V1_0; 133 bool tableUpdated = false; 134 135 StatementBuilder insertBuilder; 136 insertBuilder.InsertInto(s_DependenciesTable_Table_Name) 137 .Columns({ s_DependenciesTable_Manifest_Column_Name, s_DependenciesTable_MinVersion_Column_Name, s_DependenciesTable_PackageId_Column_Name }) 138 .Values(Unbound, Unbound, Unbound); 139 SQLite::Statement insert = insertBuilder.Prepare(connection); 140 141 for (const auto& dep : dependenciesTableRows) 142 { 143 insert.Reset(); 144 insert.Bind(1, dep.m_manifestRowId); 145 146 if (dep.m_version.has_value()) 147 { 148 insert.Bind(2, VersionTable::EnsureExists(connection, dep.m_version.value())); 149 } 150 else 151 { 152 insert.Bind(2, nullptr); 153 } 154 155 insert.Bind(3, dep.m_packageRowId); 156 157 insert.Execute(); 158 tableUpdated = true; 159 } 160 161 return tableUpdated; 162 } 163 } 164 165 bool DependenciesTable::Exists(const SQLite::Connection& connection) 166 { 167 using namespace SQLite; 168 169 Builder::StatementBuilder builder; 170 builder.Select(Builder::RowCount).From(Builder::Schema::MainTable). 171 Where(Builder::Schema::TypeColumn).Equals(Builder::Schema::Type_Table).And(Builder::Schema::NameColumn).Equals(s_DependenciesTable_Table_Name); 172 173 Statement statement = builder.Prepare(connection); 174 THROW_HR_IF(E_UNEXPECTED, !statement.Step()); 175 return statement.GetColumn<int64_t>(0) != 0; 176 } 177 178 std::string_view DependenciesTable::TableName() 179 { 180 return s_DependenciesTable_Table_Name; 181 } 182 183 void DependenciesTable::Create(SQLite::Connection& connection) 184 { 185 using namespace SQLite::Builder; 186 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createDependencyTable_v1_4"); 187 constexpr std::string_view dependencyIndexByVersionId = "dependencies_version_id_index"; 188 constexpr std::string_view dependencyIndexByPackageId = "dependencies_package_id_index"; 189 190 StatementBuilder createTableBuilder; 191 createTableBuilder.CreateTable(TableName()).BeginColumns(); 192 createTableBuilder.Column(IntegerPrimaryKey()); 193 194 std::array<DependenciesTableColumnInfo, 2> notNullableDependenciesColumns 195 { 196 DependenciesTableColumnInfo{ s_DependenciesTable_Manifest_Column_Name }, 197 DependenciesTableColumnInfo{ s_DependenciesTable_PackageId_Column_Name } 198 }; 199 200 std::array<DependenciesTableColumnInfo, 1> nullableDependenciesColumns 201 { 202 DependenciesTableColumnInfo{ s_DependenciesTable_MinVersion_Column_Name } 203 }; 204 205 // Add dependencies column tables not null columns. 206 for (const DependenciesTableColumnInfo& value : notNullableDependenciesColumns) 207 { 208 createTableBuilder.Column(ColumnBuilder(value.Name, Type::RowId).NotNull()); 209 } 210 211 // Add dependencies column tables null columns. 212 for (const DependenciesTableColumnInfo& value : nullableDependenciesColumns) 213 { 214 createTableBuilder.Column(ColumnBuilder(value.Name, Type::RowId)); 215 } 216 217 createTableBuilder.EndColumns(); 218 219 createTableBuilder.Execute(connection); 220 221 // Primary key index by package rowid and manifest rowid. 222 StatementBuilder createPKIndexBuilder; 223 createPKIndexBuilder.CreateUniqueIndex(s_DependenciesTable_Index_Name).On(s_DependenciesTable_Table_Name).Columns({ s_DependenciesTable_Manifest_Column_Name, s_DependenciesTable_PackageId_Column_Name }); 224 createPKIndexBuilder.Execute(connection); 225 226 // Index of dependency by Manifest id. 227 StatementBuilder createIndexByManifestIdBuilder; 228 createIndexByManifestIdBuilder.CreateIndex(dependencyIndexByVersionId).On(s_DependenciesTable_Table_Name).Columns({ s_DependenciesTable_MinVersion_Column_Name }); 229 createIndexByManifestIdBuilder.Execute(connection); 230 231 // Index of dependency by package id. 232 StatementBuilder createIndexByPackageIdBuilder; 233 createIndexByPackageIdBuilder.CreateIndex(dependencyIndexByPackageId).On(s_DependenciesTable_Table_Name).Columns({ s_DependenciesTable_PackageId_Column_Name }); 234 createIndexByPackageIdBuilder.Execute(connection); 235 236 savepoint.Commit(); 237 } 238 239 void DependenciesTable::Drop(SQLite::Connection& connection) 240 { 241 SQLite::Builder::StatementBuilder dropTableBuilder; 242 dropTableBuilder.DropTableIfExists(s_DependenciesTable_Table_Name); 243 244 dropTableBuilder.Execute(connection); 245 } 246 247 void DependenciesTable::AddDependencies(SQLite::Connection& connection, const Manifest::Manifest& manifest, SQLite::rowid_t manifestRowId) 248 { 249 if (!Exists(connection)) 250 { 251 return; 252 } 253 254 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, std::string{ s_DependenciesTable_Table_Name } + "add_dependencies_v1_4"); 255 256 auto dependencies = GetAndLinkDependencies(connection, manifest, manifestRowId, Manifest::DependencyType::Package); 257 if (!dependencies.size()) 258 { 259 return; 260 } 261 262 InsertManifestDependencies(connection, dependencies); 263 264 savepoint.Commit(); 265 } 266 267 bool DependenciesTable::UpdateDependencies(SQLite::Connection& connection, const Manifest::Manifest& manifest, SQLite::rowid_t manifestRowId) 268 { 269 if (!Exists(connection)) 270 { 271 return false; 272 } 273 274 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, std::string{ s_DependenciesTable_Table_Name } + "update_dependencies_v1_4"); 275 276 const auto dependencies = GetAndLinkDependencies(connection, manifest, manifestRowId, Manifest::DependencyType::Package); 277 auto existingDependencies = GetDependenciesByManifestRowId(connection, manifestRowId); 278 279 // Get dependencies to add. 280 std::set<DependencyTableRow> toAddDependencies; 281 std::copy_if( 282 dependencies.begin(), 283 dependencies.end(), 284 std::inserter(toAddDependencies, toAddDependencies.begin()), 285 [&](DependencyTableRow dep) 286 { 287 Utility::NormalizedString version = dep.m_version.has_value() ? dep.m_version.value() : ""; 288 return existingDependencies.find(std::make_pair(dep.m_packageRowId, version)) == existingDependencies.end(); 289 } 290 ); 291 292 // Get dependencies to remove. 293 std::vector<DependencyTableRow> toRemoveDependencies; 294 std::for_each( 295 existingDependencies.begin(), 296 existingDependencies.end(), 297 [&](std::pair<SQLite::rowid_t, Utility::NormalizedString> row) 298 { 299 if (dependencies.find(DependencyTableRow{row.first, manifestRowId, row.second}) == dependencies.end()) 300 { 301 toRemoveDependencies.emplace_back(DependencyTableRow{ row.first, manifestRowId }); 302 } 303 } 304 ); 305 306 bool tableUpdated = RemoveDependenciesByRowIds(connection, toRemoveDependencies); 307 tableUpdated = InsertManifestDependencies(connection, toAddDependencies) || tableUpdated; 308 savepoint.Commit(); 309 310 return tableUpdated; 311 } 312 313 void DependenciesTable::RemoveDependencies(SQLite::Connection& connection, SQLite::rowid_t manifestRowId) 314 { 315 if (!Exists(connection)) 316 { 317 return; 318 } 319 320 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, std::string{ s_DependenciesTable_Table_Name } + "remove_dependencies_by_manifest_v1_4"); 321 322 SQLite::Builder::StatementBuilder builder; 323 builder.DeleteFrom(s_DependenciesTable_Table_Name).Where(s_DependenciesTable_Manifest_Column_Name).Equals(manifestRowId); 324 325 builder.Execute(connection); 326 savepoint.Commit(); 327 } 328 329 std::vector<std::pair<SQLite::rowid_t, Utility::NormalizedString>> DependenciesTable::GetDependentsById(const SQLite::Connection& connection, Manifest::string_t packageId) 330 { 331 constexpr std::string_view depTableAlias = "dep"; 332 constexpr std::string_view minVersionAlias = "minV"; 333 constexpr std::string_view packageIdAlias = "pId"; 334 335 336 StatementBuilder builder; 337 // Find all manifest that depend on this package. Use outer join for joining version table as min_version may be NULL. 338 // SELECT [dep].[manifest], [dep].[min_version], [pId].[id], [minV].[version] FROM [dependencies] AS [dep] 339 // LEFT OUTER JOIN [versions] AS [minV] ON [dep].[min_version] = [minV].[rowid] 340 // JOIN [ids] AS [pId] ON [pId].[rowid] = [dep].[package_id] 341 // WHERE [pId].[id] = ? 342 builder.Select() 343 .Column(QCol(depTableAlias, s_DependenciesTable_Manifest_Column_Name)) 344 .Column(QCol(depTableAlias, s_DependenciesTable_MinVersion_Column_Name)) 345 .Column(QCol(packageIdAlias, IdTable::ValueName())) 346 .Column(QCol(minVersionAlias, VersionTable::ValueName())) 347 .From({ s_DependenciesTable_Table_Name }).As(depTableAlias) 348 .LeftOuterJoin({ VersionTable::TableName() }).As(minVersionAlias) 349 .On(QCol(depTableAlias, s_DependenciesTable_MinVersion_Column_Name), QCol(minVersionAlias, SQLite::RowIDName)) 350 .Join({ IdTable::TableName() }).As(packageIdAlias) 351 .On(QCol(packageIdAlias, SQLite::RowIDName), QCol(depTableAlias, s_DependenciesTable_PackageId_Column_Name)) 352 .Where(QCol(packageIdAlias, IdTable::ValueName())).Equals(Unbound); 353 354 SQLite::Statement stmt = builder.Prepare(connection); 355 stmt.Bind(1, std::string{ packageId }); 356 357 std::vector<std::pair<SQLite::rowid_t, Utility::NormalizedString>> resultSet; 358 359 while (stmt.Step()) 360 { 361 Utility::NormalizedString version = ""; 362 if (!stmt.GetColumnIsNull(1)) 363 { 364 // If min_version is not NULL, use the corresponding value from Version table. 365 version = stmt.GetColumn<std::string>(3); 366 } 367 resultSet.emplace_back( 368 std::make_pair(stmt.GetColumn<SQLite::rowid_t>(0), std::move(version))); 369 } 370 371 return resultSet; 372 } 373 374 std::set<std::pair<SQLite::rowid_t, Utility::NormalizedString>> DependenciesTable::GetDependenciesByManifestRowId(const SQLite::Connection& connection, SQLite::rowid_t manifestRowId) 375 { 376 SQLite::Builder::StatementBuilder builder; 377 378 constexpr std::string_view depTableAlias = "dep"; 379 constexpr std::string_view minVersionAlias = "minV"; 380 381 std::set<std::pair<SQLite::rowid_t, Utility::NormalizedString>> resultSet; 382 383 // Use Outer join since min_version could have NULL value. 384 // SELECT [dep].[package_id], [dep].[min_version], [minV].[version] FROM [dependencies] AS [dep] 385 // LEFT OUTER JOIN [versions] AS [minV] ON [minV].[rowid] = [dep].[min_version] 386 // WHERE [dep].[manifest] = ? 387 builder.Select() 388 .Column(QCol(depTableAlias, s_DependenciesTable_PackageId_Column_Name)) 389 .Column(QCol(depTableAlias, s_DependenciesTable_MinVersion_Column_Name)) 390 .Column(QCol(minVersionAlias, VersionTable::ValueName())) 391 .From({ s_DependenciesTable_Table_Name }).As(depTableAlias) 392 .LeftOuterJoin({ VersionTable::TableName() }).As(minVersionAlias) 393 .On(QCol(minVersionAlias, SQLite::RowIDName), QCol(depTableAlias, s_DependenciesTable_MinVersion_Column_Name)) 394 .Where(QCol(depTableAlias, s_DependenciesTable_Manifest_Column_Name)).Equals(Unbound); 395 396 SQLite::Statement select = builder.Prepare(connection); 397 398 select.Bind(1, manifestRowId); 399 while (select.Step()) 400 { 401 Utility::NormalizedString version = ""; 402 if (!select.GetColumnIsNull(1)) 403 { 404 // If min_version is not NULL, use the corresponding value from Version table. 405 version = select.GetColumn<std::string>(2); 406 } 407 resultSet.emplace(std::make_pair(select.GetColumn<SQLite::rowid_t>(0), std::move(version))); 408 } 409 410 return resultSet; 411 } 412 413 void DependenciesTable::PrepareForPackaging(SQLite::Connection& connection) 414 { 415 SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "prepareForPacking_V1_4"); 416 417 StatementBuilder dropIndexBuilder; 418 dropIndexBuilder.DropIndex({ s_DependenciesTable_Index_Name }); 419 dropIndexBuilder.Execute(connection); 420 421 StatementBuilder dropTableBuilder; 422 dropTableBuilder.DropTable({ s_DependenciesTable_Table_Name }); 423 dropTableBuilder.Execute(connection); 424 425 savepoint.Commit(); 426 } 427 428 bool DependenciesTable::CheckConsistency(const SQLite::Connection& connection, bool log) 429 { 430 StatementBuilder builder; 431 432 if (!Exists(connection)) 433 { 434 return true; 435 } 436 437 builder.Select(QCol(s_DependenciesTable_Table_Name, SQLite::RowIDName)) 438 .From(s_DependenciesTable_Table_Name) 439 .LeftOuterJoin(IdTable::TableName()) 440 .On(QCol(s_DependenciesTable_Table_Name, s_DependenciesTable_PackageId_Column_Name), QCol(IdTable::TableName(), SQLite::RowIDName)) 441 .LeftOuterJoin(ManifestTable::TableName()) 442 .On(QCol(s_DependenciesTable_Table_Name, s_DependenciesTable_Manifest_Column_Name), QCol(ManifestTable::TableName(), SQLite::RowIDName)) 443 .LeftOuterJoin(VersionTable::TableName()) 444 .On(QCol(s_DependenciesTable_Table_Name, s_DependenciesTable_MinVersion_Column_Name), QCol(VersionTable::TableName(), SQLite::RowIDName)) 445 .Where(QCol(ManifestTable::TableName(), SQLite::RowIDName)).IsNull() 446 .Or(QCol(VersionTable::TableName(), SQLite::RowIDName)).IsNull().And(QCol(s_DependenciesTable_Table_Name, s_DependenciesTable_MinVersion_Column_Name)).IsNotNull() 447 .Or(QCol(IdTable::TableName(), SQLite::RowIDName)).IsNull(); 448 449 SQLite::Statement select = builder.Prepare(connection); 450 451 bool result = true; 452 453 while (select.Step()) 454 { 455 result = false; 456 457 if (!log) 458 { 459 break; 460 } 461 462 AICLI_LOG(Repo, Info, << " [INVALID] row in [" << s_DependenciesTable_Table_Name << "] rowid [" << select.GetColumn<SQLite::rowid_t>(0) << "]"); 463 } 464 465 return result; 466 } 467 468 bool DependenciesTable::IsValueReferenced(const SQLite::Connection& connection, std::string_view tableName, SQLite::rowid_t valueRowId) 469 { 470 if (!Exists(connection)) 471 { 472 return false; 473 } 474 475 StatementBuilder builder; 476 477 if (tableName != V1_0::VersionTable::TableName()) 478 { 479 return false; 480 } 481 482 std::array<std::string_view, 1> columns = { s_DependenciesTable_MinVersion_Column_Name }; 483 bool referenced = false; 484 485 for(auto column: columns) 486 { 487 builder.Select(SQLite::RowIDName).From(s_DependenciesTable_Table_Name).Where(column).Equals(Unbound).Limit(1); 488 489 SQLite::Statement select = builder.Prepare(connection); 490 491 select.Bind(1, valueRowId); 492 if (select.Step()) 493 { 494 referenced = true; 495 break; 496 } 497 } 498 499 return referenced; 500 } 501 502 std::vector<SQLite::rowid_t> DependenciesTable::GetDependenciesMinVersionsRowIdByManifestId(const SQLite::Connection& connection, SQLite::rowid_t manifestRowId) 503 { 504 if (!Exists(connection)) 505 { 506 return {}; 507 } 508 509 StatementBuilder builder; 510 511 std::vector<SQLite::rowid_t> result; 512 513 // Find all versions for manifest row. 514 // SELECT [min_version] FROM [dependencies] 515 // WHERE [manifest] = ? 516 builder.Select() 517 .Column(s_DependenciesTable_MinVersion_Column_Name) 518 .From({ s_DependenciesTable_Table_Name }) 519 .Where(s_DependenciesTable_Manifest_Column_Name).Equals(Unbound); 520 521 auto select = builder.Prepare(connection); 522 523 select.Bind(1, manifestRowId); 524 525 while (select.Step()) 526 { 527 if (!select.GetColumnIsNull(0)) 528 { 529 result.emplace_back(select.GetColumn<SQLite::rowid_t>(0)); 530 } 531 } 532 533 return result; 534 } 535 536 std::vector<std::pair<SQLite::rowid_t, Utility::NormalizedString>> DependenciesTable::GetAllDependenciesWithMinVersions(const SQLite::Connection& connection) 537 { 538 if (!Exists(connection)) 539 { 540 return {}; 541 } 542 543 std::vector<std::pair<SQLite::rowid_t, Utility::NormalizedString>> result; 544 545 constexpr std::string_view depTableAlias = "dep"; 546 constexpr std::string_view minVersionAlias = "minV"; 547 548 StatementBuilder builder; 549 550 // SELECT [dep].[package_id], [minV].[version] FROM [dependencies] AS [dep] 551 // JOIN [versions] AS [minV] ON [minV].[rowid] = [dep].[min_version] 552 builder.Select() 553 .Column(QCol(depTableAlias, s_DependenciesTable_PackageId_Column_Name)) 554 .Column(QCol(minVersionAlias, VersionTable::ValueName())) 555 .From({ s_DependenciesTable_Table_Name }).As(depTableAlias) 556 .Join({ VersionTable::TableName() }).As(minVersionAlias) 557 .On(QCol(minVersionAlias, SQLite::RowIDName), QCol(depTableAlias, s_DependenciesTable_MinVersion_Column_Name)); 558 559 SQLite::Statement select = builder.Prepare(connection); 560 561 while (select.Step()) 562 { 563 Utility::NormalizedString version = ""; 564 if (!select.GetColumnIsNull(1)) 565 { 566 version = select.GetColumn<std::string>(1); 567 } 568 569 if (!version.empty()) 570 { 571 result.emplace_back(std::make_pair(select.GetColumn<SQLite::rowid_t>(0), version)); 572 } 573 } 574 575 return result; 576 } 577 }