diff --git a/src/realm/CMakeLists.txt b/src/realm/CMakeLists.txt index ebf0330b8b..bc4103a909 100644 --- a/src/realm/CMakeLists.txt +++ b/src/realm/CMakeLists.txt @@ -47,7 +47,6 @@ set(REALM_SOURCES mixed.cpp obj.cpp object_converter.cpp - global_key.cpp query_engine.cpp query_expression.cpp query_value.cpp @@ -157,7 +156,6 @@ set(REALM_INSTALL_HEADERS error_codes.h error_codes.hpp exceptions.hpp - global_key.hpp group.hpp group_writer.hpp handover_defs.hpp diff --git a/src/realm/cluster_tree.cpp b/src/realm/cluster_tree.cpp index 1d57a9954a..24354abf4f 100644 --- a/src/realm/cluster_tree.cpp +++ b/src/realm/cluster_tree.cpp @@ -991,7 +991,6 @@ void ClusterTree::erase(ObjKey k, CascadeState& state) } } } - m_owner->free_local_id_after_hash_collision(k); m_owner->erase_from_search_indexes(k); size_t root_size = m_root->erase(ClusterNode::RowKey(k), state); diff --git a/src/realm/global_key.cpp b/src/realm/global_key.cpp deleted file mode 100644 index 9021a02a6c..0000000000 --- a/src/realm/global_key.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/************************************************************************* - * - * Copyright 2019 Realm Inc. - * - * 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 -#include -#include -#include -#include -#include -#include -#include -#include - -namespace realm { - -std::ostream& operator<<(std::ostream& os, const GlobalKey& object_id) -{ - return os << '{' << std::setw(4) << std::right << std::setfill('0') << std::hex << object_id.hi() << '-' - << std::setw(4) << std::right << std::setfill('0') << std::hex << object_id.lo() << '}' - << std::setfill(' ') << std::setw(0); -} - -std::istream& operator>>(std::istream& in, GlobalKey& object_id) -{ - try { - std::istream::sentry sentry{in}; - if (REALM_LIKELY(sentry)) { - std::string string; - char ch; - in.get(ch); - while (REALM_LIKELY(in)) { - string.push_back(ch); // Throws - if (REALM_LIKELY(ch == '}')) - break; - in.get(ch); - } - object_id = GlobalKey::from_string(string); - } - } - catch (const InvalidArgument&) { - object_id = GlobalKey(); - in.setstate(std::ios_base::failbit); - } - return in; -} -std::string GlobalKey::to_string() const -{ - std::ostringstream ss; - ss << *this; - return ss.str(); -} - - -GlobalKey GlobalKey::from_string(StringData string) -{ - if (string.size() < 5) // Must be at least "{0-0}" - throw InvalidArgument(ErrorCodes::InvalidArgument, "Invalid object ID."); - - const char* begin = string.data(); - const char* end = string.data() + string.size(); - const char* last = end - 1; - - if (*begin != '{' || *last != '}') - throw InvalidArgument(ErrorCodes::InvalidArgument, "Invalid object ID."); - - auto dash_pos = std::find(begin, end, '-'); - if (dash_pos == end) - throw InvalidArgument(ErrorCodes::InvalidArgument, "Invalid object ID."); - size_t dash_index = dash_pos - begin; - - const char* hi_begin = begin + 1; - const char* lo_begin = dash_pos + 1; - size_t hi_len = dash_index - 1; - size_t lo_len = string.size() - dash_index - 2; - - if (hi_len == 0 || hi_len > 16 || lo_len == 0 || lo_len > 16) { - throw InvalidArgument(ErrorCodes::InvalidArgument, "Invalid object ID."); - } - - auto isxdigit = static_cast(std::isxdigit); - if (!std::all_of(hi_begin, hi_begin + hi_len, isxdigit) || !std::all_of(lo_begin, lo_begin + lo_len, isxdigit)) { - throw InvalidArgument(ErrorCodes::InvalidArgument, "Invalid object ID."); - } - - // hi_begin and lo_begin do not need to be copied into a NUL-terminated - // buffer because we have checked above that they are immediately followed - // by '-' or '}' respectively, and std::strtoull guarantees that it will - // stop processing when it reaches either of those characters. - return GlobalKey(strtoull(hi_begin, nullptr, 16), strtoull(lo_begin, nullptr, 16)); -} - -GlobalKey::GlobalKey(Mixed pk) -{ - if (pk.is_null()) { - // Choose {1, 0} as the object ID for NULL. This could just as well have been {0, 0}, - // but then the null-representation for string and integer primary keys would have to - // be different, as {0, 0} is a valid object ID for a row with an integer primary key. - // Therefore, in the interest of simplicity, {1, 0} is chosen to represent NULL for - // both integer and string primary keys. - m_hi = 1; - m_lo = 0; - return; - } - - union { - unsigned char buffer[20]; - struct { - uint64_t lo; - uint64_t hi; - } oid; - } outp; - - switch (pk.get_type()) { - case type_String: { - auto val = pk.get_string(); - util::sha1(val.data(), val.size(), outp.buffer); - m_hi = outp.oid.hi; - m_lo = outp.oid.lo; - break; - } - - case type_ObjectId: { - union ObjectIdBuffer { - ObjectIdBuffer() {} - char buffer[sizeof(ObjectId)]; - ObjectId id; - } inp; - inp.id = pk.get(); - util::sha1(inp.buffer, sizeof(ObjectId), outp.buffer); - m_hi = outp.oid.hi; - m_lo = outp.oid.lo; - break; - } - - case type_Int: - m_hi = 0; - m_lo = uint64_t(pk.get_int()); - break; - - case type_UUID: { - union UUIDBuffer { - UUIDBuffer() {} - UUID::UUIDBytes id; - struct { - uint64_t upper; - uint64_t lower; - } values; - } inp; - inp.id = pk.get().to_bytes(); - m_hi = inp.values.upper; - m_lo = inp.values.lower; - break; - } - default: - m_hi = -1; - m_lo = -1; - break; - } -} - -} // namespace realm diff --git a/src/realm/global_key.hpp b/src/realm/global_key.hpp deleted file mode 100644 index 3b90fa7d51..0000000000 --- a/src/realm/global_key.hpp +++ /dev/null @@ -1,160 +0,0 @@ -/************************************************************************* - * - * Copyright 2019 Realm Inc. - * - * 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. - * - **************************************************************************/ - -#ifndef REALM_GLOBAL_KEY_HPP -#define REALM_GLOBAL_KEY_HPP - -#include -#include -#include -#include - -namespace realm { - -class StringData; -class Mixed; - -/// GlobalKeys are globally unique for a given class (table), and up to 128 bits -/// wide. They are represented as two 64-bit integers, each of which may -/// frequently be small, for best on-wire compressibility. -/// -/// We define a way to map from 128-bit on-write GlobalKeys to local 64-bit ObjKeys. -/// -/// The three object ID types are: -/// a. Global Keys for objects in tables without primary keys. -/// b. Global Keys for objects in tables with integer primary keys. -/// c. Global Keys for objects in tables with other primary key types. -/// -/// For objects without primary keys (a), a "squeezed" tuple of the -/// client_file_ident and a peer-local sequence number is used as the local -/// ObjKey. The on-write Object ID is the "unsqueezed" format. -/// -/// For integer primary keys (b), the GlobalKey just the integer value as the low -/// part. -/// -/// For objects with other types of primary keys (c), the GlobalKey is a 128-bit -/// hash of the primary key value. However, the local object ID must be a 63-bit -/// integer, because that is the maximum size integer that can be used in an ObjKey. -/// The solution is to optimistically use the lower 62 bits of the on-wire GlobalKey. -/// If this results in a ObjKey which is already in use, a new local ObjKey is -/// generated with the 63th bit set and using a locally generated sequence number for -/// the lower bits. The mapping between GlobalKey and ObjKey is stored in the Table -/// structure. - -struct GlobalKey { - constexpr GlobalKey(uint64_t h, uint64_t l) - : m_lo(l) - , m_hi(h) - { - } - static GlobalKey from_string(StringData); - - constexpr GlobalKey(realm::util::None = realm::util::none) - : m_lo(-1) - , m_hi(-1) - { - } - - // Construct an GlobalKey from either a string, an integer or a GlobalId - GlobalKey(Mixed pk); - - // Construct an object id from the local squeezed ObjKey - GlobalKey(ObjKey squeezed, uint64_t sync_file_id) - { - uint64_t u = uint64_t(squeezed.value); - - m_lo = (u & 0xff) | ((u & 0xffffff0000) >> 8); - m_hi = ((u & 0xff00) >> 8) | ((u & 0xffffff0000000000) >> 32); - if (m_hi == 0) - m_hi = sync_file_id; - } - - constexpr GlobalKey(const GlobalKey&) noexcept = default; - GlobalKey& operator=(const GlobalKey&) noexcept = default; - - constexpr uint64_t lo() const - { - return m_lo; - } - constexpr uint64_t hi() const - { - return m_hi; - } - - std::string to_string() const; - - constexpr bool operator<(const GlobalKey& other) const - { - return (m_hi == other.m_hi) ? (m_lo < other.m_lo) : (m_hi < other.m_hi); - } - constexpr bool operator==(const GlobalKey& other) const - { - return m_hi == other.m_hi && m_lo == other.m_lo; - } - constexpr bool operator!=(const GlobalKey& other) const - { - return !(*this == other); - } - - explicit constexpr operator bool() const noexcept - { - return (*this != GlobalKey{}); - } - - // Generate a local ObjKey from the GlobalKey. If the object is created - // in this realm (sync_file_id == hi) then 0 is used for hi. In this - // way we achieves that objects created before first contact with the - // server does not need to change key. - ObjKey get_local_key(uint64_t sync_file_id) - { - REALM_ASSERT(m_hi <= 0x3fffffff); - - auto high = m_hi; - if (high == sync_file_id) - high = 0; - uint64_t a = m_lo & 0xff; - uint64_t b = (high & 0xff) << 8; - uint64_t c = (m_lo & 0xffffff00) << 8; - uint64_t d = (high & 0x3fffff00) << 32; - - return ObjKey(int64_t(a | b | c | d)); - } - -private: - uint64_t m_lo; - uint64_t m_hi; -}; - -std::ostream& operator<<(std::ostream&, const GlobalKey&); -std::istream& operator>>(std::istream&, GlobalKey&); - -} // namespace realm - -namespace std { - -template <> -struct hash { - size_t operator()(realm::GlobalKey oid) const - { - return std::hash{}(oid.lo()) ^ std::hash{}(oid.hi()); - } -}; - -} // namespace std - -#endif /* REALM_OBJECT_ID_HPP */ diff --git a/src/realm/obj.cpp b/src/realm/obj.cpp index 4dba1de45c..ef0eab3c24 100644 --- a/src/realm/obj.cpp +++ b/src/realm/obj.cpp @@ -114,11 +114,6 @@ Obj::Obj(TableRef table, MemRef mem, ObjKey key, size_t row_ndx) m_storage_version = get_alloc().get_storage_version(); } -GlobalKey Obj::get_object_id() const -{ - return m_table->get_object_id(m_key); -} - ObjLink Obj::get_link() const { return ObjLink(m_table->get_key(), m_key); diff --git a/src/realm/obj.hpp b/src/realm/obj.hpp index 67c82a0cad..5deba24816 100644 --- a/src/realm/obj.hpp +++ b/src/realm/obj.hpp @@ -34,7 +34,6 @@ class ClusterTree; class TableView; class CascadeState; class ObjList; -struct GlobalKey; template class Lst; @@ -100,7 +99,6 @@ class Obj { { return m_key; } - GlobalKey get_object_id() const; ObjLink get_link() const; /// Check if the object is still alive diff --git a/src/realm/replication.cpp b/src/realm/replication.cpp index 26826fb499..dcd40fe745 100644 --- a/src/realm/replication.cpp +++ b/src/realm/replication.cpp @@ -156,14 +156,14 @@ void Replication::track_new_object(const Table* table, ObjKey key) m_most_recently_created_object[table_index] = key; } -void Replication::create_object(const Table* t, GlobalKey id) +void Replication::create_object(const Table* t, ObjKey key) { if (auto logger = would_log(LogLevel::debug)) { logger->log(LogCategory::object, LogLevel::debug, "Create object '%1'", t->get_class_name()); } select_table(t); // Throws - m_encoder.create_object(id.get_local_key(0)); // Throws - track_new_object(t, id.get_local_key(0)); // Throws + m_encoder.create_object(key); // Throws + track_new_object(t, key); // Throws } void Replication::create_object_with_primary_key(const Table* t, ObjKey key, Mixed pk) diff --git a/src/realm/replication.hpp b/src/realm/replication.hpp index 41d6ff28dd..866bba47c4 100644 --- a/src/realm/replication.hpp +++ b/src/realm/replication.hpp @@ -75,7 +75,7 @@ class Replication { virtual void dictionary_erase(const CollectionBase& dict, size_t dict_ndx, Mixed key); virtual void dictionary_clear(const CollectionBase& dict); - virtual void create_object(const Table*, GlobalKey); + virtual void create_object(const Table*, ObjKey); virtual void create_object_with_primary_key(const Table*, ObjKey, Mixed); void create_linked_object(const Table*, ObjKey); virtual void remove_object(const Table*, ObjKey); diff --git a/src/realm/table.cpp b/src/realm/table.cpp index f163988525..5160dec5f1 100644 --- a/src/realm/table.cpp +++ b/src/realm/table.cpp @@ -1536,7 +1536,6 @@ void Table::clear() { CascadeState state(CascadeState::Mode::Strong, get_parent_group()); m_clusters.clear(state); - free_collision_table(); } @@ -2217,21 +2216,13 @@ Obj Table::create_object(ObjKey key, const FieldValues& values) if (m_primary_key_col) throw IllegalOperation(util::format("Table has primary key: %1", get_name())); if (key == null_key) { - GlobalKey object_id = allocate_object_id_squeezed(); - key = object_id.get_local_key(get_sync_file_id()); - // Check if this key collides with an already existing object - // This could happen if objects were at some point created with primary keys, - // but later primary key property was removed from the schema. - while (m_clusters.is_valid(key)) { - object_id = allocate_object_id_squeezed(); - key = object_id.get_local_key(get_sync_file_id()); - } - if (auto repl = get_repl()) - repl->create_object(this, object_id); + key = get_next_valid_key(); } REALM_ASSERT(key.value >= 0); + if (auto repl = get_repl()) + repl->create_object(this, key); Obj obj = m_clusters.insert(key, values); // repl->set() return obj; @@ -2241,10 +2232,7 @@ Obj Table::create_linked_object() { REALM_ASSERT(is_embedded()); - GlobalKey object_id = allocate_object_id_squeezed(); - ObjKey key = object_id.get_local_key(get_sync_file_id()); - REALM_ASSERT(key.value >= 0); - + ObjKey key = get_next_valid_key(); if (auto repl = get_repl()) repl->create_linked_object(this, key); @@ -2253,39 +2241,6 @@ Obj Table::create_linked_object() return obj; } -Obj Table::create_object(GlobalKey object_id, const FieldValues& values) -{ - if (is_embedded()) - throw IllegalOperation(util::format("Explicit creation of embedded object not allowed in: %1", get_name())); - if (m_primary_key_col) - throw IllegalOperation(util::format("Table has primary key: %1", get_name())); - ObjKey key = object_id.get_local_key(get_sync_file_id()); - - if (auto repl = get_repl()) - repl->create_object(this, object_id); - - try { - Obj obj = m_clusters.insert(key, values); - // Check if tombstone exists - if (m_tombstones && m_tombstones->is_valid(key.get_unresolved())) { - auto unres_key = key.get_unresolved(); - // Copy links over - auto tombstone = m_tombstones->get(unres_key); - obj.assign_pk_and_backlinks(tombstone); - // If tombstones had no links to it, it may still be alive - if (m_tombstones->is_valid(unres_key)) { - CascadeState state(CascadeState::Mode::None); - m_tombstones->erase(unres_key, state); - } - } - - return obj; - } - catch (const KeyAlreadyUsed&) { - return m_clusters.get(key); - } -} - Obj Table::create_object_with_primary_key(const Mixed& primary_key, FieldValues&& field_values, UpdateMode mode, bool* did_create) { @@ -2329,17 +2284,16 @@ Obj Table::create_object_with_primary_key(const Mixed& primary_key, FieldValues& ObjKey unres_key; if (m_tombstones) { - // Check for potential tombstone - GlobalKey object_id{primary_key}; - ObjKey object_key = global_to_local_object_id_hashed(object_id); - - ObjKey key = object_key.get_unresolved(); - if (auto obj = m_tombstones->try_get_obj(key)) { - auto existing_pk_value = obj.get_any(primary_key_col); - - // If the primary key is the same, the object should be resurrected below - if (existing_pk_value == primary_key) { - unres_key = key; + if (auto sz = m_tombstones->size()) { + // Check for potential tombstone + Iterator end(*m_tombstones, sz); + for (Iterator it(*m_tombstones, 0); it != end; ++it) { + auto existing_pk_value = it->get_any(primary_key_col); + // If the primary key is the same, the object should be resurrected below + if (existing_pk_value == primary_key) { + unres_key = it->get_key(); + break; + } } } } @@ -2387,25 +2341,8 @@ ObjKey Table::find_primary_key(Mixed primary_key) const DataType type = DataType(primary_key_col.get_type()); REALM_ASSERT((primary_key.is_null() && primary_key_col.get_attrs().test(col_attr_Nullable)) || primary_key.get_type() == type); - - if (auto&& index = m_index_accessors[primary_key_col.get_index().val]) { - return index->find_first(primary_key); - } - - // This must be file format 11, 20 or 21 as those are the ones we can open in read-only mode - // so try the old algorithm - GlobalKey object_id{primary_key}; - ObjKey object_key = global_to_local_object_id_hashed(object_id); - - // Check if existing - if (auto obj = m_clusters.try_get_obj(object_key)) { - auto existing_pk_value = obj.get_any(primary_key_col); - - if (existing_pk_value == primary_key) { - return object_key; - } - } - return {}; + REALM_ASSERT(m_index_accessors[primary_key_col.get_index().val]); + return m_index_accessors[primary_key_col.get_index().val]->find_first(primary_key); } ObjKey Table::get_objkey_from_primary_key(const Mixed& primary_key) @@ -2414,52 +2351,8 @@ ObjKey Table::get_objkey_from_primary_key(const Mixed& primary_key) if (auto key = find_primary_key(primary_key)) { return key; } - // Object does not exist - create tombstone - GlobalKey object_id{primary_key}; - ObjKey object_key = global_to_local_object_id_hashed(object_id); - return get_or_create_tombstone(object_key, m_primary_key_col, primary_key).get_key(); -} - -ObjKey Table::get_objkey_from_global_key(GlobalKey global_key) -{ - REALM_ASSERT(!m_primary_key_col); - auto object_key = global_key.get_local_key(get_sync_file_id()); - - // Check if existing - if (m_clusters.is_valid(object_key)) { - return object_key; - } - - return get_or_create_tombstone(object_key, {}, {}).get_key(); -} - -ObjKey Table::get_objkey(GlobalKey global_key) const -{ - ObjKey key; - REALM_ASSERT(!m_primary_key_col); - uint32_t max = std::numeric_limits::max(); - if (global_key.hi() <= max && global_key.lo() <= max) { - key = global_key.get_local_key(get_sync_file_id()); - } - if (key && !is_valid(key)) { - key = realm::null_key; - } - return key; -} - -GlobalKey Table::get_object_id(ObjKey key) const -{ - auto col = get_primary_key_column(); - if (col) { - const Obj obj = get_object(key); - auto val = obj.get_any(col); - return {val}; - } - else { - return {key, get_sync_file_id()}; - } - return {}; + return get_or_create_tombstone(ObjKey{}, m_primary_key_col, primary_key).get_key(); } Obj Table::get_object_with_primary_key(Mixed primary_key) const @@ -2486,157 +2379,28 @@ Mixed Table::get_primary_key(ObjKey key) const } } -GlobalKey Table::allocate_object_id_squeezed() -{ - // m_client_file_ident will be zero if we haven't been in contact with - // the server yet. - auto peer_id = get_sync_file_id(); - auto sequence = allocate_sequence_number(); - return GlobalKey{peer_id, sequence}; -} - -namespace { - -/// Calculate optimistic local ID that may collide with others. It is up to -/// the caller to ensure that collisions are detected and that -/// allocate_local_id_after_collision() is called to obtain a non-colliding -/// ID. -inline ObjKey get_optimistic_local_id_hashed(GlobalKey global_id) -{ -#if REALM_EXERCISE_OBJECT_ID_COLLISION - const uint64_t optimistic_mask = 0xff; -#else - const uint64_t optimistic_mask = 0x3fffffffffffffff; -#endif - static_assert(!(optimistic_mask >> 62), "optimistic Object ID mask must leave the 63rd and 64th bit zero"); - return ObjKey{int64_t(global_id.lo() & optimistic_mask)}; -} - -inline ObjKey make_tagged_local_id_after_hash_collision(uint64_t sequence_number) -{ - REALM_ASSERT(!(sequence_number >> 62)); - return ObjKey{int64_t(0x4000000000000000 | sequence_number)}; -} - -} // namespace - -ObjKey Table::global_to_local_object_id_hashed(GlobalKey object_id) const +Obj Table::get_or_create_tombstone(ObjKey key, ColKey pk_col, Mixed pk_val) { - ObjKey optimistic = get_optimistic_local_id_hashed(object_id); - - if (ref_type collision_map_ref = to_ref(m_top.get(top_position_for_collision_map))) { - Allocator& alloc = m_top.get_alloc(); - Array collision_map{alloc}; - collision_map.init_from_ref(collision_map_ref); // Throws - - Array hi{alloc}; - hi.init_from_ref(to_ref(collision_map.get(s_collision_map_hi))); // Throws - - // Entries are ordered by hi,lo - size_t found = hi.find_first(object_id.hi()); - if (found != npos && uint64_t(hi.get(found)) == object_id.hi()) { - Array lo{alloc}; - lo.init_from_ref(to_ref(collision_map.get(s_collision_map_lo))); // Throws - size_t candidate = lo.find_first(object_id.lo(), found); - if (candidate != npos && uint64_t(hi.get(candidate)) == object_id.hi()) { - Array local_id{alloc}; - local_id.init_from_ref(to_ref(collision_map.get(s_collision_map_local_id))); // Throws - return ObjKey{local_id.get(candidate)}; + ensure_graveyard(); + if (auto sz = m_tombstones->size()) { + // Check for existing tombstone + Iterator end(*m_tombstones, sz); + for (Iterator it(*m_tombstones, 0); it != end; ++it) { + auto existing_pk_value = it->get_any(m_primary_key_col); + // If the primary key is the same, the object should be resurrected below + if (existing_pk_value == pk_val) { + return *it; } } } - return optimistic; -} - -ObjKey Table::allocate_local_id_after_hash_collision(GlobalKey incoming_id, GlobalKey colliding_id, - ObjKey colliding_local_id) -{ - // Possible optimization: Cache these accessors - Allocator& alloc = m_top.get_alloc(); - Array collision_map{alloc}; - Array hi{alloc}; - Array lo{alloc}; - Array local_id{alloc}; - - collision_map.set_parent(&m_top, top_position_for_collision_map); - hi.set_parent(&collision_map, s_collision_map_hi); - lo.set_parent(&collision_map, s_collision_map_lo); - local_id.set_parent(&collision_map, s_collision_map_local_id); - - ref_type collision_map_ref = to_ref(m_top.get(top_position_for_collision_map)); - if (collision_map_ref) { - collision_map.init_from_parent(); // Throws + // Create new tombstone + if (!key) { + key = get_next_valid_key(); } - else { - MemRef mem = Array::create_empty_array(Array::type_HasRefs, false, alloc); // Throws - collision_map.init_from_mem(mem); // Throws - collision_map.update_parent(); - - ref_type lo_ref = Array::create_array(Array::type_Normal, false, 0, 0, alloc).get_ref(); // Throws - ref_type hi_ref = Array::create_array(Array::type_Normal, false, 0, 0, alloc).get_ref(); // Throws - ref_type local_id_ref = Array::create_array(Array::type_Normal, false, 0, 0, alloc).get_ref(); // Throws - collision_map.add(lo_ref); // Throws - collision_map.add(hi_ref); // Throws - collision_map.add(local_id_ref); // Throws - } - - hi.init_from_parent(); // Throws - lo.init_from_parent(); // Throws - local_id.init_from_parent(); // Throws - - size_t num_entries = hi.size(); - REALM_ASSERT(lo.size() == num_entries); - REALM_ASSERT(local_id.size() == num_entries); - - auto lower_bound_object_id = [&](GlobalKey object_id) -> size_t { - size_t i = hi.lower_bound_int(int64_t(object_id.hi())); - while (i < num_entries && uint64_t(hi.get(i)) == object_id.hi() && uint64_t(lo.get(i)) < object_id.lo()) - ++i; - return i; - }; - - auto insert_collision = [&](GlobalKey object_id, ObjKey new_local_id) { - size_t i = lower_bound_object_id(object_id); - if (i != num_entries) { - GlobalKey existing{uint64_t(hi.get(i)), uint64_t(lo.get(i))}; - if (existing == object_id) { - REALM_ASSERT(new_local_id.value == local_id.get(i)); - return; - } - } - hi.insert(i, int64_t(object_id.hi())); - lo.insert(i, int64_t(object_id.lo())); - local_id.insert(i, new_local_id.value); - ++num_entries; - }; - - auto sequence_number_for_local_id = allocate_sequence_number(); - ObjKey new_local_id = make_tagged_local_id_after_hash_collision(sequence_number_for_local_id); - insert_collision(incoming_id, new_local_id); - insert_collision(colliding_id, colliding_local_id); - - return new_local_id; -} - -Obj Table::get_or_create_tombstone(ObjKey key, ColKey pk_col, Mixed pk_val) -{ auto unres_key = key.get_unresolved(); + REALM_ASSERT(!m_tombstones->is_valid(unres_key)); - ensure_graveyard(); - auto tombstone = m_tombstones->try_get_obj(unres_key); - if (tombstone) { - if (pk_col) { - auto existing_pk_value = tombstone.get_any(pk_col); - // It may just be the same object - if (existing_pk_value != pk_val) { - // We have a collision - create new ObjKey - key = allocate_local_id_after_hash_collision({pk_val}, {existing_pk_value}, key); - return get_or_create_tombstone(key, pk_col, pk_val); - } - } - return tombstone; - } if (Replication* repl = get_repl()) { if (auto logger = repl->would_log(util::Logger::Level::debug)) { logger->log(LogCategory::object, util::Logger::Level::debug, @@ -2644,42 +2408,8 @@ Obj Table::get_or_create_tombstone(ObjKey key, ColKey pk_col, Mixed pk_val) unres_key); } } - return m_tombstones->insert(unres_key, {{pk_col, pk_val}}); -} -void Table::free_local_id_after_hash_collision(ObjKey key) -{ - if (ref_type collision_map_ref = to_ref(m_top.get(top_position_for_collision_map))) { - if (key.is_unresolved()) { - // Keys will always be inserted as resolved - key = key.get_unresolved(); - } - // Possible optimization: Cache these accessors - Array collision_map{m_alloc}; - Array local_id{m_alloc}; - - collision_map.set_parent(&m_top, top_position_for_collision_map); - local_id.set_parent(&collision_map, s_collision_map_local_id); - collision_map.init_from_ref(collision_map_ref); - local_id.init_from_parent(); - auto ndx = local_id.find_first(key.value); - if (ndx != realm::npos) { - Array hi{m_alloc}; - Array lo{m_alloc}; - - hi.set_parent(&collision_map, s_collision_map_hi); - lo.set_parent(&collision_map, s_collision_map_lo); - hi.init_from_parent(); - lo.init_from_parent(); - - hi.erase(ndx); - lo.erase(ndx); - local_id.erase(ndx); - if (hi.size() == 0) { - free_collision_table(); - } - } - } + return m_tombstones->insert(unres_key, {{pk_col, pk_val}}); } void Table::free_collision_table() @@ -2741,15 +2471,10 @@ ObjKey Table::invalidate_object(ObjKey key) if (obj.has_backlinks(false)) { // If the object has backlinks, we should make a tombstone // and make inward links point to it, - if (auto primary_key_col = get_primary_key_column()) { - auto pk = obj.get_any(primary_key_col); - GlobalKey object_id{pk}; - auto unres_key = global_to_local_object_id_hashed(object_id); - tombstone = get_or_create_tombstone(unres_key, primary_key_col, pk); - } - else { - tombstone = get_or_create_tombstone(key, {}, {}); - } + auto primary_key_col = get_primary_key_column(); + REALM_ASSERT(primary_key_col); + auto pk = obj.get_any(primary_key_col); + tombstone = get_or_create_tombstone(key, primary_key_col, pk); tombstone.assign_pk_and_backlinks(obj); } diff --git a/src/realm/table.hpp b/src/realm/table.hpp index b5e19db851..ba4660da9e 100644 --- a/src/realm/table.hpp +++ b/src/realm/table.hpp @@ -35,7 +35,6 @@ #include #include #include -#include // Only set this to one when testing the code paths that exercise object ID // hash collisions. It artificially limits the "optimistic" local ID to use @@ -52,7 +51,6 @@ class ColKeys; template class Columns; class DictionaryLinkValues; -struct GlobalKey; class Group; class LinkChain; class SearchIndex; @@ -293,9 +291,6 @@ class Table { // Create an object with key. If the key is omitted, a key will be generated by the system Obj create_object(ObjKey key = {}, const FieldValues& = {}); - // Create an object with specific GlobalKey - or return already existing object - // Potential tombstone will be resurrected - Obj create_object(GlobalKey object_id, const FieldValues& = {}); // Create an object with primary key. If an object with the given primary key already exists, it // will be returned and did_create (if supplied) will be set to false. // Potential tombstone will be resurrected @@ -307,18 +302,10 @@ class Table { } // Return key for existing object or return null key. ObjKey find_primary_key(Mixed value) const; - // Return ObjKey for object identified by id. If objects does not exist, return null key - // Important: This function must not be called for tables with primary keys. - ObjKey get_objkey(GlobalKey id) const; // Return key for existing object or return unresolved key. // Important: This is to be used ONLY by the Sync client. SDKs should NEVER // observe an unresolved key. Ever. ObjKey get_objkey_from_primary_key(const Mixed& primary_key); - // Return key for existing object or return unresolved key. - // Important: This is to be used ONLY by the Sync client. SDKs should NEVER - // observe an unresolved key. Ever. - // Important (2): This function must not be called for tables with primary keys. - ObjKey get_objkey_from_global_key(GlobalKey key); /// Create a number of objects and add corresponding keys to a vector void create_objects(size_t number, std::vector& keys); /// Create a number of objects with keys supplied @@ -328,7 +315,6 @@ class Table { { return key && m_clusters.is_valid(key); } - GlobalKey get_object_id(ObjKey key) const; Obj get_object(ObjKey key) const { REALM_ASSERT(!key.is_unresolved()); @@ -791,26 +777,10 @@ class Table { void validate_column_is_unique(ColKey col_key) const; ObjKey get_next_valid_key(); - /// Some Object IDs are generated as a tuple of the client_file_ident and a - /// local sequence number. This function takes the next number in the - /// sequence for the given table and returns an appropriate globally unique - /// GlobalKey. - GlobalKey allocate_object_id_squeezed(); - - /// Find the local 64-bit object ID for the provided global 128-bit ID. - ObjKey global_to_local_object_id_hashed(GlobalKey global_id) const; - - /// After a local ObjKey collision has been detected, this function may be - /// called to obtain a non-colliding local ObjKey in such a way that subsequent - /// calls to global_to_local_object_id() will return the correct local ObjKey - /// for both \a incoming_id and \a colliding_id. - ObjKey allocate_local_id_after_hash_collision(GlobalKey incoming_id, GlobalKey colliding_id, - ObjKey colliding_local_id); + /// Create a placeholder for a not yet existing object and return key to it Obj get_or_create_tombstone(ObjKey key, ColKey pk_col, Mixed pk_val); - /// Should be called when an object is deleted - void free_local_id_after_hash_collision(ObjKey key); - /// Should be called when last entry is removed - or when table is cleared + /// Should be called from upgrade function void free_collision_table(); /// Called in the context of Group::commit() to ensure that @@ -1419,10 +1389,6 @@ class _impl::TableFriend { { table.batch_erase_rows(keys); // Throws } - static ObjKey global_to_local_object_id_hashed(const Table& table, GlobalKey global_id) - { - return table.global_to_local_object_id_hashed(global_id); - } }; } // namespace realm diff --git a/src/realm/transaction.cpp b/src/realm/transaction.cpp index 9e93875923..f4d7576f51 100644 --- a/src/realm/transaction.cpp +++ b/src/realm/transaction.cpp @@ -582,6 +582,7 @@ void Transaction::upgrade_file_format(int target_file_format_version) // avoid upgrading them because it affects a small niche case. Instead, there is a // workaround in the String Index search code for not relying on items being ordered. t->migrate_col_keys(); + t->free_collision_table(); } } // NOTE: Additional future upgrade steps go here. diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 68621735e4..e2d6a53ec2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -57,7 +57,6 @@ set(CORE_TEST_SOURCES test_dictionary.cpp test_file.cpp test_file_locks.cpp - test_global_key.cpp test_group.cpp test_impl_simulated_failure.cpp test_index_string.cpp diff --git a/test/object-store/transaction_log_parsing.cpp b/test/object-store/transaction_log_parsing.cpp index a4f1bc16e9..5ea6bcefe0 100644 --- a/test/object-store/transaction_log_parsing.cpp +++ b/test/object-store/transaction_log_parsing.cpp @@ -1588,20 +1588,18 @@ TEMPLATE_TEST_CASE("DeepChangeChecker collections", "[notifications]", cf::ListO InMemoryTestFile config; config.automatic_change_notifications = false; auto r = Realm::get_shared_realm(config); - r->update_schema({ - {"table", - {{"int", PropertyType::Int}, - {"link1", PropertyType::Object | PropertyType::Nullable, "table"}, - {"link2", PropertyType::Object | PropertyType::Nullable, "table"}, - test_type.property()}}, - }); + r->update_schema({{"table", + {{"int", PropertyType::Int, Property::IsPrimary{true}}, + {"link1", PropertyType::Object | PropertyType::Nullable, "table"}, + {"link2", PropertyType::Object | PropertyType::Nullable, "table"}, + test_type.property()}}}); auto table = r->read_group().get_table("class_table"); TableKey dst_table_key = table->get_key(); std::vector objects; r->begin_transaction(); for (int i = 0; i < 10; ++i) - objects.push_back(table->create_object().set_all(i)); + objects.push_back(table->create_object_with_primary_key(i)); r->commit_transaction(); auto track_changes = [&](auto&& f) { @@ -1833,7 +1831,7 @@ TEST_CASE("DeepChangeChecker singular links", "[notifications]") { r->update_schema({{ "table", { - {"int", PropertyType::Int}, + {"int", PropertyType::Int, Property::IsPrimary{true}}, {"link1", PropertyType::Object | PropertyType::Nullable, "table"}, {"link2", PropertyType::Object | PropertyType::Nullable, "table"}, {"mixed_link", PropertyType::Mixed | PropertyType::Nullable}, @@ -1845,7 +1843,7 @@ TEST_CASE("DeepChangeChecker singular links", "[notifications]") { std::vector objects; r->begin_transaction(); for (int i = 0; i < 10; ++i) - objects.push_back(table->create_object().set_all(i)); + objects.push_back(table->create_object_with_primary_key(i)); r->commit_transaction(); auto track_changes = [&](auto&& f) { @@ -2122,7 +2120,7 @@ TEST_CASE("DeepChangeChecker singular links", "[notifications]") { table->clear(); objects.clear(); for (int i = 0; i < 20; ++i) - objects.push_back(table->create_object().set_all(i)); + objects.push_back(table->create_object_with_primary_key(i)); for (int i = 0; i < 19; ++i) set_link(objects[i], link_column, ObjLink{dst_table_key, objects[i + 1].get_key()}); r->commit_transaction(); diff --git a/test/test_global_key.cpp b/test/test_global_key.cpp deleted file mode 100644 index e504d53c2a..0000000000 --- a/test/test_global_key.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/************************************************************************* - * - * Copyright 2016 Realm Inc. - * - * 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 - -#include "test.hpp" - -#include -#include - -using namespace realm; - -TEST(GlobalKey_ToString) -{ - CHECK_EQUAL(GlobalKey(0xabc, 0xdef).to_string(), "{0abc-0def}"); - CHECK_EQUAL(GlobalKey(0x11abc, 0x999def).to_string(), "{11abc-999def}"); - CHECK_EQUAL(GlobalKey(0, 0).to_string(), "{0000-0000}"); -} - -TEST(GlobalKey_FromString) -{ - CHECK_EQUAL(GlobalKey::from_string("{0-0}"), GlobalKey(0, 0)); - CHECK_EQUAL(GlobalKey::from_string("{aaaabbbbccccdddd-eeeeffff00001111}"), - GlobalKey(0xaaaabbbbccccddddULL, 0xeeeeffff00001111ULL)); - CHECK_THROW(GlobalKey::from_string(""), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{}"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("}"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("0"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{0}"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("-"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("0-"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{0-0"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{0-0-0}"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{aaaabbbbccccdddde-0}"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{0g-0}"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{0-0g}"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{0-aaaabbbbccccdddde}"), InvalidArgument); - CHECK_THROW(GlobalKey::from_string("{-}"), InvalidArgument); - - // std::strtoull accepts the "0x" prefix. We don't. - CHECK_THROW(GlobalKey::from_string("{0x0-0x0}"), InvalidArgument); - { - std::istringstream istr("{1-2}"); - GlobalKey oid; - istr >> oid; - CHECK_EQUAL(oid, GlobalKey(1, 2)); - } - { - std::istringstream istr("{1-2"); - GlobalKey oid; - istr >> oid; - CHECK(istr.rdstate() & std::istream::failbit); - CHECK_EQUAL(oid, GlobalKey()); - } -} - -TEST(GlobalKey_Compare) -{ - CHECK_LESS(GlobalKey(0, 0), GlobalKey(0, 1)); - CHECK_LESS(GlobalKey(0, 0), GlobalKey(1, 0)); -} diff --git a/test/test_mixed_null_assertions.cpp b/test/test_mixed_null_assertions.cpp index 71a85e7a22..6841ae0bde 100644 --- a/test/test_mixed_null_assertions.cpp +++ b/test/test_mixed_null_assertions.cpp @@ -89,10 +89,10 @@ TEST(List_Mixed_do_insert) TEST(Mixed_List_unresolved_as_null) { Group g; - auto t = g.add_table("foo"); + auto t = g.add_table_with_primary_key("foo", type_Int, "id"); t->add_column_list(type_Mixed, "mixeds"); - auto obj = t->create_object(); - auto obj1 = t->create_object(); + auto obj = t->create_object_with_primary_key(1); + auto obj1 = t->create_object_with_primary_key(2); auto list = obj.get_list("mixeds"); @@ -168,10 +168,10 @@ TEST(Mixed_List_unresolved_as_null) { Group g; - auto t = g.add_table("foo"); + auto t = g.add_table_with_primary_key("foo", type_Int, "id"); t->add_column_list(type_Mixed, "mixeds"); - auto obj = t->create_object(); - auto obj1 = t->create_object(); + auto obj = t->create_object_with_primary_key(1); + auto obj1 = t->create_object_with_primary_key(2); auto list = obj.get_list("mixeds"); list.insert(0, obj1); @@ -186,10 +186,10 @@ TEST(Mixed_List_unresolved_as_null) { Group g; - auto t = g.add_table("foo"); + auto t = g.add_table_with_primary_key("foo", type_Int, "id"); t->add_column_list(type_Mixed, "mixeds"); - auto obj = t->create_object(); - auto obj1 = t->create_object(); + auto obj = t->create_object_with_primary_key(1); + auto obj1 = t->create_object_with_primary_key(2); auto list = obj.get_list("mixeds"); list.insert(0, obj1); @@ -205,11 +205,11 @@ TEST(Mixed_Set_unresolved_links) { Group g; - auto t = g.add_table("foo"); + auto t = g.add_table_with_primary_key("foo", type_Int, "id"); t->add_column_set(type_Mixed, "mixeds"); - auto obj = t->create_object(); - auto obj1 = t->create_object(); - auto obj2 = t->create_object(); + auto obj = t->create_object_with_primary_key(1); + auto obj1 = t->create_object_with_primary_key(2); + auto obj2 = t->create_object_with_primary_key(3); auto set = obj.get_set("mixeds"); auto [it, success] = set.insert(Mixed{obj1}); obj1.invalidate(); @@ -261,11 +261,11 @@ TEST(Mixed_Set_unresolved_links) { // erase null but there are only unresolved links in the set Group g; - auto t = g.add_table("foo"); + auto t = g.add_table_with_primary_key("foo", type_Int, "id"); t->add_column_set(type_Mixed, "mixeds"); - auto obj = t->create_object(); - auto obj1 = t->create_object(); - auto obj2 = t->create_object(); + auto obj = t->create_object_with_primary_key(1); + auto obj1 = t->create_object_with_primary_key(2); + auto obj2 = t->create_object_with_primary_key(3); auto set = obj.get_set("mixeds"); set.insert(obj1); set.insert(obj2); @@ -286,11 +286,11 @@ TEST(Mixed_Set_unresolved_links) { // erase null when there are unresolved and nulls Group g; - auto t = g.add_table("foo"); + auto t = g.add_table_with_primary_key("foo", type_Int, "id"); t->add_column_set(type_Mixed, "mixeds"); - auto obj = t->create_object(); - auto obj1 = t->create_object(); - auto obj2 = t->create_object(); + auto obj = t->create_object_with_primary_key(1); + auto obj1 = t->create_object_with_primary_key(2); + auto obj2 = t->create_object_with_primary_key(3); auto set = obj.get_set("mixeds"); set.insert(obj1); set.insert(obj2); @@ -312,11 +312,11 @@ TEST(Mixed_Set_unresolved_links) { // assure that random access iterator does not return an unresolved link Group g; - auto t = g.add_table("foo"); + auto t = g.add_table_with_primary_key("foo", type_Int, "id"); t->add_column_set(type_Mixed, "mixeds"); - auto obj = t->create_object(); - auto obj1 = t->create_object(); - auto obj2 = t->create_object(); + auto obj = t->create_object_with_primary_key(1); + auto obj1 = t->create_object_with_primary_key(2); + auto obj2 = t->create_object_with_primary_key(3); auto set = obj.get_set("mixeds"); set.insert(obj1); set.insert(obj2); diff --git a/test/test_set.cpp b/test/test_set.cpp index 7d775fe870..ab19fbc4a9 100644 --- a/test/test_set.cpp +++ b/test/test_set.cpp @@ -241,7 +241,7 @@ TEST(Set_Links) { Group g; auto foos = g.add_table("class_Foo"); - auto bars = g.add_table("class_Bar"); + auto bars = g.add_table_with_primary_key("class_Bar", type_Int, "id"); auto cabs = g.add_table("class_Cab"); ColKey col_links = foos->add_column_set(*bars, "links"); @@ -249,10 +249,10 @@ TEST(Set_Links) auto foo = foos->create_object(); - auto bar1 = bars->create_object(); - auto bar2 = bars->create_object(); - auto bar3 = bars->create_object(); - auto bar4 = bars->create_object(); + auto bar1 = bars->create_object_with_primary_key(1); + auto bar2 = bars->create_object_with_primary_key(2); + auto bar3 = bars->create_object_with_primary_key(3); + auto bar4 = bars->create_object_with_primary_key(4); auto cab1 = cabs->create_object(); auto cab2 = cabs->create_object(); @@ -500,13 +500,13 @@ TEST(Set_LnkSetUnresolved) { Group g; auto foos = g.add_table("class_Foo"); - auto bars = g.add_table("class_Bar"); + auto bars = g.add_table_with_primary_key("class_Bar", type_Int, "id"); ColKey col_links = foos->add_column_set(*bars, "links"); auto foo = foos->create_object(); - auto bar1 = bars->create_object(); - auto bar2 = bars->create_object(); - auto bar3 = bars->create_object(); + auto bar1 = bars->create_object_with_primary_key(1); + auto bar2 = bars->create_object_with_primary_key(2); + auto bar3 = bars->create_object_with_primary_key(3); auto key_set = foo.get_set(col_links); auto link_set = foo.get_linkset(col_links); diff --git a/test/test_unresolved_links.cpp b/test/test_unresolved_links.cpp index adaf698113..426e05e9de 100644 --- a/test/test_unresolved_links.cpp +++ b/test/test_unresolved_links.cpp @@ -65,7 +65,7 @@ TEST(Unresolved_Basic) col_owns = persons->add_column(*cars, "car"); auto dealers = wt->add_table_with_primary_key("Dealer", type_Int, "cvr"); col_has = dealers->add_column_list(*cars, "stock"); - auto parts = wt->add_table("Parts"); // No primary key + auto parts = wt->add_table_with_primary_key("Parts", type_String, "id"); col_part = cars->add_column(*parts, "part"); auto finn = persons->create_object_with_primary_key("finn.schiermer-andersen@mongodb.com"); @@ -77,7 +77,7 @@ TEST(Unresolved_Basic) auto stock = joergen.get_list(col_has); auto skoda = cars->create_object_with_primary_key("Skoda Fabia").set(col_price, Decimal128("149999.5")); - auto thingamajig = parts->create_object(); + auto thingamajig = parts->create_object_with_primary_key("abc-123"); skoda.set(col_part, thingamajig.get_key()); auto new_tesla = cars->get_objkey_from_primary_key("Tesla 10"); @@ -90,7 +90,7 @@ TEST(Unresolved_Basic) stock.insert(1, skoda.get_key()); // Create a tombstone implicitly - auto doodad = parts->get_objkey_from_global_key(GlobalKey{999, 999}); + auto doodad = parts->get_objkey_from_primary_key("def-123"); CHECK(doodad.is_unresolved()); CHECK_EQUAL(parts->nb_unresolved(), 1); @@ -150,12 +150,11 @@ TEST(Unresolved_Basic) auto parts = wt->get_table("Parts"); auto tesla = wt->get_table("Car")->create_object_with_primary_key("Tesla 10"); tesla.set(col_price, Decimal128("499999.5")); - auto doodad = parts->create_object(GlobalKey{999, 999}); - auto doodad1 = parts->create_object(GlobalKey{999, 999}); // Check idempotency + auto doodad = parts->create_object_with_primary_key("def-123"); + auto doodad1 = parts->create_object_with_primary_key("def-123"); // Check idempotency CHECK_EQUAL(doodad.get_key(), doodad1.get_key()); - CHECK_EQUAL(doodad.get_object_id(), doodad1.get_object_id()); tesla.set(col_part, doodad.get_key()); - auto doodad_key = parts->get_objkey_from_global_key(GlobalKey{999, 999}); + auto doodad_key = parts->get_objkey_from_primary_key("def-123"); CHECK(!doodad_key.is_unresolved()); CHECK_EQUAL(wt->get_table("Parts")->nb_unresolved(), 0); @@ -177,13 +176,13 @@ TEST(Unresolved_InvalidateObject) auto cars = g.add_table_with_primary_key("Car", type_String, "model"); auto col_wheels = cars->add_column_list(*wheels, "wheels"); auto col_price = cars->add_column(type_Decimal, "price"); - auto dealers = g.add_table("Dealer"); + auto dealers = g.add_table_with_primary_key("Dealer", type_Int, "id"); auto col_has = dealers->add_column_list(*cars, "stock"); auto organization = g.add_table("Organization"); auto col_members = organization->add_column_list(*dealers, "members"); - auto dealer1 = dealers->create_object(); - auto dealer2 = dealers->create_object(); + auto dealer1 = dealers->create_object_with_primary_key(1); + auto dealer2 = dealers->create_object_with_primary_key(2); auto org = organization->create_object(); auto members = org.get_linklist(col_members);