• 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 <algorithm>
18 #include <cstdint>
19 #include <queue>
20 #include <string>
21 #include <string_view>
22 #include <unordered_map>
23 #include <unordered_set>
24 #include <utility>
25 #include <vector>
26 
27 #include "icing/text_classifier/lib3/utils/base/status.h"
28 #include "icing/absl_ports/annotate.h"
29 #include "icing/absl_ports/canonical_errors.h"
30 #include "icing/absl_ports/str_cat.h"
31 #include "icing/absl_ports/str_join.h"
32 #include "icing/proto/schema.pb.h"
33 #include "icing/proto/term.pb.h"
34 #include "icing/util/logging.h"
35 #include "icing/util/status-macros.h"
36 
37 namespace icing {
38 namespace lib {
39 
40 namespace {
41 
ArePropertiesEqual(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)42 bool ArePropertiesEqual(const PropertyConfigProto& old_property,
43                         const PropertyConfigProto& new_property) {
44   return old_property.property_name() == new_property.property_name() &&
45          old_property.data_type() == new_property.data_type() &&
46          old_property.schema_type() == new_property.schema_type() &&
47          old_property.cardinality() == new_property.cardinality() &&
48          old_property.string_indexing_config().term_match_type() ==
49              new_property.string_indexing_config().term_match_type() &&
50          old_property.string_indexing_config().tokenizer_type() ==
51              new_property.string_indexing_config().tokenizer_type() &&
52          old_property.document_indexing_config().index_nested_properties() ==
53              new_property.document_indexing_config().index_nested_properties();
54 }
55 
IsCardinalityCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)56 bool IsCardinalityCompatible(const PropertyConfigProto& old_property,
57                              const PropertyConfigProto& new_property) {
58   if (old_property.cardinality() < new_property.cardinality()) {
59     // We allow a new, less restrictive cardinality (i.e. a REQUIRED field
60     // can become REPEATED or OPTIONAL, but not the other way around).
61     ICING_VLOG(1) << absl_ports::StrCat(
62         "Cardinality is more restrictive than before ",
63         PropertyConfigProto::Cardinality::Code_Name(old_property.cardinality()),
64         "->",
65         PropertyConfigProto::Cardinality::Code_Name(
66             new_property.cardinality()));
67     return false;
68   }
69   return true;
70 }
71 
IsDataTypeCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)72 bool IsDataTypeCompatible(const PropertyConfigProto& old_property,
73                           const PropertyConfigProto& new_property) {
74   if (old_property.data_type() != new_property.data_type()) {
75     // TODO(cassiewang): Maybe we can be a bit looser with this, e.g. we just
76     // string cast an int64_t to a string. But for now, we'll stick with
77     // simplistics.
78     ICING_VLOG(1) << absl_ports::StrCat(
79         "Data type ",
80         PropertyConfigProto::DataType::Code_Name(old_property.data_type()),
81         "->",
82         PropertyConfigProto::DataType::Code_Name(new_property.data_type()));
83     return false;
84   }
85   return true;
86 }
87 
IsSchemaTypeCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)88 bool IsSchemaTypeCompatible(const PropertyConfigProto& old_property,
89                             const PropertyConfigProto& new_property) {
90   if (old_property.schema_type() != new_property.schema_type()) {
91     ICING_VLOG(1) << absl_ports::StrCat("Schema type ",
92                                         old_property.schema_type(), "->",
93                                         new_property.schema_type());
94     return false;
95   }
96   return true;
97 }
98 
IsPropertyCompatible(const PropertyConfigProto & old_property,const PropertyConfigProto & new_property)99 bool IsPropertyCompatible(const PropertyConfigProto& old_property,
100                           const PropertyConfigProto& new_property) {
101   return IsDataTypeCompatible(old_property, new_property) &&
102          IsSchemaTypeCompatible(old_property, new_property) &&
103          IsCardinalityCompatible(old_property, new_property);
104 }
105 
IsTermMatchTypeCompatible(const StringIndexingConfig & old_indexed,const StringIndexingConfig & new_indexed)106 bool IsTermMatchTypeCompatible(const StringIndexingConfig& old_indexed,
107                                const StringIndexingConfig& new_indexed) {
108   return old_indexed.term_match_type() == new_indexed.term_match_type() &&
109          old_indexed.tokenizer_type() == new_indexed.tokenizer_type();
110 }
111 
IsIntegerNumericMatchTypeCompatible(const IntegerIndexingConfig & old_indexed,const IntegerIndexingConfig & new_indexed)112 bool IsIntegerNumericMatchTypeCompatible(
113     const IntegerIndexingConfig& old_indexed,
114     const IntegerIndexingConfig& new_indexed) {
115   return old_indexed.numeric_match_type() == new_indexed.numeric_match_type();
116 }
117 
AddIncompatibleChangeToDelta(std::unordered_set<std::string> & incompatible_delta,const SchemaTypeConfigProto & old_type_config,const SchemaUtil::DependentMap & new_schema_dependent_map,const SchemaUtil::TypeConfigMap & old_type_config_map,const SchemaUtil::TypeConfigMap & new_type_config_map)118 void AddIncompatibleChangeToDelta(
119     std::unordered_set<std::string>& incompatible_delta,
120     const SchemaTypeConfigProto& old_type_config,
121     const SchemaUtil::DependentMap& new_schema_dependent_map,
122     const SchemaUtil::TypeConfigMap& old_type_config_map,
123     const SchemaUtil::TypeConfigMap& new_type_config_map) {
124   // If this type is incompatible, then every type that depends on it might
125   // also be incompatible. Use the dependent map to mark those ones as
126   // incompatible too.
127   incompatible_delta.insert(old_type_config.schema_type());
128   auto dependent_types_itr =
129       new_schema_dependent_map.find(old_type_config.schema_type());
130   if (dependent_types_itr != new_schema_dependent_map.end()) {
131     for (const auto& [dependent_type, _] : dependent_types_itr->second) {
132       // The types from new_schema that depend on the current
133       // old_type_config may not present in old_schema.
134       // Those types will be listed at schema_delta.schema_types_new
135       // instead.
136       std::string dependent_type_str(dependent_type);
137       if (old_type_config_map.find(dependent_type_str) !=
138           old_type_config_map.end()) {
139         incompatible_delta.insert(std::move(dependent_type_str));
140       }
141     }
142   }
143 }
144 
145 // Returns if C1 <= C2 based on the following rule, where C1 and C2 are
146 // cardinalities that can be one of REPEATED, OPTIONAL, or REQUIRED.
147 //
148 // Rule: REQUIRED < OPTIONAL < REPEATED
CardinalityLessThanEq(PropertyConfigProto::Cardinality::Code C1,PropertyConfigProto::Cardinality::Code C2)149 bool CardinalityLessThanEq(PropertyConfigProto::Cardinality::Code C1,
150                            PropertyConfigProto::Cardinality::Code C2) {
151   if (C1 == C2) {
152     return true;
153   }
154   if (C1 == PropertyConfigProto::Cardinality::REQUIRED) {
155     return C2 == PropertyConfigProto::Cardinality::OPTIONAL ||
156            C2 == PropertyConfigProto::Cardinality::REPEATED;
157   }
158   if (C1 == PropertyConfigProto::Cardinality::OPTIONAL) {
159     return C2 == PropertyConfigProto::Cardinality::REPEATED;
160   }
161   return false;
162 }
163 
164 }  // namespace
165 
CalculateTransitiveNestedTypeRelations(const SchemaUtil::DependentMap & direct_nested_types_map,const std::unordered_set<std::string_view> & joinable_types,std::string_view type,bool path_contains_joinable_property,SchemaUtil::DependentMap * expanded_nested_types_map,std::unordered_map<std::string_view,bool> && pending_expansion_paths_indexable,std::unordered_set<std::string_view> * sink_types)166 libtextclassifier3::Status CalculateTransitiveNestedTypeRelations(
167     const SchemaUtil::DependentMap& direct_nested_types_map,
168     const std::unordered_set<std::string_view>& joinable_types,
169     std::string_view type, bool path_contains_joinable_property,
170     SchemaUtil::DependentMap* expanded_nested_types_map,
171     std::unordered_map<std::string_view, bool>&&
172         pending_expansion_paths_indexable,
173     std::unordered_set<std::string_view>* sink_types) {
174   // TODO(b/280698121): Implement optimizations to this code to avoid reentering
175   // a node after it's already been expanded.
176 
177   auto itr = direct_nested_types_map.find(type);
178   if (itr == direct_nested_types_map.end()) {
179     // It's a sink node. Just return.
180     sink_types->insert(type);
181     return libtextclassifier3::Status::OK;
182   }
183   std::unordered_map<std::string_view, std::vector<const PropertyConfigProto*>>
184       expanded_relations;
185 
186   // Add all of the adjacent outgoing relations.
187   expanded_relations.reserve(itr->second.size());
188   expanded_relations.insert(itr->second.begin(), itr->second.end());
189 
190   // Iterate through each adjacent outgoing relation and add their indirect
191   // outgoing relations.
192   for (const auto& [adjacent_type, adjacent_property_protos] : itr->second) {
193     // Make a copy of pending_expansion_paths_indexable for every iteration.
194     std::unordered_map<std::string_view, bool> pending_expansion_paths_copy(
195         pending_expansion_paths_indexable);
196 
197     // 1. Check the nested indexable config of the edge (type -> adjacent_type),
198     //    and the joinable config of the current path up to adjacent_type.
199     //
200     // The nested indexable config is true if any of the PropertyConfigProtos
201     // representing the connecting edge has index_nested_properties=true.
202     bool is_edge_nested_indexable = std::any_of(
203         adjacent_property_protos.begin(), adjacent_property_protos.end(),
204         [](const PropertyConfigProto* property_config) {
205           return property_config->document_indexing_config()
206               .index_nested_properties();
207         });
208     // TODO(b/265304217): change this once we add joinable_properties_list.
209     // Check if addition of the new edge (type->adjacent_type) makes the path
210     // joinable.
211     bool new_path_contains_joinable_property =
212         joinable_types.count(type) > 0 || path_contains_joinable_property;
213     // Set is_nested_indexable field for the current edge
214     pending_expansion_paths_copy[type] = is_edge_nested_indexable;
215 
216     // If is_edge_nested_indexable=false, then all paths to adjacent_type
217     // currently in the pending_expansions map are also not nested indexable.
218     if (!is_edge_nested_indexable) {
219       for (auto& pending_expansion : pending_expansion_paths_copy) {
220         pending_expansion.second = false;
221       }
222     }
223 
224     // 2. Check if we're in the middle of expanding this type - IOW
225     // there's a cycle!
226     //
227     // This cycle is not allowed if either:
228     //  1. The cycle starting at adjacent_type is nested indexable, OR
229     //  2. The current path contains a joinable property.
230     auto adjacent_itr = pending_expansion_paths_copy.find(adjacent_type);
231     if (adjacent_itr != pending_expansion_paths_copy.end()) {
232       if (adjacent_itr->second || new_path_contains_joinable_property) {
233         return absl_ports::InvalidArgumentError(absl_ports::StrCat(
234             "Invalid cycle detected in type configs. '", type,
235             "' references itself and is nested-indexable or nested-joinable."));
236       }
237       // The cycle is allowed and there's no need to keep iterating the loop.
238       // Move on to the next adjacent value.
239       continue;
240     }
241 
242     // 3. Expand this type as needed.
243     ICING_RETURN_IF_ERROR(CalculateTransitiveNestedTypeRelations(
244         direct_nested_types_map, joinable_types, adjacent_type,
245         new_path_contains_joinable_property, expanded_nested_types_map,
246         std::move(pending_expansion_paths_copy), sink_types));
247     if (sink_types->count(adjacent_type) > 0) {
248       // "adjacent" is a sink node. Just skip to the next.
249       continue;
250     }
251 
252     // 4. "adjacent" has been fully expanded. Add all of its transitive
253     // outgoing relations to this type's transitive outgoing relations.
254     auto adjacent_expanded_itr = expanded_nested_types_map->find(adjacent_type);
255     expanded_relations.reserve(expanded_relations.size() +
256                                adjacent_expanded_itr->second.size());
257     for (const auto& [transitive_reachable, _] :
258          adjacent_expanded_itr->second) {
259       // Insert a transitive reachable node `transitive_reachable` for `type` if
260       // it wasn't previously reachable.
261       // Since there is no direct edge between `type` and `transitive_reachable`
262       // we insert an empty vector into the dependent map.
263       expanded_relations.insert({transitive_reachable, {}});
264     }
265   }
266   for (const auto& kvp : expanded_relations) {
267     expanded_nested_types_map->operator[](type).insert(kvp);
268   }
269   return libtextclassifier3::Status::OK;
270 }
271 
272 template <typename T>
CalculateAcyclicTransitiveRelations(const SchemaUtil::TypeRelationMap<T> & direct_relation_map,std::string_view type,SchemaUtil::TypeRelationMap<T> * expanded_relation_map,std::unordered_set<std::string_view> * pending_expansions,std::unordered_set<std::string_view> * sink_types)273 libtextclassifier3::Status CalculateAcyclicTransitiveRelations(
274     const SchemaUtil::TypeRelationMap<T>& direct_relation_map,
275     std::string_view type,
276     SchemaUtil::TypeRelationMap<T>* expanded_relation_map,
277     std::unordered_set<std::string_view>* pending_expansions,
278     std::unordered_set<std::string_view>* sink_types) {
279   auto expanded_itr = expanded_relation_map->find(type);
280   if (expanded_itr != expanded_relation_map->end()) {
281     // We've already expanded this type. Just return.
282     return libtextclassifier3::Status::OK;
283   }
284   auto itr = direct_relation_map.find(type);
285   if (itr == direct_relation_map.end()) {
286     // It's a sink node. Just return.
287     sink_types->insert(type);
288     return libtextclassifier3::Status::OK;
289   }
290   pending_expansions->insert(type);
291   std::unordered_map<std::string_view, T> expanded_relations;
292 
293   // Add all of the adjacent outgoing relations.
294   expanded_relations.reserve(itr->second.size());
295   expanded_relations.insert(itr->second.begin(), itr->second.end());
296 
297   // Iterate through each adjacent outgoing relation and add their indirect
298   // outgoing relations.
299   for (const auto& [adjacent, _] : itr->second) {
300     // 1. Check if we're in the middle of expanding this type - IOW there's a
301     // cycle!
302     if (pending_expansions->count(adjacent) > 0) {
303       return absl_ports::InvalidArgumentError(
304           absl_ports::StrCat("Invalid cycle detected in type configs. '", type,
305                              "' references or inherits from itself."));
306     }
307 
308     // 2. Expand this type as needed.
309     ICING_RETURN_IF_ERROR(CalculateAcyclicTransitiveRelations(
310         direct_relation_map, adjacent, expanded_relation_map,
311         pending_expansions, sink_types));
312     if (sink_types->count(adjacent) > 0) {
313       // "adjacent" is a sink node. Just skip to the next.
314       continue;
315     }
316 
317     // 3. "adjacent" has been fully expanded. Add all of its transitive outgoing
318     // relations to this type's transitive outgoing relations.
319     auto adjacent_expanded_itr = expanded_relation_map->find(adjacent);
320     expanded_relations.reserve(expanded_relations.size() +
321                                adjacent_expanded_itr->second.size());
322     for (const auto& [transitive_reachable, _] :
323          adjacent_expanded_itr->second) {
324       // Insert a transitive reachable node `transitive_reachable` for `type`.
325       // Also since there is no direct edge between `type` and
326       // `transitive_reachable`, the direct edge is initialized by default.
327       expanded_relations.insert({transitive_reachable, T()});
328     }
329   }
330   expanded_relation_map->insert({type, std::move(expanded_relations)});
331   pending_expansions->erase(type);
332   return libtextclassifier3::Status::OK;
333 }
334 
335 // Calculate and return the expanded nested-type map from
336 // direct_nested_type_map. This expands the direct_nested_type_map to also
337 // include indirect nested-type relations.
338 //
339 // Ex. Suppose we have the following relations in direct_nested_type_map.
340 //
341 // C -> B (Schema type B has a document property of type C)
342 // B -> A (Schema type A has a document property of type B)
343 //
344 // Then, this function would expand the map by adding C -> A to the map.
345 libtextclassifier3::StatusOr<SchemaUtil::DependentMap>
CalculateTransitiveNestedTypeRelations(const SchemaUtil::DependentMap & direct_nested_type_map,const std::unordered_set<std::string_view> & joinable_types,bool allow_circular_schema_definitions)346 CalculateTransitiveNestedTypeRelations(
347     const SchemaUtil::DependentMap& direct_nested_type_map,
348     const std::unordered_set<std::string_view>& joinable_types,
349     bool allow_circular_schema_definitions) {
350   SchemaUtil::DependentMap expanded_nested_type_map;
351   // Types that have no outgoing relations.
352   std::unordered_set<std::string_view> sink_types;
353 
354   if (allow_circular_schema_definitions) {
355     // Map of nodes that are pending expansion -> whether the path from each key
356     // node to the 'current' node is nested_indexable.
357     // A copy of this map is made for each new node that we expand.
358     std::unordered_map<std::string_view, bool>
359         pending_expansion_paths_indexable;
360     for (const auto& kvp : direct_nested_type_map) {
361       ICING_RETURN_IF_ERROR(CalculateTransitiveNestedTypeRelations(
362           direct_nested_type_map, joinable_types, kvp.first,
363           /*path_contains_joinable_property=*/false, &expanded_nested_type_map,
364           std::unordered_map<std::string_view, bool>(
365               pending_expansion_paths_indexable),
366           &sink_types));
367     }
368   } else {
369     // If allow_circular_schema_definitions is false, then fallback to the old
370     // way of detecting cycles.
371     // Types that we are expanding.
372     std::unordered_set<std::string_view> pending_expansions;
373     for (const auto& kvp : direct_nested_type_map) {
374       ICING_RETURN_IF_ERROR(CalculateAcyclicTransitiveRelations(
375           direct_nested_type_map, kvp.first, &expanded_nested_type_map,
376           &pending_expansions, &sink_types));
377     }
378   }
379   return expanded_nested_type_map;
380 }
381 
382 // Calculate and return the expanded inheritance map from
383 // direct_nested_type_map. This expands the direct_inheritance_map to also
384 // include indirect inheritance relations.
385 //
386 // Ex. Suppose we have the following relations in direct_inheritance_map.
387 //
388 // C -> B (Schema type C is B's parent_type )
389 // B -> A (Schema type B is A's parent_type)
390 //
391 // Then, this function would expand the map by adding C -> A to the map.
392 libtextclassifier3::StatusOr<SchemaUtil::InheritanceMap>
CalculateTransitiveInheritanceRelations(const SchemaUtil::InheritanceMap & direct_inheritance_map)393 CalculateTransitiveInheritanceRelations(
394     const SchemaUtil::InheritanceMap& direct_inheritance_map) {
395   SchemaUtil::InheritanceMap expanded_inheritance_map;
396 
397   // Types that we are expanding.
398   std::unordered_set<std::string_view> pending_expansions;
399 
400   // Types that have no outgoing relation.
401   std::unordered_set<std::string_view> sink_types;
402   for (const auto& kvp : direct_inheritance_map) {
403     ICING_RETURN_IF_ERROR(CalculateAcyclicTransitiveRelations(
404         direct_inheritance_map, kvp.first, &expanded_inheritance_map,
405         &pending_expansions, &sink_types));
406   }
407   return expanded_inheritance_map;
408 }
409 
410 // Builds a transitive dependent map. Types with no dependents will not be
411 // present in the map as keys.
412 //
413 // Ex. Suppose we have a schema with four types A, B, C, D. A has a property of
414 // type B and B has a property of type C. C and D only have non-document
415 // properties.
416 //
417 // The transitive dependent map for this schema would be:
418 // C -> A, B (both A and B depend on C)
419 // B -> A (A depends on B)
420 //
421 // A and D will not be present in the map as keys because no type depends on
422 // them.
423 //
424 // RETURNS:
425 //   On success, a transitive dependent map of all types in the schema.
426 //   INVALID_ARGUMENT if the schema contains a cycle or an undefined type.
427 //   ALREADY_EXISTS if a schema type is specified more than once in the schema
428 libtextclassifier3::StatusOr<SchemaUtil::DependentMap>
BuildTransitiveDependentGraph(const SchemaProto & schema,bool allow_circular_schema_definitions)429 BuildTransitiveDependentGraph(const SchemaProto& schema,
430                               bool allow_circular_schema_definitions) {
431   // We expand the nested-type dependent map and inheritance map differently
432   // when calculating transitive relations. These two types of relations also
433   // should not be transitive so we keep these as separate maps.
434   //
435   // e.g. For schema type A, B and C, B depends on A through inheritance, and
436   // C depends on B by having a property with type B, we will have the two
437   // relations {A, B} and {B, C} in the dependent map, but will not have {A, C}
438   // in the map.
439   SchemaUtil::DependentMap direct_nested_type_map;
440   SchemaUtil::InheritanceMap direct_inheritance_map;
441 
442   // Set of schema types that have at least one joinable property.
443   std::unordered_set<std::string_view> joinable_types;
444 
445   // Add all first-order dependents.
446   std::unordered_set<std::string_view> known_types;
447   std::unordered_set<std::string_view> unknown_types;
448   for (const auto& type_config : schema.types()) {
449     std::string_view schema_type(type_config.schema_type());
450     if (known_types.count(schema_type) > 0) {
451       return absl_ports::AlreadyExistsError(absl_ports::StrCat(
452           "Field 'schema_type' '", schema_type, "' is already defined"));
453     }
454     known_types.insert(schema_type);
455     unknown_types.erase(schema_type);
456     // Insert inheritance relations into the inheritance map.
457     for (std::string_view parent_schema_type : type_config.parent_types()) {
458       if (known_types.count(parent_schema_type) == 0) {
459         unknown_types.insert(parent_schema_type);
460       }
461       direct_inheritance_map[parent_schema_type][schema_type] = true;
462     }
463     for (const auto& property_config : type_config.properties()) {
464       if (property_config.joinable_config().value_type() !=
465           JoinableConfig::ValueType::NONE) {
466         joinable_types.insert(schema_type);
467       }
468       // Insert nested-type relations into the nested-type map.
469       if (property_config.data_type() ==
470           PropertyConfigProto::DataType::DOCUMENT) {
471         // Need to know what schema_type these Document properties should be
472         // validated against
473         std::string_view property_schema_type(property_config.schema_type());
474         if (known_types.count(property_schema_type) == 0) {
475           unknown_types.insert(property_schema_type);
476         }
477         direct_nested_type_map[property_schema_type][schema_type].push_back(
478             &property_config);
479       }
480     }
481   }
482   if (!unknown_types.empty()) {
483     return absl_ports::InvalidArgumentError(absl_ports::StrCat(
484         "Undefined 'schema_type's: ", absl_ports::StrJoin(unknown_types, ",")));
485   }
486 
487   // Merge two expanded maps into a single dependent_map, without making
488   // inheritance and nested-type relations transitive.
489   ICING_ASSIGN_OR_RETURN(SchemaUtil::DependentMap merged_dependent_map,
490                          CalculateTransitiveNestedTypeRelations(
491                              direct_nested_type_map, joinable_types,
492                              allow_circular_schema_definitions));
493   ICING_ASSIGN_OR_RETURN(
494       SchemaUtil::InheritanceMap expanded_inheritance_map,
495       CalculateTransitiveInheritanceRelations(direct_inheritance_map));
496   for (const auto& [parent_type, inheritance_relation] :
497        expanded_inheritance_map) {
498     // Insert the parent_type into the dependent map if it is not present
499     // already.
500     merged_dependent_map.insert({parent_type, {}});
501     merged_dependent_map[parent_type].reserve(inheritance_relation.size());
502     for (const auto& [child_type, _] : inheritance_relation) {
503       // Insert the child_type into parent_type's dependent map if it's not
504       // present already, in which case the value will be an empty vector.
505       merged_dependent_map[parent_type].insert({child_type, {}});
506     }
507   }
508   return merged_dependent_map;
509 }
510 
511 libtextclassifier3::StatusOr<SchemaUtil::InheritanceMap>
BuildTransitiveInheritanceGraph(const SchemaProto & schema)512 SchemaUtil::BuildTransitiveInheritanceGraph(const SchemaProto& schema) {
513   SchemaUtil::InheritanceMap direct_inheritance_map;
514   for (const auto& type_config : schema.types()) {
515     for (std::string_view parent_schema_type : type_config.parent_types()) {
516       direct_inheritance_map[parent_schema_type][type_config.schema_type()] =
517           true;
518     }
519   }
520   return CalculateTransitiveInheritanceRelations(direct_inheritance_map);
521 }
522 
Validate(const SchemaProto & schema,bool allow_circular_schema_definitions)523 libtextclassifier3::StatusOr<SchemaUtil::DependentMap> SchemaUtil::Validate(
524     const SchemaProto& schema, bool allow_circular_schema_definitions) {
525   // 1. Build the dependent map. This will detect any cycles, non-existent or
526   // duplicate types in the schema.
527   ICING_ASSIGN_OR_RETURN(
528       SchemaUtil::DependentMap dependent_map,
529       BuildTransitiveDependentGraph(schema, allow_circular_schema_definitions));
530 
531   // Tracks PropertyConfigs within a SchemaTypeConfig that we've validated
532   // already.
533   std::unordered_set<std::string_view> known_property_names;
534 
535   // Tracks PropertyConfigs containing joinable properties.
536   std::unordered_set<std::string_view> schema_types_with_joinable_property;
537 
538   // 2. Validate the properties of each type.
539   for (const auto& type_config : schema.types()) {
540     std::string_view schema_type(type_config.schema_type());
541     ICING_RETURN_IF_ERROR(ValidateSchemaType(schema_type));
542 
543     // We only care about properties being unique within one type_config
544     known_property_names.clear();
545 
546     for (const auto& property_config : type_config.properties()) {
547       std::string_view property_name(property_config.property_name());
548       ICING_RETURN_IF_ERROR(ValidatePropertyName(property_name, schema_type));
549 
550       // Property names must be unique
551       if (!known_property_names.insert(property_name).second) {
552         return absl_ports::AlreadyExistsError(absl_ports::StrCat(
553             "Field 'property_name' '", property_name,
554             "' is already defined for schema '", schema_type, "'"));
555       }
556 
557       auto data_type = property_config.data_type();
558       ICING_RETURN_IF_ERROR(
559           ValidateDataType(data_type, schema_type, property_name));
560 
561       if (data_type == PropertyConfigProto::DataType::DOCUMENT) {
562         // Need to know what schema_type these Document properties should be
563         // validated against
564         std::string_view property_schema_type(property_config.schema_type());
565         libtextclassifier3::Status validated_status =
566             ValidateSchemaType(property_schema_type);
567         if (!validated_status.ok()) {
568           return absl_ports::Annotate(
569               validated_status,
570               absl_ports::StrCat("Field 'schema_type' is required for DOCUMENT "
571                                  "data_types in schema property '",
572                                  schema_type, ".", property_name, "'"));
573         }
574       }
575 
576       ICING_RETURN_IF_ERROR(ValidateCardinality(property_config.cardinality(),
577                                                 schema_type, property_name));
578 
579       if (data_type == PropertyConfigProto::DataType::STRING) {
580         ICING_RETURN_IF_ERROR(ValidateStringIndexingConfig(
581             property_config.string_indexing_config(), data_type, schema_type,
582             property_name));
583       }
584 
585       ICING_RETURN_IF_ERROR(ValidateJoinableConfig(
586           property_config.joinable_config(), data_type,
587           property_config.cardinality(), schema_type, property_name));
588       if (property_config.joinable_config().value_type() !=
589           JoinableConfig::ValueType::NONE) {
590         schema_types_with_joinable_property.insert(schema_type);
591       }
592     }
593   }
594 
595   // BFS traverse the dependent graph to make sure that no nested levels
596   // (properties with DOCUMENT data type) have REPEATED cardinality while
597   // depending on schema types with joinable property.
598   std::queue<std::string_view> frontier;
599   for (const auto& schema_type : schema_types_with_joinable_property) {
600     frontier.push(schema_type);
601   }
602   std::unordered_set<std::string_view> traversed =
603       std::move(schema_types_with_joinable_property);
604   while (!frontier.empty()) {
605     std::string_view schema_type = frontier.front();
606     frontier.pop();
607 
608     const auto it = dependent_map.find(schema_type);
609     if (it == dependent_map.end()) {
610       continue;
611     }
612 
613     // Check every type that has a property of type schema_type.
614     for (const auto& [next_schema_type, property_configs] : it->second) {
615       // Check all properties in "next_schema_type" that are of type
616       // "schema_type".
617       for (const PropertyConfigProto* property_config : property_configs) {
618         if (property_config != nullptr &&
619             property_config->cardinality() ==
620                 PropertyConfigProto::Cardinality::REPEATED) {
621           return absl_ports::InvalidArgumentError(absl_ports::StrCat(
622               "Schema type '", next_schema_type,
623               "' cannot have REPEATED nested document property '",
624               property_config->property_name(),
625               "' while connecting to some joinable properties"));
626         }
627       }
628 
629       if (traversed.count(next_schema_type) == 0) {
630         traversed.insert(next_schema_type);
631         frontier.push(next_schema_type);
632       }
633     }
634   }
635 
636   // Verify that every child type's property set has included all compatible
637   // properties from parent types.
638   ICING_RETURN_IF_ERROR(ValidateInheritedProperties(schema));
639   return dependent_map;
640 }
641 
ValidateSchemaType(std::string_view schema_type)642 libtextclassifier3::Status SchemaUtil::ValidateSchemaType(
643     std::string_view schema_type) {
644   // Require a schema_type
645   if (schema_type.empty()) {
646     return absl_ports::InvalidArgumentError(
647         "Field 'schema_type' cannot be empty.");
648   }
649 
650   return libtextclassifier3::Status::OK;
651 }
652 
ValidatePropertyName(std::string_view property_name,std::string_view schema_type)653 libtextclassifier3::Status SchemaUtil::ValidatePropertyName(
654     std::string_view property_name, std::string_view schema_type) {
655   // Require a property_name
656   if (property_name.empty()) {
657     return absl_ports::InvalidArgumentError(
658         absl_ports::StrCat("Field 'property_name' for schema '", schema_type,
659                            "' cannot be empty."));
660   }
661 
662   // Only support alphanumeric values.
663   for (char c : property_name) {
664     if (!std::isalnum(c)) {
665       return absl_ports::InvalidArgumentError(
666           absl_ports::StrCat("Field 'property_name' '", property_name,
667                              "' can only contain alphanumeric characters."));
668     }
669   }
670 
671   return libtextclassifier3::Status::OK;
672 }
673 
ValidateDataType(PropertyConfigProto::DataType::Code data_type,std::string_view schema_type,std::string_view property_name)674 libtextclassifier3::Status SchemaUtil::ValidateDataType(
675     PropertyConfigProto::DataType::Code data_type, std::string_view schema_type,
676     std::string_view property_name) {
677   // UNKNOWN is the default enum value and should only be used for backwards
678   // compatibility
679   if (data_type == PropertyConfigProto::DataType::UNKNOWN) {
680     return absl_ports::InvalidArgumentError(absl_ports::StrCat(
681         "Field 'data_type' cannot be UNKNOWN for schema property '",
682         schema_type, ".", property_name, "'"));
683   }
684 
685   return libtextclassifier3::Status::OK;
686 }
687 
ValidateCardinality(PropertyConfigProto::Cardinality::Code cardinality,std::string_view schema_type,std::string_view property_name)688 libtextclassifier3::Status SchemaUtil::ValidateCardinality(
689     PropertyConfigProto::Cardinality::Code cardinality,
690     std::string_view schema_type, std::string_view property_name) {
691   // UNKNOWN is the default enum value and should only be used for backwards
692   // compatibility
693   if (cardinality == PropertyConfigProto::Cardinality::UNKNOWN) {
694     return absl_ports::InvalidArgumentError(absl_ports::StrCat(
695         "Field 'cardinality' cannot be UNKNOWN for schema property '",
696         schema_type, ".", property_name, "'"));
697   }
698 
699   return libtextclassifier3::Status::OK;
700 }
701 
ValidateStringIndexingConfig(const StringIndexingConfig & config,PropertyConfigProto::DataType::Code data_type,std::string_view schema_type,std::string_view property_name)702 libtextclassifier3::Status SchemaUtil::ValidateStringIndexingConfig(
703     const StringIndexingConfig& config,
704     PropertyConfigProto::DataType::Code data_type, std::string_view schema_type,
705     std::string_view property_name) {
706   if (config.term_match_type() == TermMatchType::UNKNOWN &&
707       config.tokenizer_type() != StringIndexingConfig::TokenizerType::NONE) {
708     // They set a tokenizer type, but no term match type.
709     return absl_ports::InvalidArgumentError(absl_ports::StrCat(
710         "Indexed string property '", schema_type, ".", property_name,
711         "' cannot have a term match type UNKNOWN"));
712   }
713 
714   if (config.term_match_type() != TermMatchType::UNKNOWN &&
715       config.tokenizer_type() == StringIndexingConfig::TokenizerType::NONE) {
716     // They set a term match type, but no tokenizer type
717     return absl_ports::InvalidArgumentError(
718         absl_ports::StrCat("Indexed string property '", property_name,
719                            "' cannot have a tokenizer type of NONE"));
720   }
721 
722   return libtextclassifier3::Status::OK;
723 }
724 
ValidateJoinableConfig(const JoinableConfig & config,PropertyConfigProto::DataType::Code data_type,PropertyConfigProto::Cardinality::Code cardinality,std::string_view schema_type,std::string_view property_name)725 libtextclassifier3::Status SchemaUtil::ValidateJoinableConfig(
726     const JoinableConfig& config, PropertyConfigProto::DataType::Code data_type,
727     PropertyConfigProto::Cardinality::Code cardinality,
728     std::string_view schema_type, std::string_view property_name) {
729   if (config.value_type() == JoinableConfig::ValueType::QUALIFIED_ID) {
730     if (data_type != PropertyConfigProto::DataType::STRING) {
731       return absl_ports::InvalidArgumentError(
732           absl_ports::StrCat("Qualified id joinable property '", property_name,
733                              "' is required to have STRING data type"));
734     }
735 
736     if (cardinality == PropertyConfigProto::Cardinality::REPEATED) {
737       return absl_ports::InvalidArgumentError(
738           absl_ports::StrCat("Qualified id joinable property '", property_name,
739                              "' cannot have REPEATED cardinality"));
740     }
741   }
742 
743   if (config.propagate_delete() &&
744       config.value_type() != JoinableConfig::ValueType::QUALIFIED_ID) {
745     return absl_ports::InvalidArgumentError(
746         absl_ports::StrCat("Field 'property_name' '", property_name,
747                            "' is required to have QUALIFIED_ID joinable "
748                            "value type with delete propagation enabled"));
749   }
750 
751   return libtextclassifier3::Status::OK;
752 }
753 
IsIndexedProperty(const PropertyConfigProto & property_config)754 /* static */ bool SchemaUtil::IsIndexedProperty(
755     const PropertyConfigProto& property_config) {
756   switch (property_config.data_type()) {
757     case PropertyConfigProto::DataType::STRING:
758       return property_config.string_indexing_config().term_match_type() !=
759                  TermMatchType::UNKNOWN &&
760              property_config.string_indexing_config().tokenizer_type() !=
761                  StringIndexingConfig::TokenizerType::NONE;
762     case PropertyConfigProto::DataType::INT64:
763       return property_config.integer_indexing_config().numeric_match_type() !=
764              IntegerIndexingConfig::NumericMatchType::UNKNOWN;
765     case PropertyConfigProto::DataType::UNKNOWN:
766     case PropertyConfigProto::DataType::DOUBLE:
767     case PropertyConfigProto::DataType::BOOLEAN:
768     case PropertyConfigProto::DataType::BYTES:
769     case PropertyConfigProto::DataType::DOCUMENT:
770       return false;
771   }
772 }
773 
IsParent(const SchemaUtil::InheritanceMap & inheritance_map,std::string_view parent_type,std::string_view child_type)774 bool SchemaUtil::IsParent(const SchemaUtil::InheritanceMap& inheritance_map,
775                           std::string_view parent_type,
776                           std::string_view child_type) {
777   auto iter = inheritance_map.find(parent_type);
778   if (iter == inheritance_map.end()) {
779     return false;
780   }
781   return iter->second.count(child_type) > 0;
782 }
783 
IsInheritedPropertyCompatible(const SchemaUtil::InheritanceMap & inheritance_map,const PropertyConfigProto & child_property_config,const PropertyConfigProto & parent_property_config)784 bool SchemaUtil::IsInheritedPropertyCompatible(
785     const SchemaUtil::InheritanceMap& inheritance_map,
786     const PropertyConfigProto& child_property_config,
787     const PropertyConfigProto& parent_property_config) {
788   // Check if child_property_config->cardinality() <=
789   // parent_property_config->cardinality().
790   // Subtype may require a stricter cardinality, but cannot loosen cardinality
791   // requirements.
792   if (!CardinalityLessThanEq(child_property_config.cardinality(),
793                              parent_property_config.cardinality())) {
794     return false;
795   }
796 
797   // Now we can assume T1 and T2 are not nullptr, and cardinality check passes.
798   if (child_property_config.data_type() !=
799           PropertyConfigProto::DataType::DOCUMENT ||
800       parent_property_config.data_type() !=
801           PropertyConfigProto::DataType::DOCUMENT) {
802     return child_property_config.data_type() ==
803            parent_property_config.data_type();
804   }
805 
806   // Now we can assume T1 and T2 are both document type.
807   return child_property_config.schema_type() ==
808              parent_property_config.schema_type() ||
809          IsParent(inheritance_map, parent_property_config.schema_type(),
810                   child_property_config.schema_type());
811 }
812 
ValidateInheritedProperties(const SchemaProto & schema)813 libtextclassifier3::Status SchemaUtil::ValidateInheritedProperties(
814     const SchemaProto& schema) {
815   // Create a inheritance map
816   ICING_ASSIGN_OR_RETURN(SchemaUtil::InheritanceMap inheritance_map,
817                          BuildTransitiveInheritanceGraph(schema));
818 
819   // Create a map that maps from type name to property names, and then from
820   // property names to PropertyConfigProto.
821   std::unordered_map<
822       std::string, std::unordered_map<std::string, const PropertyConfigProto*>>
823       property_map;
824   for (const SchemaTypeConfigProto& type_config : schema.types()) {
825     // Skipping building entries for types without any child or parent, since
826     // such entry will never be used.
827     if (type_config.parent_types().empty() &&
828         inheritance_map.count(type_config.schema_type()) == 0) {
829       continue;
830     }
831     auto& curr_property_map = property_map[type_config.schema_type()];
832     for (const PropertyConfigProto& property_config :
833          type_config.properties()) {
834       curr_property_map[property_config.property_name()] = &property_config;
835     }
836   }
837 
838   // Validate child properties.
839   for (const SchemaTypeConfigProto& type_config : schema.types()) {
840     const std::string& child_type_name = type_config.schema_type();
841     auto& child_property_map = property_map[child_type_name];
842 
843     for (const std::string& parent_type_name : type_config.parent_types()) {
844       auto& parent_property_map = property_map[parent_type_name];
845 
846       for (const auto& [property_name, parent_property_config] :
847            parent_property_map) {
848         auto child_property_iter = child_property_map.find(property_name);
849         if (child_property_iter == child_property_map.end()) {
850           return absl_ports::InvalidArgumentError(absl_ports::StrCat(
851               "Property ", property_name, " is not present in child type ",
852               child_type_name, ", but it is defined in the parent type ",
853               parent_type_name, "."));
854         }
855         if (!IsInheritedPropertyCompatible(inheritance_map,
856                                            *child_property_iter->second,
857                                            *parent_property_config)) {
858           return absl_ports::InvalidArgumentError(absl_ports::StrCat(
859               "Property ", property_name, " from child type ", child_type_name,
860               " is not compatible to the parent type ", parent_type_name, "."));
861         }
862       }
863     }
864   }
865   return libtextclassifier3::Status::OK;
866 }
867 
BuildTypeConfigMap(const SchemaProto & schema,SchemaUtil::TypeConfigMap * type_config_map)868 void SchemaUtil::BuildTypeConfigMap(
869     const SchemaProto& schema, SchemaUtil::TypeConfigMap* type_config_map) {
870   type_config_map->clear();
871   for (const SchemaTypeConfigProto& type_config : schema.types()) {
872     type_config_map->emplace(type_config.schema_type(), type_config);
873   }
874 }
875 
ParsePropertyConfigs(const SchemaTypeConfigProto & type_config)876 SchemaUtil::ParsedPropertyConfigs SchemaUtil::ParsePropertyConfigs(
877     const SchemaTypeConfigProto& type_config) {
878   ParsedPropertyConfigs parsed_property_configs;
879 
880   // TODO(cassiewang): consider caching property_config_map for some properties,
881   // e.g. using LRU cache. Or changing schema.proto to use go/protomap.
882   for (const PropertyConfigProto& property_config : type_config.properties()) {
883     parsed_property_configs.property_config_map.emplace(
884         property_config.property_name(), &property_config);
885     if (property_config.cardinality() ==
886         PropertyConfigProto::Cardinality::REQUIRED) {
887       ++parsed_property_configs.num_required_properties;
888     }
889 
890     // A non-default term_match_type indicates that this property is meant to be
891     // indexed.
892     if (IsIndexedProperty(property_config)) {
893       ++parsed_property_configs.num_indexed_properties;
894     }
895 
896     // A non-default value_type indicates that this property is meant to be
897     // joinable.
898     if (property_config.joinable_config().value_type() !=
899         JoinableConfig::ValueType::NONE) {
900       ++parsed_property_configs.num_joinable_properties;
901     }
902   }
903 
904   return parsed_property_configs;
905 }
906 
ComputeCompatibilityDelta(const SchemaProto & old_schema,const SchemaProto & new_schema,const DependentMap & new_schema_dependent_map)907 const SchemaUtil::SchemaDelta SchemaUtil::ComputeCompatibilityDelta(
908     const SchemaProto& old_schema, const SchemaProto& new_schema,
909     const DependentMap& new_schema_dependent_map) {
910   SchemaDelta schema_delta;
911 
912   TypeConfigMap old_type_config_map, new_type_config_map;
913   BuildTypeConfigMap(old_schema, &old_type_config_map);
914   BuildTypeConfigMap(new_schema, &new_type_config_map);
915 
916   // Iterate through and check each field of the old schema
917   for (const auto& old_type_config : old_schema.types()) {
918     auto new_schema_type_and_config =
919         new_type_config_map.find(old_type_config.schema_type());
920 
921     if (new_schema_type_and_config == new_type_config_map.end()) {
922       // Didn't find the old schema type in the new schema, all the old
923       // documents of this schema type are invalid without the schema
924       ICING_VLOG(1) << absl_ports::StrCat("Previously defined schema type '",
925                                           old_type_config.schema_type(),
926                                           "' was not defined in new schema");
927       schema_delta.schema_types_deleted.insert(old_type_config.schema_type());
928       continue;
929     }
930 
931     ParsedPropertyConfigs new_parsed_property_configs =
932         ParsePropertyConfigs(new_schema_type_and_config->second);
933 
934     // We only need to check the old, existing properties to see if they're
935     // compatible since we'll have old data that may be invalidated or need to
936     // be reindexed.
937     int32_t old_required_properties = 0;
938     int32_t old_indexed_properties = 0;
939     int32_t old_joinable_properties = 0;
940 
941     // If there is a different number of properties, then there must have been a
942     // change.
943     bool has_property_changed =
944         old_type_config.properties_size() !=
945         new_schema_type_and_config->second.properties_size();
946     bool is_incompatible = false;
947     bool is_index_incompatible = false;
948     bool is_join_incompatible = false;
949     for (const auto& old_property_config : old_type_config.properties()) {
950       if (old_property_config.cardinality() ==
951           PropertyConfigProto::Cardinality::REQUIRED) {
952         ++old_required_properties;
953       }
954 
955       // A non-default term_match_type indicates that this property is meant to
956       // be indexed.
957       bool is_indexed_property = IsIndexedProperty(old_property_config);
958       if (is_indexed_property) {
959         ++old_indexed_properties;
960       }
961 
962       bool is_joinable_property =
963           old_property_config.joinable_config().value_type() !=
964           JoinableConfig::ValueType::NONE;
965       if (is_joinable_property) {
966         ++old_joinable_properties;
967       }
968 
969       auto new_property_name_and_config =
970           new_parsed_property_configs.property_config_map.find(
971               old_property_config.property_name());
972 
973       if (new_property_name_and_config ==
974           new_parsed_property_configs.property_config_map.end()) {
975         // Didn't find the old property
976         ICING_VLOG(1) << absl_ports::StrCat(
977             "Previously defined property type '", old_type_config.schema_type(),
978             ".", old_property_config.property_name(),
979             "' was not defined in new schema");
980         is_incompatible = true;
981         is_index_incompatible |= is_indexed_property;
982         is_join_incompatible |= is_joinable_property;
983         continue;
984       }
985 
986       const PropertyConfigProto* new_property_config =
987           new_property_name_and_config->second;
988       if (!has_property_changed &&
989           !ArePropertiesEqual(old_property_config, *new_property_config)) {
990         // Finally found a property that changed.
991         has_property_changed = true;
992       }
993 
994       if (!IsPropertyCompatible(old_property_config, *new_property_config)) {
995         ICING_VLOG(1) << absl_ports::StrCat(
996             "Property '", old_type_config.schema_type(), ".",
997             old_property_config.property_name(), "' is incompatible.");
998         is_incompatible = true;
999       }
1000 
1001       // Any change in the indexed property requires a reindexing
1002       if (!IsTermMatchTypeCompatible(
1003               old_property_config.string_indexing_config(),
1004               new_property_config->string_indexing_config()) ||
1005           !IsIntegerNumericMatchTypeCompatible(
1006               old_property_config.integer_indexing_config(),
1007               new_property_config->integer_indexing_config()) ||
1008           old_property_config.document_indexing_config()
1009                   .index_nested_properties() !=
1010               new_property_config->document_indexing_config()
1011                   .index_nested_properties()) {
1012         is_index_incompatible = true;
1013       }
1014 
1015       if (old_property_config.joinable_config().value_type() !=
1016           new_property_config->joinable_config().value_type()) {
1017         is_join_incompatible = true;
1018       }
1019     }
1020 
1021     // We can't have new properties that are REQUIRED since we won't know how
1022     // to backfill the data, and the existing data will be invalid. We're
1023     // guaranteed from our previous checks that all the old properties are also
1024     // present in the new property config, so we can do a simple int comparison
1025     // here to detect new required properties.
1026     if (new_parsed_property_configs.num_required_properties >
1027         old_required_properties) {
1028       ICING_VLOG(1) << absl_ports::StrCat(
1029           "New schema '", old_type_config.schema_type(),
1030           "' has REQUIRED properties that are not "
1031           "present in the previously defined schema");
1032       is_incompatible = true;
1033     }
1034 
1035     // If we've gained any new indexed properties, then the section ids may
1036     // change. Since the section ids are stored in the index, we'll need to
1037     // reindex everything.
1038     if (new_parsed_property_configs.num_indexed_properties >
1039         old_indexed_properties) {
1040       ICING_VLOG(1) << "Set of indexed properties in schema type '"
1041                     << old_type_config.schema_type()
1042                     << "' has changed, required reindexing.";
1043       is_index_incompatible = true;
1044     }
1045 
1046     // If we've gained any new joinable properties, then the joinable property
1047     // ids may change. Since the joinable property ids are stored in the cache,
1048     // we'll need to reconstruct joinable cache.
1049     if (new_parsed_property_configs.num_joinable_properties >
1050         old_joinable_properties) {
1051       ICING_VLOG(1) << "Set of joinable properties in schema type '"
1052                     << old_type_config.schema_type()
1053                     << "' has changed, required reconstructing joinable cache.";
1054       is_join_incompatible = true;
1055     }
1056 
1057     if (is_incompatible) {
1058       AddIncompatibleChangeToDelta(schema_delta.schema_types_incompatible,
1059                                    old_type_config, new_schema_dependent_map,
1060                                    old_type_config_map, new_type_config_map);
1061     }
1062 
1063     if (is_index_incompatible) {
1064       AddIncompatibleChangeToDelta(schema_delta.schema_types_index_incompatible,
1065                                    old_type_config, new_schema_dependent_map,
1066                                    old_type_config_map, new_type_config_map);
1067     }
1068 
1069     if (is_join_incompatible) {
1070       AddIncompatibleChangeToDelta(schema_delta.schema_types_join_incompatible,
1071                                    old_type_config, new_schema_dependent_map,
1072                                    old_type_config_map, new_type_config_map);
1073     }
1074 
1075     if (!is_incompatible && !is_index_incompatible && !is_join_incompatible &&
1076         has_property_changed) {
1077       schema_delta.schema_types_changed_fully_compatible.insert(
1078           old_type_config.schema_type());
1079     }
1080 
1081     // Lastly, remove this type from the map. We know that this type can't
1082     // come up in future iterations through the old schema types because the old
1083     // type config has unique types.
1084     new_type_config_map.erase(old_type_config.schema_type());
1085   }
1086 
1087   // Any types that are still present in the new_type_config_map are newly added
1088   // types.
1089   schema_delta.schema_types_new.reserve(new_type_config_map.size());
1090   for (auto& kvp : new_type_config_map) {
1091     schema_delta.schema_types_new.insert(std::move(kvp.first));
1092   }
1093 
1094   return schema_delta;
1095 }
1096 
1097 }  // namespace lib
1098 }  // namespace icing
1099