blob: e17e388f7d566fd0066ac0f22abdb97b358abaed [file]
// Copyright (C) 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "icing/schema/schema-store.h"
#include <algorithm>
#include <cinttypes>
#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "icing/text_classifier/lib3/utils/base/status.h"
#include "icing/text_classifier/lib3/utils/base/statusor.h"
#include "icing/absl_ports/canonical_errors.h"
#include "icing/absl_ports/str_cat.h"
#include "icing/file/destructible-directory.h"
#include "icing/file/file-backed-proto.h"
#include "icing/file/filesystem.h"
#include "icing/file/version-util.h"
#include "icing/proto/debug.pb.h"
#include "icing/proto/document.pb.h"
#include "icing/proto/logging.pb.h"
#include "icing/proto/schema.pb.h"
#include "icing/proto/search.pb.h"
#include "icing/proto/storage.pb.h"
#include "icing/schema/backup-schema-producer.h"
#include "icing/schema/joinable-property.h"
#include "icing/schema/property-util.h"
#include "icing/schema/schema-type-manager.h"
#include "icing/schema/schema-util.h"
#include "icing/schema/section.h"
#include "icing/store/document-filter-data.h"
#include "icing/store/dynamic-trie-key-mapper.h"
#include "icing/util/crc32.h"
#include "icing/util/logging.h"
#include "icing/util/status-macros.h"
namespace icing {
namespace lib {
namespace {
constexpr char kSchemaStoreHeaderFilename[] = "schema_store_header";
constexpr char kSchemaFilename[] = "schema.pb";
constexpr char kOverlaySchemaFilename[] = "overlay_schema.pb";
constexpr char kSchemaTypeMapperFilename[] = "schema_type_mapper";
// A DynamicTrieKeyMapper stores its data across 3 arrays internally. Giving
// each array 128KiB for storage means the entire DynamicTrieKeyMapper requires
// 384KiB.
constexpr int32_t kSchemaTypeMapperMaxSize = 3 * 128 * 1024; // 384 KiB
std::string MakeHeaderFilename(const std::string& base_dir) {
return absl_ports::StrCat(base_dir, "/", kSchemaStoreHeaderFilename);
}
std::string MakeSchemaFilename(const std::string& base_dir) {
return absl_ports::StrCat(base_dir, "/", kSchemaFilename);
}
std::string MakeOverlaySchemaFilename(const std::string& base_dir) {
return absl_ports::StrCat(base_dir, "/", kOverlaySchemaFilename);
}
std::string MakeSchemaTypeMapperFilename(const std::string& base_dir) {
return absl_ports::StrCat(base_dir, "/", kSchemaTypeMapperFilename);
}
// Assuming that SchemaTypeIds are assigned to schema types based on their order
// in the SchemaProto. Check if the schema type->SchemaTypeId mapping would
// change with the new schema.
std::unordered_set<SchemaTypeId> SchemaTypeIdsChanged(
const SchemaProto& old_schema, const SchemaProto& new_schema) {
std::unordered_set<SchemaTypeId> old_schema_type_ids_changed;
std::unordered_map<std::string, int> old_types_and_index;
for (int i = 0; i < old_schema.types_size(); ++i) {
old_types_and_index.emplace(old_schema.types(i).schema_type(), i);
}
std::unordered_map<std::string, int> new_types_and_index;
for (int i = 0; i < new_schema.types_size(); ++i) {
new_types_and_index.emplace(new_schema.types(i).schema_type(), i);
}
for (const auto& old_type_index : old_types_and_index) {
const auto& iter = new_types_and_index.find(old_type_index.first);
// We only care if the type exists in both the old and new schema. If the
// type has been deleted, then it'll be captured in
// SetSchemaResult.schema_types_deleted*. If the type has been added in the
// new schema then we also don't care because nothing needs to be updated.
if (iter != new_types_and_index.end()) {
// Since the SchemaTypeId of the schema type is just the index of it in
// the SchemaProto, compare the index and save it if it's not the same
if (old_type_index.second != iter->second) {
old_schema_type_ids_changed.emplace(old_type_index.second);
}
}
}
return old_schema_type_ids_changed;
}
} // namespace
/* static */ libtextclassifier3::StatusOr<SchemaStore::Header>
SchemaStore::Header::Read(const Filesystem* filesystem,
const std::string& path) {
Header header;
ScopedFd sfd(filesystem->OpenForRead(path.c_str()));
if (!sfd.is_valid()) {
return absl_ports::NotFoundError("SchemaStore header doesn't exist");
}
// If file is sizeof(LegacyHeader), then it must be LegacyHeader.
int64_t file_size = filesystem->GetFileSize(sfd.get());
if (file_size == sizeof(LegacyHeader)) {
LegacyHeader legacy_header;
if (!filesystem->Read(path.c_str(), &legacy_header,
sizeof(legacy_header))) {
return absl_ports::InternalError(
absl_ports::StrCat("Couldn't read: ", path));
}
if (legacy_header.magic != Header::kMagic) {
return absl_ports::InternalError(
absl_ports::StrCat("Invalid header kMagic for file: ", path));
}
header.set_checksum(legacy_header.checksum);
} else if (file_size == sizeof(Header)) {
if (!filesystem->Read(path.c_str(), &header, sizeof(header))) {
return absl_ports::InternalError(
absl_ports::StrCat("Couldn't read: ", path));
}
if (header.magic() != Header::kMagic) {
return absl_ports::InternalError(
absl_ports::StrCat("Invalid header kMagic for file: ", path));
}
} else {
int legacy_header_size = sizeof(LegacyHeader);
int header_size = sizeof(Header);
return absl_ports::InternalError(IcingStringUtil::StringPrintf(
"Unexpected header size %" PRId64 ". Expected %d or %d", file_size,
legacy_header_size, header_size));
}
return header;
}
libtextclassifier3::Status SchemaStore::Header::Write(
const Filesystem* filesystem, const std::string& path) {
ScopedFd scoped_fd(filesystem->OpenForWrite(path.c_str()));
// This should overwrite the header.
if (!scoped_fd.is_valid() ||
!filesystem->Write(scoped_fd.get(), this, sizeof(*this)) ||
!filesystem->DataSync(scoped_fd.get())) {
return absl_ports::InternalError(
absl_ports::StrCat("Failed to write SchemaStore header: ", path));
}
return libtextclassifier3::Status::OK;
}
libtextclassifier3::StatusOr<std::unique_ptr<SchemaStore>> SchemaStore::Create(
const Filesystem* filesystem, const std::string& base_dir,
const Clock* clock, InitializeStatsProto* initialize_stats) {
ICING_RETURN_ERROR_IF_NULL(filesystem);
ICING_RETURN_ERROR_IF_NULL(clock);
if (!filesystem->DirectoryExists(base_dir.c_str())) {
return absl_ports::FailedPreconditionError(
"Schema store base directory does not exist!");
}
std::unique_ptr<SchemaStore> schema_store = std::unique_ptr<SchemaStore>(
new SchemaStore(filesystem, base_dir, clock));
ICING_RETURN_IF_ERROR(schema_store->Initialize(initialize_stats));
return schema_store;
}
libtextclassifier3::StatusOr<std::unique_ptr<SchemaStore>> SchemaStore::Create(
const Filesystem* filesystem, const std::string& base_dir,
const Clock* clock, SchemaProto schema) {
ICING_RETURN_ERROR_IF_NULL(filesystem);
ICING_RETURN_ERROR_IF_NULL(clock);
if (!filesystem->DirectoryExists(base_dir.c_str())) {
return absl_ports::FailedPreconditionError(
"Schema store base directory does not exist!");
}
std::unique_ptr<SchemaStore> schema_store = std::unique_ptr<SchemaStore>(
new SchemaStore(filesystem, base_dir, clock));
ICING_RETURN_IF_ERROR(schema_store->Initialize(std::move(schema)));
return schema_store;
}
/* static */ libtextclassifier3::Status SchemaStore::DiscardOverlaySchema(
const Filesystem* filesystem, const std::string& base_dir, Header& header) {
std::string header_filename = MakeHeaderFilename(base_dir);
if (header.overlay_created()) {
header.SetOverlayInfo(
/*overlay_created=*/false,
/*min_overlay_version_compatibility=*/ std::numeric_limits<
int32_t>::max());
ICING_RETURN_IF_ERROR(header.Write(filesystem, header_filename));
}
std::string schema_overlay_filename = MakeOverlaySchemaFilename(base_dir);
if (!filesystem->DeleteFile(schema_overlay_filename.c_str())) {
return absl_ports::InternalError(
"Unable to delete stale schema overlay file.");
}
return libtextclassifier3::Status::OK;
}
/* static */ libtextclassifier3::Status SchemaStore::MigrateSchema(
const Filesystem* filesystem, const std::string& base_dir,
version_util::StateChange version_state_change, int32_t new_version) {
if (!filesystem->DirectoryExists(base_dir.c_str())) {
// Situations when schema store directory doesn't exist:
// - Initializing new Icing instance: don't have to do anything now. The
// directory will be created later.
// - Lose schema store: there is nothing we can do now. The logic will be
// handled later by initializing.
//
// Therefore, just simply return OK here.
return libtextclassifier3::Status::OK;
}
std::string overlay_schema_filename = MakeOverlaySchemaFilename(base_dir);
if (!filesystem->FileExists(overlay_schema_filename.c_str())) {
// The overlay doesn't exist. So there should be nothing particularly
// interesting to worry about.
return libtextclassifier3::Status::OK;
}
std::string header_filename = MakeHeaderFilename(base_dir);
libtextclassifier3::StatusOr<Header> header_or;
switch (version_state_change) {
// No necessary actions for normal upgrades or no version change. The data
// that was produced by the previous version is fully compatible with this
// version and there's no stale data for us to clean up.
// The same is true for a normal rollforward. A normal rollforward implies
// that the previous version was one that understood the concept of the
// overlay schema and would have already discarded it if it was unusable.
case version_util::StateChange::kVersionZeroUpgrade:
// fallthrough
case version_util::StateChange::kUpgrade:
// fallthrough
case version_util::StateChange::kRollForward:
// fallthrough
case version_util::StateChange::kCompatible:
return libtextclassifier3::Status::OK;
case version_util::StateChange::kVersionZeroRollForward:
// We've rolled forward. The schema overlay file, if it exists, is
// possibly stale. We must throw it out.
header_or = Header::Read(filesystem, header_filename);
if (!header_or.ok()) {
return header_or.status();
}
return SchemaStore::DiscardOverlaySchema(filesystem, base_dir,
header_or.ValueOrDie());
case version_util::StateChange::kRollBack:
header_or = Header::Read(filesystem, header_filename);
if (!header_or.ok()) {
return header_or.status();
}
if (header_or.ValueOrDie().min_overlay_version_compatibility() <=
new_version) {
// We've been rolled back, but the overlay schema claims that it
// supports this version. So we can safely return.
return libtextclassifier3::Status::OK;
}
// We've been rolled back to a version that the overlay schema doesn't
// support. We must throw it out.
return SchemaStore::DiscardOverlaySchema(filesystem, base_dir,
header_or.ValueOrDie());
case version_util::StateChange::kUndetermined:
// It's not clear what version we're on, but the base schema should always
// be safe to use. Throw out the overlay.
header_or = Header::Read(filesystem, header_filename);
if (!header_or.ok()) {
return header_or.status();
}
return SchemaStore::DiscardOverlaySchema(filesystem, base_dir,
header_or.ValueOrDie());
}
return libtextclassifier3::Status::OK;
}
/* static */ libtextclassifier3::Status SchemaStore::DiscardDerivedFiles(
const Filesystem* filesystem, const std::string& base_dir) {
// Schema type mapper
return DynamicTrieKeyMapper<SchemaTypeId>::Delete(
*filesystem, MakeSchemaTypeMapperFilename(base_dir));
}
SchemaStore::SchemaStore(const Filesystem* filesystem, std::string base_dir,
const Clock* clock)
: filesystem_(filesystem),
base_dir_(std::move(base_dir)),
clock_(clock),
schema_file_(std::make_unique<FileBackedProto<SchemaProto>>(
*filesystem, MakeSchemaFilename(base_dir_))) {}
SchemaStore::~SchemaStore() {
if (has_schema_successfully_set_ && schema_file_ != nullptr &&
schema_type_mapper_ != nullptr && schema_type_manager_ != nullptr) {
if (!PersistToDisk().ok()) {
ICING_LOG(ERROR) << "Error persisting to disk in SchemaStore destructor";
}
}
}
libtextclassifier3::Status SchemaStore::Initialize(SchemaProto new_schema) {
ICING_RETURN_IF_ERROR(LoadSchema());
if (!absl_ports::IsNotFound(GetSchema().status())) {
return absl_ports::FailedPreconditionError(
"Incorrectly tried to initialize schema store with a new schema, when "
"one is already set!");
}
ICING_RETURN_IF_ERROR(schema_file_->Write(
std::make_unique<SchemaProto>(std::move(new_schema))));
return InitializeInternal(/*create_overlay_if_necessary=*/true,
/*initialize_stats=*/nullptr);
}
libtextclassifier3::Status SchemaStore::Initialize(
InitializeStatsProto* initialize_stats) {
ICING_RETURN_IF_ERROR(LoadSchema());
auto schema_proto_or = GetSchema();
if (absl_ports::IsNotFound(schema_proto_or.status())) {
// Don't have an existing schema proto, that's fine
return libtextclassifier3::Status::OK;
} else if (!schema_proto_or.ok()) {
// Real error when trying to read the existing schema
return schema_proto_or.status();
}
return InitializeInternal(/*create_overlay_if_necessary=*/false,
initialize_stats);
}
libtextclassifier3::Status SchemaStore::LoadSchema() {
libtextclassifier3::StatusOr<Header> header_or =
Header::Read(filesystem_, MakeHeaderFilename(base_dir_));
bool header_exists = false;
if (!header_or.ok() && !absl_ports::IsNotFound(header_or.status())) {
return header_or.status();
} else if (!header_or.ok()) {
header_ = std::make_unique<Header>();
} else {
header_exists = true;
header_ = std::make_unique<Header>(std::move(header_or).ValueOrDie());
}
std::string overlay_schema_filename = MakeOverlaySchemaFilename(base_dir_);
bool overlay_schema_file_exists =
filesystem_->FileExists(overlay_schema_filename.c_str());
libtextclassifier3::Status base_schema_state = schema_file_->Read().status();
if (!base_schema_state.ok() && !absl_ports::IsNotFound(base_schema_state)) {
return base_schema_state;
}
// There are three valid cases:
// 1. Everything is missing. This is an empty schema store.
if (!base_schema_state.ok() && !overlay_schema_file_exists &&
!header_exists) {
return libtextclassifier3::Status::OK;
}
// 2. There never was a overlay schema. The header exists, the base schema
// exists and the header says the overlay schema shouldn't exist
if (base_schema_state.ok() && !overlay_schema_file_exists && header_exists &&
!header_->overlay_created()) {
// Nothing else to do. Just return safely.
return libtextclassifier3::Status::OK;
}
// 3. There is an overlay schema and a base schema and a header. The header
// says that the overlay schema should exist.
if (base_schema_state.ok() && overlay_schema_file_exists && header_exists &&
header_->overlay_created()) {
overlay_schema_file_ = std::make_unique<FileBackedProto<SchemaProto>>(
*filesystem_, MakeOverlaySchemaFilename(base_dir_));
return libtextclassifier3::Status::OK;
}
// Something has gone wrong. We've lost part of the schema ground truth.
// Return an error.
bool overlay_created = header_->overlay_created();
bool base_schema_exists = base_schema_state.ok();
return absl_ports::InternalError(IcingStringUtil::StringPrintf(
"Unable to properly load schema. Header {exists:%d, overlay_created:%d}, "
"base schema exists: %d, overlay_schema_exists: %d",
header_exists, overlay_created, base_schema_exists,
overlay_schema_file_exists));
}
libtextclassifier3::Status SchemaStore::InitializeInternal(
bool create_overlay_if_necessary, InitializeStatsProto* initialize_stats) {
if (!InitializeDerivedFiles().ok()) {
ICING_VLOG(3)
<< "Couldn't find derived files or failed to initialize them, "
"regenerating derived files for SchemaStore.";
std::unique_ptr<Timer> regenerate_timer = clock_->GetNewTimer();
if (initialize_stats != nullptr) {
initialize_stats->set_schema_store_recovery_cause(
InitializeStatsProto::IO_ERROR);
}
ICING_RETURN_IF_ERROR(RegenerateDerivedFiles(create_overlay_if_necessary));
if (initialize_stats != nullptr) {
initialize_stats->set_schema_store_recovery_latency_ms(
regenerate_timer->GetElapsedMilliseconds());
}
}
if (initialize_stats != nullptr) {
initialize_stats->set_num_schema_types(type_config_map_.size());
}
has_schema_successfully_set_ = true;
return libtextclassifier3::Status::OK;
}
libtextclassifier3::Status SchemaStore::InitializeDerivedFiles() {
ICING_ASSIGN_OR_RETURN(
schema_type_mapper_,
DynamicTrieKeyMapper<SchemaTypeId>::Create(
*filesystem_, MakeSchemaTypeMapperFilename(base_dir_),
kSchemaTypeMapperMaxSize));
ICING_ASSIGN_OR_RETURN(Crc32 checksum, ComputeChecksum());
if (checksum.Get() != header_->checksum()) {
return absl_ports::InternalError(
"Combined checksum of SchemaStore was inconsistent");
}
ICING_RETURN_IF_ERROR(BuildInMemoryCache());
return libtextclassifier3::Status::OK;
}
libtextclassifier3::Status SchemaStore::RegenerateDerivedFiles(
bool create_overlay_if_necessary) {
ICING_ASSIGN_OR_RETURN(const SchemaProto* schema_proto, GetSchema());
ICING_RETURN_IF_ERROR(ResetSchemaTypeMapper());
for (const SchemaTypeConfigProto& type_config : schema_proto->types()) {
// Assign a SchemaTypeId to the type
ICING_RETURN_IF_ERROR(schema_type_mapper_->Put(
type_config.schema_type(), schema_type_mapper_->num_keys()));
}
ICING_RETURN_IF_ERROR(BuildInMemoryCache());
if (create_overlay_if_necessary) {
ICING_ASSIGN_OR_RETURN(
BackupSchemaProducer producer,
BackupSchemaProducer::Create(*schema_proto,
schema_type_manager_->section_manager()));
if (producer.is_backup_necessary()) {
SchemaProto base_schema = std::move(producer).Produce();
// The overlay schema should be written to the overlay file location.
overlay_schema_file_ = std::make_unique<FileBackedProto<SchemaProto>>(
*filesystem_, MakeOverlaySchemaFilename(base_dir_));
auto schema_ptr = std::make_unique<SchemaProto>(std::move(*schema_proto));
ICING_RETURN_IF_ERROR(overlay_schema_file_->Write(std::move(schema_ptr)));
// The base schema should be written to the original file
auto base_schema_ptr =
std::make_unique<SchemaProto>(std::move(base_schema));
ICING_RETURN_IF_ERROR(schema_file_->Write(std::move(base_schema_ptr)));
// LINT.IfChange(min_overlay_version_compatibility)
// Although the current version is 3, the schema is compatible with
// version 1, so min_overlay_version_compatibility should be 1.
int32_t min_overlay_version_compatibility = version_util::kVersionOne;
// LINT.ThenChange(//depot/google3/icing/file/version-util.h:kVersion)
header_->SetOverlayInfo(
/*overlay_created=*/true, min_overlay_version_compatibility);
// Rebuild in memory data - references to the old schema will be invalid
// now.
ICING_RETURN_IF_ERROR(BuildInMemoryCache());
}
}
// Write the header
ICING_ASSIGN_OR_RETURN(Crc32 checksum, ComputeChecksum());
header_->set_checksum(checksum.Get());
return header_->Write(filesystem_, MakeHeaderFilename(base_dir_));
}
libtextclassifier3::Status SchemaStore::BuildInMemoryCache() {
ICING_ASSIGN_OR_RETURN(const SchemaProto* schema_proto, GetSchema());
ICING_ASSIGN_OR_RETURN(
SchemaUtil::InheritanceMap inheritance_map,
SchemaUtil::BuildTransitiveInheritanceGraph(*schema_proto));
reverse_schema_type_mapper_.clear();
type_config_map_.clear();
schema_subtype_id_map_.clear();
for (const SchemaTypeConfigProto& type_config : schema_proto->types()) {
std::string_view type_name = type_config.schema_type();
ICING_ASSIGN_OR_RETURN(SchemaTypeId type_id,
schema_type_mapper_->Get(type_name));
// Build reverse_schema_type_mapper_
reverse_schema_type_mapper_.insert({type_id, std::string(type_name)});
// Build type_config_map_
type_config_map_.insert({std::string(type_name), type_config});
// Build schema_subtype_id_map_
std::unordered_set<SchemaTypeId>& subtype_id_set =
schema_subtype_id_map_[type_id];
// Find all child types
auto child_types_names = inheritance_map.find(type_name);
if (child_types_names != inheritance_map.end()) {
subtype_id_set.reserve(child_types_names->second.size() + 1);
for (const auto& [child_type_name, is_direct_child] :
child_types_names->second) {
ICING_ASSIGN_OR_RETURN(SchemaTypeId child_type_id,
schema_type_mapper_->Get(child_type_name));
subtype_id_set.insert(child_type_id);
}
}
// Every type is a subtype of itself.
subtype_id_set.insert(type_id);
}
// Build schema_type_manager_
ICING_ASSIGN_OR_RETURN(
schema_type_manager_,
SchemaTypeManager::Create(type_config_map_, schema_type_mapper_.get()));
return libtextclassifier3::Status::OK;
}
libtextclassifier3::Status SchemaStore::ResetSchemaTypeMapper() {
// TODO(b/139734457): Replace ptr.reset()->Delete->Create flow with Reset().
schema_type_mapper_.reset();
// TODO(b/216487496): Implement a more robust version of TC_RETURN_IF_ERROR
// that can support error logging.
libtextclassifier3::Status status =
DynamicTrieKeyMapper<SchemaTypeId>::Delete(
*filesystem_, MakeSchemaTypeMapperFilename(base_dir_));
if (!status.ok()) {
ICING_LOG(ERROR) << status.error_message()
<< "Failed to delete old schema_type mapper";
return status;
}
ICING_ASSIGN_OR_RETURN(
schema_type_mapper_,
DynamicTrieKeyMapper<SchemaTypeId>::Create(
*filesystem_, MakeSchemaTypeMapperFilename(base_dir_),
kSchemaTypeMapperMaxSize));
return libtextclassifier3::Status::OK;
}
libtextclassifier3::StatusOr<Crc32> SchemaStore::ComputeChecksum() const {
// Base schema checksum
auto schema_proto_or = schema_file_->Read();
if (absl_ports::IsNotFound(schema_proto_or.status())) {
return Crc32();
}
ICING_ASSIGN_OR_RETURN(const SchemaProto* schema_proto, schema_proto_or);
Crc32 schema_checksum;
schema_checksum.Append(schema_proto->SerializeAsString());
Crc32 overlay_schema_checksum;
if (overlay_schema_file_ != nullptr) {
auto schema_proto_or = schema_file_->Read();
if (schema_proto_or.ok()) {
ICING_ASSIGN_OR_RETURN(schema_proto, schema_proto_or);
overlay_schema_checksum.Append(schema_proto->SerializeAsString());
}
}
ICING_ASSIGN_OR_RETURN(Crc32 schema_type_mapper_checksum,
schema_type_mapper_->ComputeChecksum());
Crc32 total_checksum;
total_checksum.Append(std::to_string(schema_checksum.Get()));
if (overlay_schema_file_ != nullptr) {
total_checksum.Append(std::to_string(overlay_schema_checksum.Get()));
}
total_checksum.Append(std::to_string(schema_type_mapper_checksum.Get()));
return total_checksum;
}
libtextclassifier3::StatusOr<const SchemaProto*> SchemaStore::GetSchema()
const {
if (overlay_schema_file_ != nullptr) {
return overlay_schema_file_->Read();
}
return schema_file_->Read();
}
// TODO(cassiewang): Consider removing this definition of SetSchema if it's not
// needed by production code. It's currently being used by our tests, but maybe
// it's trivial to change our test code to also use the
// SetSchema(SchemaProto&& new_schema)
libtextclassifier3::StatusOr<const SchemaStore::SetSchemaResult>
SchemaStore::SetSchema(const SchemaProto& new_schema,
bool ignore_errors_and_delete_documents,
bool allow_circular_schema_definitions) {
return SetSchema(SchemaProto(new_schema), ignore_errors_and_delete_documents,
allow_circular_schema_definitions);
}
libtextclassifier3::StatusOr<const SchemaStore::SetSchemaResult>
SchemaStore::SetSchema(SchemaProto&& new_schema,
bool ignore_errors_and_delete_documents,
bool allow_circular_schema_definitions) {
ICING_ASSIGN_OR_RETURN(
SchemaUtil::DependentMap new_dependent_map,
SchemaUtil::Validate(new_schema, allow_circular_schema_definitions));
SetSchemaResult result;
auto schema_proto_or = GetSchema();
if (absl_ports::IsNotFound(schema_proto_or.status())) {
// We don't have a pre-existing schema, so anything is valid.
result.success = true;
for (const SchemaTypeConfigProto& type_config : new_schema.types()) {
result.schema_types_new_by_name.insert(type_config.schema_type());
}
} else if (!schema_proto_or.ok()) {
// Real error
return schema_proto_or.status();
} else {
// At this point, we're guaranteed that we have a schema.
const SchemaProto old_schema = *schema_proto_or.ValueOrDie();
// Assume we can set the schema unless proven otherwise.
result.success = true;
if (new_schema.SerializeAsString() == old_schema.SerializeAsString()) {
// Same schema as before. No need to update anything
return result;
}
// Different schema, track the differences and see if we can still write it
SchemaUtil::SchemaDelta schema_delta =
SchemaUtil::ComputeCompatibilityDelta(old_schema, new_schema,
new_dependent_map);
result.schema_types_new_by_name = std::move(schema_delta.schema_types_new);
result.schema_types_changed_fully_compatible_by_name =
std::move(schema_delta.schema_types_changed_fully_compatible);
result.schema_types_index_incompatible_by_name =
std::move(schema_delta.schema_types_index_incompatible);
result.schema_types_join_incompatible_by_name =
std::move(schema_delta.schema_types_join_incompatible);
for (const auto& schema_type : schema_delta.schema_types_deleted) {
// We currently don't support deletions, so mark this as not possible.
// This will change once we allow force-set schemas.
result.success = false;
result.schema_types_deleted_by_name.emplace(schema_type);
ICING_ASSIGN_OR_RETURN(SchemaTypeId schema_type_id,
GetSchemaTypeId(schema_type));
result.schema_types_deleted_by_id.emplace(schema_type_id);
}
for (const auto& schema_type : schema_delta.schema_types_incompatible) {
// We currently don't support incompatible schemas, so mark this as
// not possible. This will change once we allow force-set schemas.
result.success = false;
result.schema_types_incompatible_by_name.emplace(schema_type);
ICING_ASSIGN_OR_RETURN(SchemaTypeId schema_type_id,
GetSchemaTypeId(schema_type));
result.schema_types_incompatible_by_id.emplace(schema_type_id);
}
// SchemaTypeIds changing is fine, we can update the DocumentStore
result.old_schema_type_ids_changed =
SchemaTypeIdsChanged(old_schema, new_schema);
}
// We can force set the schema if the caller has told us to ignore any errors
result.success = result.success || ignore_errors_and_delete_documents;
if (result.success) {
ICING_RETURN_IF_ERROR(ApplySchemaChange(std::move(new_schema)));
has_schema_successfully_set_ = true;
}
return result;
}
libtextclassifier3::Status SchemaStore::ApplySchemaChange(
SchemaProto new_schema) {
// We need to ensure that we either 1) successfully set the schema and
// update all derived data structures or 2) fail and leave the schema store
// unchanged.
// So, first, we create an empty temporary directory to build a new schema
// store in.
std::string temp_schema_store_dir_path = base_dir_ + "_temp";
if (!filesystem_->DeleteDirectoryRecursively(
temp_schema_store_dir_path.c_str())) {
ICING_LOG(ERROR) << "Recursively deleting "
<< temp_schema_store_dir_path.c_str();
return absl_ports::InternalError(
"Unable to delete temp directory to prepare to build new schema "
"store.");
}
DestructibleDirectory temp_schema_store_dir(
filesystem_, std::move(temp_schema_store_dir_path));
if (!temp_schema_store_dir.is_valid()) {
return absl_ports::InternalError(
"Unable to create temp directory to build new schema store.");
}
// Then we create our new schema store with the new schema.
ICING_ASSIGN_OR_RETURN(
std::unique_ptr<SchemaStore> new_schema_store,
SchemaStore::Create(filesystem_, temp_schema_store_dir.dir(), clock_,
std::move(new_schema)));
// Then we swap the new schema file + new derived files with the old files.
if (!filesystem_->SwapFiles(base_dir_.c_str(),
temp_schema_store_dir.dir().c_str())) {
return absl_ports::InternalError(
"Unable to apply new schema due to failed swap!");
}
std::string old_base_dir = std::move(base_dir_);
*this = std::move(*new_schema_store);
// After the std::move, the filepaths saved in this instance and in the
// schema_file_ instance will still be the one from temp_schema_store_dir
// even though they now point to files that are within old_base_dir.
// Manually set them to the correct paths.
base_dir_ = std::move(old_base_dir);
schema_file_->SetSwappedFilepath(MakeSchemaFilename(base_dir_));
if (overlay_schema_file_ != nullptr) {
overlay_schema_file_->SetSwappedFilepath(
MakeOverlaySchemaFilename(base_dir_));
}
return libtextclassifier3::Status::OK;
}
libtextclassifier3::StatusOr<const SchemaTypeConfigProto*>
SchemaStore::GetSchemaTypeConfig(std::string_view schema_type) const {
ICING_RETURN_IF_ERROR(CheckSchemaSet());
const auto& type_config_iter =
type_config_map_.find(std::string(schema_type));
if (type_config_iter == type_config_map_.end()) {
return absl_ports::NotFoundError(
absl_ports::StrCat("Schema type config '", schema_type, "' not found"));
}
return &type_config_iter->second;
}
libtextclassifier3::StatusOr<SchemaTypeId> SchemaStore::GetSchemaTypeId(
std::string_view schema_type) const {
ICING_RETURN_IF_ERROR(CheckSchemaSet());
return schema_type_mapper_->Get(schema_type);
}
libtextclassifier3::StatusOr<const std::string*> SchemaStore::GetSchemaType(
SchemaTypeId schema_type_id) const {
ICING_RETURN_IF_ERROR(CheckSchemaSet());
if (const auto it = reverse_schema_type_mapper_.find(schema_type_id);
it == reverse_schema_type_mapper_.end()) {
return absl_ports::InvalidArgumentError("Invalid schema type id");
} else {
return &it->second;
}
}
libtextclassifier3::StatusOr<const std::unordered_set<SchemaTypeId>*>
SchemaStore::GetSchemaTypeIdsWithChildren(std::string_view schema_type) const {
ICING_ASSIGN_OR_RETURN(SchemaTypeId schema_type_id,
GetSchemaTypeId(schema_type));
auto iter = schema_subtype_id_map_.find(schema_type_id);
if (iter == schema_subtype_id_map_.end()) {
// This should never happen, unless there is an inconsistency or IO error.
return absl_ports::InternalError(absl_ports::StrCat(
"Schema type '", schema_type, "' is not found in the subtype map."));
}
return &iter->second;
}
libtextclassifier3::StatusOr<const SectionMetadata*>
SchemaStore::GetSectionMetadata(SchemaTypeId schema_type_id,
SectionId section_id) const {
ICING_RETURN_IF_ERROR(CheckSchemaSet());
return schema_type_manager_->section_manager().GetSectionMetadata(
schema_type_id, section_id);
}
libtextclassifier3::StatusOr<SectionGroup> SchemaStore::ExtractSections(
const DocumentProto& document) const {
ICING_RETURN_IF_ERROR(CheckSchemaSet());
return schema_type_manager_->section_manager().ExtractSections(document);
}
libtextclassifier3::StatusOr<const JoinablePropertyMetadata*>
SchemaStore::GetJoinablePropertyMetadata(
SchemaTypeId schema_type_id, const std::string& property_path) const {
ICING_RETURN_IF_ERROR(CheckSchemaSet());
return schema_type_manager_->joinable_property_manager()
.GetJoinablePropertyMetadata(schema_type_id, property_path);
}
libtextclassifier3::StatusOr<JoinablePropertyGroup>
SchemaStore::ExtractJoinableProperties(const DocumentProto& document) const {
ICING_RETURN_IF_ERROR(CheckSchemaSet());
return schema_type_manager_->joinable_property_manager()
.ExtractJoinableProperties(document);
}
libtextclassifier3::Status SchemaStore::PersistToDisk() {
if (!has_schema_successfully_set_) {
return libtextclassifier3::Status::OK;
}
ICING_RETURN_IF_ERROR(schema_type_mapper_->PersistToDisk());
// Write the header
ICING_ASSIGN_OR_RETURN(Crc32 checksum, ComputeChecksum());
header_->set_checksum(checksum.Get());
return header_->Write(filesystem_, MakeHeaderFilename(base_dir_));
}
SchemaStoreStorageInfoProto SchemaStore::GetStorageInfo() const {
SchemaStoreStorageInfoProto storage_info;
int64_t directory_size = filesystem_->GetDiskUsage(base_dir_.c_str());
storage_info.set_schema_store_size(
Filesystem::SanitizeFileSize(directory_size));
ICING_ASSIGN_OR_RETURN(const SchemaProto* schema, GetSchema(), storage_info);
storage_info.set_num_schema_types(schema->types_size());
int total_sections = 0;
int num_types_sections_exhausted = 0;
for (const SchemaTypeConfigProto& type : schema->types()) {
auto sections_list_or =
schema_type_manager_->section_manager().GetMetadataList(
type.schema_type());
if (!sections_list_or.ok()) {
continue;
}
total_sections += sections_list_or.ValueOrDie()->size();
if (sections_list_or.ValueOrDie()->size() == kTotalNumSections) {
++num_types_sections_exhausted;
}
}
storage_info.set_num_total_sections(total_sections);
storage_info.set_num_schema_types_sections_exhausted(
num_types_sections_exhausted);
return storage_info;
}
libtextclassifier3::StatusOr<const std::vector<SectionMetadata>*>
SchemaStore::GetSectionMetadata(const std::string& schema_type) const {
return schema_type_manager_->section_manager().GetMetadataList(schema_type);
}
bool SchemaStore::IsPropertyDefinedInSchema(
SchemaTypeId schema_type_id, const std::string& property_path) const {
auto schema_name_itr = reverse_schema_type_mapper_.find(schema_type_id);
if (schema_name_itr == reverse_schema_type_mapper_.end()) {
return false;
}
const std::string* current_type_name = &schema_name_itr->second;
std::vector<std::string_view> property_path_parts =
property_util::SplitPropertyPathExpr(property_path);
for (int i = 0; i < property_path_parts.size(); ++i) {
auto type_config_itr = type_config_map_.find(*current_type_name);
if (type_config_itr == type_config_map_.end()) {
return false;
}
std::string_view property_name = property_path_parts.at(i);
const PropertyConfigProto* selected_property = nullptr;
for (const PropertyConfigProto& property :
type_config_itr->second.properties()) {
if (property.property_name() == property_name) {
selected_property = &property;
break;
}
}
if (selected_property == nullptr) {
return false;
}
if (i == property_path_parts.size() - 1) {
// We've found a property at the final part of the path.
return true;
}
if (selected_property->data_type() !=
PropertyConfigProto::DataType::DOCUMENT) {
// If this isn't final part of the path, but this property isn't a
// document, so we know that this path doesn't exist.
return false;
}
current_type_name = &selected_property->schema_type();
}
// We should never reach this point.
return false;
}
libtextclassifier3::StatusOr<SchemaDebugInfoProto> SchemaStore::GetDebugInfo()
const {
SchemaDebugInfoProto debug_info;
if (has_schema_successfully_set_) {
ICING_ASSIGN_OR_RETURN(const SchemaProto* schema, GetSchema());
*debug_info.mutable_schema() = *schema;
}
ICING_ASSIGN_OR_RETURN(Crc32 crc, ComputeChecksum());
debug_info.set_crc(crc.Get());
return debug_info;
}
std::vector<SchemaStore::ExpandedTypePropertyMask>
SchemaStore::ExpandTypePropertyMasks(
const google::protobuf::RepeatedPtrField<TypePropertyMask>& type_property_masks)
const {
std::unordered_map<SchemaTypeId, ExpandedTypePropertyMask> result_map;
for (const TypePropertyMask& type_field_mask : type_property_masks) {
if (type_field_mask.schema_type() == kSchemaTypeWildcard) {
ExpandedTypePropertyMask entry{type_field_mask.schema_type(),
/*paths=*/{}};
entry.paths.insert(type_field_mask.paths().begin(),
type_field_mask.paths().end());
result_map.insert({kInvalidSchemaTypeId, std::move(entry)});
} else {
auto schema_type_ids_or =
GetSchemaTypeIdsWithChildren(type_field_mask.schema_type());
// If we can't find the SchemaTypeIds, just throw it away
if (!schema_type_ids_or.ok()) {
continue;
}
const std::unordered_set<SchemaTypeId>* schema_type_ids =
schema_type_ids_or.ValueOrDie();
for (SchemaTypeId schema_type_id : *schema_type_ids) {
auto schema_type_name_iter =
reverse_schema_type_mapper_.find(schema_type_id);
if (schema_type_name_iter == reverse_schema_type_mapper_.end()) {
// This should never happen, unless there is an inconsistency or IO
// error.
ICING_LOG(ERROR) << "Got unknown schema type id: " << schema_type_id;
continue;
}
auto iter = result_map.find(schema_type_id);
if (iter == result_map.end()) {
ExpandedTypePropertyMask entry{schema_type_name_iter->second,
/*paths=*/{}};
iter = result_map.insert({schema_type_id, std::move(entry)}).first;
}
iter->second.paths.insert(type_field_mask.paths().begin(),
type_field_mask.paths().end());
}
}
}
std::vector<ExpandedTypePropertyMask> result;
result.reserve(result_map.size());
for (auto& entry : result_map) {
result.push_back(std::move(entry.second));
}
return result;
}
} // namespace lib
} // namespace icing