• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (C) 2019 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include "icing/schema/schema-util.h"
16 
17 #include <cstdint>
18 #include <string>
19 #include <string_view>
20 #include <unordered_map>
21 #include <unordered_set>
22 #include <utility>
23 
24 #include "icing/text_classifier/lib3/utils/base/status.h"
25 #include "icing/absl_ports/annotate.h"
26 #include "icing/absl_ports/canonical_errors.h"
27 #include "icing/absl_ports/str_cat.h"
28 #include "icing/absl_ports/str_join.h"
29 #include "icing/legacy/core/icing-string-util.h"
30 #include "icing/proto/schema.pb.h"
31 #include "icing/proto/term.pb.h"
32 #include "icing/util/logging.h"
33 #include "icing/util/status-macros.h"
34 
35 namespace icing {
36 namespace lib {
37 
38 namespace {
39 
ArePropertiesEqual(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)40 bool ArePropertiesEqual(const PropertyConfigProto& old_property,
41                         const PropertyConfigProto& new_property) {
42   return old_property.property_name() == new_property.property_name() &&
43          old_property.data_type() == new_property.data_type() &&
44          old_property.schema_type() == new_property.schema_type() &&
45          old_property.cardinality() == new_property.cardinality() &&
46          old_property.string_indexing_config().term_match_type() ==
47              new_property.string_indexing_config().term_match_type() &&
48          old_property.string_indexing_config().tokenizer_type() ==
49              new_property.string_indexing_config().tokenizer_type() &&
50          old_property.document_indexing_config().index_nested_properties() ==
51              new_property.document_indexing_config().index_nested_properties();
52 }
53 
IsCardinalityCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)54 bool IsCardinalityCompatible(const PropertyConfigProto& old_property,
55                              const PropertyConfigProto& new_property) {
56   if (old_property.cardinality() < new_property.cardinality()) {
57     // We allow a new, less restrictive cardinality (i.e. a REQUIRED field
58     // can become REPEATED or OPTIONAL, but not the other way around).
59     ICING_VLOG(1) << absl_ports::StrCat(
60         "Cardinality is more restrictive than before ",
61         PropertyConfigProto::Cardinality::Code_Name(old_property.cardinality()),
62         "->",
63         PropertyConfigProto::Cardinality::Code_Name(
64             new_property.cardinality()));
65     return false;
66   }
67   return true;
68 }
69 
IsDataTypeCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)70 bool IsDataTypeCompatible(const PropertyConfigProto& old_property,
71                           const PropertyConfigProto& new_property) {
72   if (old_property.data_type() != new_property.data_type()) {
73     // TODO(cassiewang): Maybe we can be a bit looser with this, e.g. we just
74     // string cast an int64_t to a string. But for now, we'll stick with
75     // simplistics.
76     ICING_VLOG(1) << absl_ports::StrCat(
77         "Data type ",
78         PropertyConfigProto::DataType::Code_Name(old_property.data_type()),
79         "->",
80         PropertyConfigProto::DataType::Code_Name(new_property.data_type()));
81     return false;
82   }
83   return true;
84 }
85 
IsSchemaTypeCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)86 bool IsSchemaTypeCompatible(const PropertyConfigProto& old_property,
87                             const PropertyConfigProto& new_property) {
88   if (old_property.schema_type() != new_property.schema_type()) {
89     ICING_VLOG(1) << absl_ports::StrCat("Schema type ",
90                                         old_property.schema_type(), "->",
91                                         new_property.schema_type());
92     return false;
93   }
94   return true;
95 }
96 
IsPropertyCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)97 bool IsPropertyCompatible(const PropertyConfigProto& old_property,
98                           const PropertyConfigProto& new_property) {
99   return IsDataTypeCompatible(old_property, new_property) &&
100          IsSchemaTypeCompatible(old_property, new_property) &&
101          IsCardinalityCompatible(old_property, new_property);
102 }
103 
IsTermMatchTypeCompatible(const StringIndexingConfig & old_indexed,const StringIndexingConfig & new_indexed)104 bool IsTermMatchTypeCompatible(const StringIndexingConfig& old_indexed,
105                                const StringIndexingConfig& new_indexed) {
106   return old_indexed.term_match_type() == new_indexed.term_match_type() &&
107          old_indexed.tokenizer_type() == new_indexed.tokenizer_type();
108 }
109 
AddIncompatibleChangeToDelta(std::unordered_set<std::string> & incompatible_delta,const SchemaTypeConfigProto & old_type_config,const SchemaUtil::DependencyMap & new_schema_dependency_map,const SchemaUtil::TypeConfigMap & old_type_config_map,const SchemaUtil::TypeConfigMap & new_type_config_map)110 void AddIncompatibleChangeToDelta(
111     std::unordered_set<std::string>& incompatible_delta,
112     const SchemaTypeConfigProto& old_type_config,
113     const SchemaUtil::DependencyMap& new_schema_dependency_map,
114     const SchemaUtil::TypeConfigMap& old_type_config_map,
115     const SchemaUtil::TypeConfigMap& new_type_config_map) {
116   // If this type is incompatible, then every type that depends on it might
117   // also be incompatible. Use the dependency map to mark those ones as
118   // incompatible too.
119   incompatible_delta.insert(old_type_config.schema_type());
120   auto parent_types_itr =
121       new_schema_dependency_map.find(old_type_config.schema_type());
122   if (parent_types_itr != new_schema_dependency_map.end()) {
123     for (std::string_view parent_type : parent_types_itr->second) {
124       // The types from new_schema that depend on the current
125       // old_type_config may not present in old_schema.
126       // Those types will be listed at schema_delta.schema_types_new
127       // instead.
128       std::string parent_type_str(parent_type);
129       if (old_type_config_map.find(parent_type_str) !=
130           old_type_config_map.end()) {
131         incompatible_delta.insert(std::move(parent_type_str));
132       }
133     }
134   }
135 }
136 
137 }  // namespace
138 
ExpandTranstiveDependencies(const SchemaUtil::DependencyMap & child_to_direct_parent_map,std::string_view type,SchemaUtil::DependencyMap * expanded_child_to_parent_map,std::unordered_set<std::string_view> * pending_expansions,std::unordered_set<std::string_view> * orphaned_types)139 libtextclassifier3::Status ExpandTranstiveDependencies(
140     const SchemaUtil::DependencyMap& child_to_direct_parent_map,
141     std::string_view type,
142     SchemaUtil::DependencyMap* expanded_child_to_parent_map,
143     std::unordered_set<std::string_view>* pending_expansions,
144     std::unordered_set<std::string_view>* orphaned_types) {
145   auto expanded_itr = expanded_child_to_parent_map->find(type);
146   if (expanded_itr != expanded_child_to_parent_map->end()) {
147     // We've already expanded this type. Just return.
148     return libtextclassifier3::Status::OK;
149   }
150   auto itr = child_to_direct_parent_map.find(type);
151   if (itr == child_to_direct_parent_map.end()) {
152     // It's an orphan. Just return.
153     orphaned_types->insert(type);
154     return libtextclassifier3::Status::OK;
155   }
156   pending_expansions->insert(type);
157   std::unordered_set<std::string_view> expanded_dependencies;
158 
159   // Add all of the direct parent dependencies.
160   expanded_dependencies.reserve(itr->second.size());
161   expanded_dependencies.insert(itr->second.begin(), itr->second.end());
162 
163   // Iterate through each direct parent and add their indirect parents.
164   for (std::string_view dep : itr->second) {
165     // 1. Check if we're in the middle of expanding this type - IOW there's a
166     // cycle!
167     if (pending_expansions->count(dep) > 0) {
168       return absl_ports::InvalidArgumentError(
169           absl_ports::StrCat("Infinite loop detected in type configs. '", type,
170                              "' references itself."));
171     }
172 
173     // 2. Expand this type as needed.
174     ICING_RETURN_IF_ERROR(ExpandTranstiveDependencies(
175         child_to_direct_parent_map, dep, expanded_child_to_parent_map,
176         pending_expansions, orphaned_types));
177     if (orphaned_types->count(dep) > 0) {
178       // Dep is an orphan. Just skip to the next dep.
179       continue;
180     }
181 
182     // 3. Dep has been fully expanded. Add all of its dependencies to this
183     // type's dependencies.
184     auto dep_expanded_itr = expanded_child_to_parent_map->find(dep);
185     expanded_dependencies.reserve(expanded_dependencies.size() +
186                                   dep_expanded_itr->second.size());
187     expanded_dependencies.insert(dep_expanded_itr->second.begin(),
188                                  dep_expanded_itr->second.end());
189   }
190   expanded_child_to_parent_map->insert(
191       {type, std::move(expanded_dependencies)});
192   pending_expansions->erase(type);
193   return libtextclassifier3::Status::OK;
194 }
195 
196 // Expands the dependencies represented by the child_to_direct_parent_map to
197 // also include indirect parents.
198 //
199 // Ex. Suppose we have a schema with four types A, B, C, D. A has a property of
200 // type B and B has a property of type C. C and D only have non-document
201 // properties.
202 //
203 // The child to direct parent dependency map for this schema would be:
204 // C -> B
205 // B -> A
206 //
207 // This function would expand it so that A is also present as an indirect parent
208 // of C.
209 libtextclassifier3::StatusOr<SchemaUtil::DependencyMap>
ExpandTranstiveDependencies(const SchemaUtil::DependencyMap & child_to_direct_parent_map)210 ExpandTranstiveDependencies(
211     const SchemaUtil::DependencyMap& child_to_direct_parent_map) {
212   SchemaUtil::DependencyMap expanded_child_to_parent_map;
213 
214   // Types that we are expanding.
215   std::unordered_set<std::string_view> pending_expansions;
216 
217   // Types that have no parents that depend on them.
218   std::unordered_set<std::string_view> orphaned_types;
219   for (const auto& kvp : child_to_direct_parent_map) {
220     ICING_RETURN_IF_ERROR(ExpandTranstiveDependencies(
221         child_to_direct_parent_map, kvp.first, &expanded_child_to_parent_map,
222         &pending_expansions, &orphaned_types));
223   }
224   return expanded_child_to_parent_map;
225 }
226 
227 // Builds a transitive child-parent dependency map. 'Orphaned' types (types with
228 // no parents) will not be present in the map.
229 //
230 // Ex. Suppose we have a schema with four types A, B, C, D. A has a property of
231 // type B and B has a property of type C. C and D only have non-document
232 // properties.
233 //
234 // The transitive child-parent dependency map for this schema would be:
235 // C -> A, B
236 // B -> A
237 //
238 // A and D would be considered orphaned properties because no type refers to
239 // them.
240 //
241 // RETURNS:
242 //   On success, a transitive child-parent dependency map of all types in the
243 //   schema.
244 //   INVALID_ARGUMENT if the schema contains a cycle or an undefined type.
245 //   ALREADY_EXISTS if a schema type is specified more than once in the schema
246 libtextclassifier3::StatusOr<SchemaUtil::DependencyMap>
BuildTransitiveDependencyGraph(const SchemaProto & schema)247 BuildTransitiveDependencyGraph(const SchemaProto& schema) {
248   // Child to parent map.
249   SchemaUtil::DependencyMap child_to_direct_parent_map;
250 
251   // Add all first-order dependencies.
252   std::unordered_set<std::string_view> known_types;
253   std::unordered_set<std::string_view> unknown_types;
254   for (const auto& type_config : schema.types()) {
255     std::string_view schema_type(type_config.schema_type());
256     if (known_types.count(schema_type) > 0) {
257       return absl_ports::AlreadyExistsError(absl_ports::StrCat(
258           "Field 'schema_type' '", schema_type, "' is already defined"));
259     }
260     known_types.insert(schema_type);
261     unknown_types.erase(schema_type);
262     for (const auto& property_config : type_config.properties()) {
263       if (property_config.data_type() ==
264           PropertyConfigProto::DataType::DOCUMENT) {
265         // Need to know what schema_type these Document properties should be
266         // validated against
267         std::string_view property_schema_type(property_config.schema_type());
268         if (property_schema_type == schema_type) {
269           return absl_ports::InvalidArgumentError(
270               absl_ports::StrCat("Infinite loop detected in type configs. '",
271                                  schema_type, "' references itself."));
272         }
273         if (known_types.count(property_schema_type) == 0) {
274           unknown_types.insert(property_schema_type);
275         }
276         auto itr = child_to_direct_parent_map.find(property_schema_type);
277         if (itr == child_to_direct_parent_map.end()) {
278           child_to_direct_parent_map.insert(
279               {property_schema_type, std::unordered_set<std::string_view>()});
280           itr = child_to_direct_parent_map.find(property_schema_type);
281         }
282         itr->second.insert(schema_type);
283       }
284     }
285   }
286   if (!unknown_types.empty()) {
287     return absl_ports::InvalidArgumentError(absl_ports::StrCat(
288         "Undefined 'schema_type's: ", absl_ports::StrJoin(unknown_types, ",")));
289   }
290   return ExpandTranstiveDependencies(child_to_direct_parent_map);
291 }
292 
Validate(const SchemaProto & schema)293 libtextclassifier3::StatusOr<SchemaUtil::DependencyMap> SchemaUtil::Validate(
294     const SchemaProto& schema) {
295   // 1. Build the dependency map. This will detect any cycles, non-existent or
296   // duplicate types in the schema.
297   ICING_ASSIGN_OR_RETURN(SchemaUtil::DependencyMap dependency_map,
298                          BuildTransitiveDependencyGraph(schema));
299 
300   // Tracks PropertyConfigs within a SchemaTypeConfig that we've validated
301   // already.
302   std::unordered_set<std::string_view> known_property_names;
303 
304   // 2. Validate the properties of each type.
305   for (const auto& type_config : schema.types()) {
306     std::string_view schema_type(type_config.schema_type());
307     ICING_RETURN_IF_ERROR(ValidateSchemaType(schema_type));
308 
309     // We only care about properties being unique within one type_config
310     known_property_names.clear();
311 
312     for (const auto& property_config : type_config.properties()) {
313       std::string_view property_name(property_config.property_name());
314       ICING_RETURN_IF_ERROR(ValidatePropertyName(property_name, schema_type));
315 
316       // Property names must be unique
317       if (!known_property_names.insert(property_name).second) {
318         return absl_ports::AlreadyExistsError(absl_ports::StrCat(
319             "Field 'property_name' '", property_name,
320             "' is already defined for schema '", schema_type, "'"));
321       }
322 
323       auto data_type = property_config.data_type();
324       ICING_RETURN_IF_ERROR(
325           ValidateDataType(data_type, schema_type, property_name));
326 
327       if (data_type == PropertyConfigProto::DataType::DOCUMENT) {
328         // Need to know what schema_type these Document properties should be
329         // validated against
330         std::string_view property_schema_type(property_config.schema_type());
331         libtextclassifier3::Status validated_status =
332             ValidateSchemaType(property_schema_type);
333         if (!validated_status.ok()) {
334           return absl_ports::Annotate(
335               validated_status,
336               absl_ports::StrCat("Field 'schema_type' is required for DOCUMENT "
337                                  "data_types in schema property '",
338                                  schema_type, ".", property_name, "'"));
339         }
340       }
341 
342       ICING_RETURN_IF_ERROR(ValidateCardinality(property_config.cardinality(),
343                                                 schema_type, property_name));
344 
345       if (data_type == PropertyConfigProto::DataType::STRING) {
346         ICING_RETURN_IF_ERROR(ValidateStringIndexingConfig(
347             property_config.string_indexing_config(), data_type, schema_type,
348             property_name));
349       }
350     }
351   }
352 
353   return dependency_map;
354 }
355 
ValidateSchemaType(std::string_view schema_type)356 libtextclassifier3::Status SchemaUtil::ValidateSchemaType(
357     std::string_view schema_type) {
358   // Require a schema_type
359   if (schema_type.empty()) {
360     return absl_ports::InvalidArgumentError(
361         "Field 'schema_type' cannot be empty.");
362   }
363 
364   return libtextclassifier3::Status::OK;
365 }
366 
ValidatePropertyName(std::string_view property_name,std::string_view schema_type)367 libtextclassifier3::Status SchemaUtil::ValidatePropertyName(
368     std::string_view property_name, std::string_view schema_type) {
369   // Require a property_name
370   if (property_name.empty()) {
371     return absl_ports::InvalidArgumentError(
372         absl_ports::StrCat("Field 'property_name' for schema '", schema_type,
373                            "' cannot be empty."));
374   }
375 
376   // Only support alphanumeric values.
377   for (char c : property_name) {
378     if (!std::isalnum(c)) {
379       return absl_ports::InvalidArgumentError(
380           absl_ports::StrCat("Field 'property_name' '", property_name,
381                              "' can only contain alphanumeric characters."));
382     }
383   }
384 
385   return libtextclassifier3::Status::OK;
386 }
387 
ValidateDataType(PropertyConfigProto::DataType::Code data_type,std::string_view schema_type,std::string_view property_name)388 libtextclassifier3::Status SchemaUtil::ValidateDataType(
389     PropertyConfigProto::DataType::Code data_type, std::string_view schema_type,
390     std::string_view property_name) {
391   // UNKNOWN is the default enum value and should only be used for backwards
392   // compatibility
393   if (data_type == PropertyConfigProto::DataType::UNKNOWN) {
394     return absl_ports::InvalidArgumentError(absl_ports::StrCat(
395         "Field 'data_type' cannot be UNKNOWN for schema property '",
396         schema_type, ".", property_name, "'"));
397   }
398 
399   return libtextclassifier3::Status::OK;
400 }
401 
ValidateCardinality(PropertyConfigProto::Cardinality::Code cardinality,std::string_view schema_type,std::string_view property_name)402 libtextclassifier3::Status SchemaUtil::ValidateCardinality(
403     PropertyConfigProto::Cardinality::Code cardinality,
404     std::string_view schema_type, std::string_view property_name) {
405   // UNKNOWN is the default enum value and should only be used for backwards
406   // compatibility
407   if (cardinality == PropertyConfigProto::Cardinality::UNKNOWN) {
408     return absl_ports::InvalidArgumentError(absl_ports::StrCat(
409         "Field 'cardinality' cannot be UNKNOWN for schema property '",
410         schema_type, ".", property_name, "'"));
411   }
412 
413   return libtextclassifier3::Status::OK;
414 }
415 
ValidateStringIndexingConfig(const StringIndexingConfig & config,PropertyConfigProto::DataType::Code data_type,std::string_view schema_type,std::string_view property_name)416 libtextclassifier3::Status SchemaUtil::ValidateStringIndexingConfig(
417     const StringIndexingConfig& config,
418     PropertyConfigProto::DataType::Code data_type, std::string_view schema_type,
419     std::string_view property_name) {
420   if (config.term_match_type() == TermMatchType::UNKNOWN &&
421       config.tokenizer_type() != StringIndexingConfig::TokenizerType::NONE) {
422     // They set a tokenizer type, but no term match type.
423     return absl_ports::InvalidArgumentError(absl_ports::StrCat(
424         "Indexed string property '", schema_type, ".", property_name,
425         "' cannot have a term match type UNKNOWN"));
426   }
427 
428   if (config.term_match_type() != TermMatchType::UNKNOWN &&
429       config.tokenizer_type() == StringIndexingConfig::TokenizerType::NONE) {
430     // They set a term match type, but no tokenizer type
431     return absl_ports::InvalidArgumentError(
432         absl_ports::StrCat("Indexed string property '", property_name,
433                            "' cannot have a tokenizer type of NONE"));
434   }
435 
436   return libtextclassifier3::Status::OK;
437 }
438 
BuildTypeConfigMap(const SchemaProto & schema,SchemaUtil::TypeConfigMap * type_config_map)439 void SchemaUtil::BuildTypeConfigMap(
440     const SchemaProto& schema, SchemaUtil::TypeConfigMap* type_config_map) {
441   type_config_map->clear();
442   for (const SchemaTypeConfigProto& type_config : schema.types()) {
443     type_config_map->emplace(type_config.schema_type(), type_config);
444   }
445 }
446 
ParsePropertyConfigs(const SchemaTypeConfigProto & type_config)447 SchemaUtil::ParsedPropertyConfigs SchemaUtil::ParsePropertyConfigs(
448     const SchemaTypeConfigProto& type_config) {
449   ParsedPropertyConfigs parsed_property_configs;
450 
451   // TODO(cassiewang): consider caching property_config_map for some properties,
452   // e.g. using LRU cache. Or changing schema.proto to use go/protomap.
453   for (const PropertyConfigProto& property_config : type_config.properties()) {
454     parsed_property_configs.property_config_map.emplace(
455         property_config.property_name(), &property_config);
456     if (property_config.cardinality() ==
457         PropertyConfigProto::Cardinality::REQUIRED) {
458       parsed_property_configs.num_required_properties++;
459     }
460 
461     // A non-default term_match_type indicates that this property is meant to be
462     // indexed.
463     if (property_config.string_indexing_config().term_match_type() !=
464         TermMatchType::UNKNOWN) {
465       parsed_property_configs.num_indexed_properties++;
466     }
467   }
468 
469   return parsed_property_configs;
470 }
471 
ComputeCompatibilityDelta(const SchemaProto & old_schema,const SchemaProto & new_schema,const DependencyMap & new_schema_dependency_map)472 const SchemaUtil::SchemaDelta SchemaUtil::ComputeCompatibilityDelta(
473     const SchemaProto& old_schema, const SchemaProto& new_schema,
474     const DependencyMap& new_schema_dependency_map) {
475   SchemaDelta schema_delta;
476 
477   TypeConfigMap old_type_config_map, new_type_config_map;
478   BuildTypeConfigMap(old_schema, &old_type_config_map);
479   BuildTypeConfigMap(new_schema, &new_type_config_map);
480 
481   // Iterate through and check each field of the old schema
482   for (const auto& old_type_config : old_schema.types()) {
483     auto new_schema_type_and_config =
484         new_type_config_map.find(old_type_config.schema_type());
485 
486     if (new_schema_type_and_config == new_type_config_map.end()) {
487       // Didn't find the old schema type in the new schema, all the old
488       // documents of this schema type are invalid without the schema
489       ICING_VLOG(1) << absl_ports::StrCat("Previously defined schema type '",
490                                           old_type_config.schema_type(),
491                                           "' was not defined in new schema");
492       schema_delta.schema_types_deleted.insert(old_type_config.schema_type());
493       continue;
494     }
495 
496     ParsedPropertyConfigs new_parsed_property_configs =
497         ParsePropertyConfigs(new_schema_type_and_config->second);
498 
499     // We only need to check the old, existing properties to see if they're
500     // compatible since we'll have old data that may be invalidated or need to
501     // be reindexed.
502     int32_t old_required_properties = 0;
503     int32_t old_indexed_properties = 0;
504 
505     // If there is a different number of properties, then there must have been a
506     // change.
507     bool has_property_changed =
508         old_type_config.properties_size() !=
509         new_schema_type_and_config->second.properties_size();
510     bool is_incompatible = false;
511     bool is_index_incompatible = false;
512     for (const auto& old_property_config : old_type_config.properties()) {
513       if (old_property_config.cardinality() ==
514           PropertyConfigProto::Cardinality::REQUIRED) {
515         ++old_required_properties;
516       }
517 
518       // A non-default term_match_type indicates that this property is meant to
519       // be indexed.
520       bool is_indexed_property =
521           old_property_config.string_indexing_config().term_match_type() !=
522           TermMatchType::UNKNOWN;
523       if (is_indexed_property) {
524         ++old_indexed_properties;
525       }
526 
527       auto new_property_name_and_config =
528           new_parsed_property_configs.property_config_map.find(
529               old_property_config.property_name());
530 
531       if (new_property_name_and_config ==
532           new_parsed_property_configs.property_config_map.end()) {
533         // Didn't find the old property
534         ICING_VLOG(1) << absl_ports::StrCat(
535             "Previously defined property type '", old_type_config.schema_type(),
536             ".", old_property_config.property_name(),
537             "' was not defined in new schema");
538         is_incompatible = true;
539         is_index_incompatible |= is_indexed_property;
540         continue;
541       }
542 
543       const PropertyConfigProto* new_property_config =
544           new_property_name_and_config->second;
545       if (!has_property_changed &&
546           !ArePropertiesEqual(old_property_config, *new_property_config)) {
547         // Finally found a property that changed.
548         has_property_changed = true;
549       }
550 
551       if (!IsPropertyCompatible(old_property_config, *new_property_config)) {
552         ICING_VLOG(1) << absl_ports::StrCat(
553             "Property '", old_type_config.schema_type(), ".",
554             old_property_config.property_name(), "' is incompatible.");
555         is_incompatible = true;
556       }
557 
558       // Any change in the indexed property requires a reindexing
559       if (!IsTermMatchTypeCompatible(
560               old_property_config.string_indexing_config(),
561               new_property_config->string_indexing_config()) ||
562           old_property_config.document_indexing_config()
563                   .index_nested_properties() !=
564               new_property_config->document_indexing_config()
565                   .index_nested_properties()) {
566         is_index_incompatible = true;
567       }
568     }
569 
570     // We can't have new properties that are REQUIRED since we won't know how
571     // to backfill the data, and the existing data will be invalid. We're
572     // guaranteed from our previous checks that all the old properties are also
573     // present in the new property config, so we can do a simple int comparison
574     // here to detect new required properties.
575     if (new_parsed_property_configs.num_required_properties >
576         old_required_properties) {
577       ICING_VLOG(1) << absl_ports::StrCat(
578           "New schema '", old_type_config.schema_type(),
579           "' has REQUIRED properties that are not "
580           "present in the previously defined schema");
581       is_incompatible = true;
582     }
583 
584     // If we've gained any new indexed properties, then the section ids may
585     // change. Since the section ids are stored in the index, we'll need to
586     // reindex everything.
587     if (new_parsed_property_configs.num_indexed_properties >
588         old_indexed_properties) {
589       ICING_VLOG(1) << absl_ports::StrCat(
590           "Set of indexed properties in schema type '",
591           old_type_config.schema_type(),
592           "' has  changed, required reindexing.");
593       is_index_incompatible = true;
594     }
595 
596     if (is_incompatible) {
597       AddIncompatibleChangeToDelta(schema_delta.schema_types_incompatible,
598                                    old_type_config, new_schema_dependency_map,
599                                    old_type_config_map, new_type_config_map);
600     }
601 
602     if (is_index_incompatible) {
603       AddIncompatibleChangeToDelta(schema_delta.schema_types_index_incompatible,
604                                    old_type_config, new_schema_dependency_map,
605                                    old_type_config_map, new_type_config_map);
606     }
607 
608     if (!is_incompatible && !is_index_incompatible && has_property_changed) {
609       schema_delta.schema_types_changed_fully_compatible.insert(
610           old_type_config.schema_type());
611     }
612 
613     // Lastly, remove this type from the map. We know that this type can't
614     // come up in future iterations through the old schema types because the old
615     // type config has unique types.
616     new_type_config_map.erase(old_type_config.schema_type());
617   }
618 
619   // Any types that are still present in the new_type_config_map are newly added
620   // types.
621   schema_delta.schema_types_new.reserve(new_type_config_map.size());
622   for (auto& kvp : new_type_config_map) {
623     schema_delta.schema_types_new.insert(std::move(kvp.first));
624   }
625 
626   return schema_delta;
627 }
628 
629 }  // namespace lib
630 }  // namespace icing
631