added iOS source code
[wl-app.git] / iOS / Pods / Realm / Realm / ObjectStore / src / impl / transact_log_handler.cpp
diff --git a/iOS/Pods/Realm/Realm/ObjectStore/src/impl/transact_log_handler.cpp b/iOS/Pods/Realm/Realm/ObjectStore/src/impl/transact_log_handler.cpp
new file mode 100644 (file)
index 0000000..ae4a668
--- /dev/null
@@ -0,0 +1,862 @@
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2015 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 "impl/transact_log_handler.hpp"
+
+#include "binding_context.hpp"
+#include "impl/collection_notifier.hpp"
+#include "index_set.hpp"
+#include "shared_realm.hpp"
+
+#include <realm/group_shared.hpp>
+#include <realm/lang_bind_helper.hpp>
+
+#include <algorithm>
+#include <numeric>
+
+using namespace realm;
+
+namespace {
+
+class KVOAdapter : public _impl::TransactionChangeInfo {
+public:
+    KVOAdapter(std::vector<BindingContext::ObserverState>& observers, BindingContext* context);
+
+    void before(SharedGroup& sg);
+    void after(SharedGroup& sg);
+
+private:
+    BindingContext* m_context;
+    std::vector<BindingContext::ObserverState>& m_observers;
+    std::vector<void *> m_invalidated;
+
+    struct ListInfo {
+        BindingContext::ObserverState* observer;
+        _impl::CollectionChangeBuilder builder;
+        size_t col;
+        size_t initial_size;
+    };
+    std::vector<ListInfo> m_lists;
+    VersionID m_version;
+
+    size_t new_table_ndx(size_t ndx) const { return ndx < table_indices.size() ? table_indices[ndx] : ndx; }
+};
+
+KVOAdapter::KVOAdapter(std::vector<BindingContext::ObserverState>& observers, BindingContext* context)
+: _impl::TransactionChangeInfo{}
+, m_context(context)
+, m_observers(observers)
+{
+    if (m_observers.empty())
+        return;
+
+    std::vector<size_t> tables_needed;
+    for (auto& observer : observers) {
+        tables_needed.push_back(observer.table_ndx);
+    }
+    std::sort(begin(tables_needed), end(tables_needed));
+    tables_needed.erase(std::unique(begin(tables_needed), end(tables_needed)), end(tables_needed));
+
+    auto realm = context->realm.lock();
+    auto& group = realm->read_group();
+    for (auto& observer : observers) {
+        auto table = group.get_table(observer.table_ndx);
+        for (size_t i = 0, count = table->get_column_count(); i < count; ++i) {
+            auto type = table->get_column_type(i);
+            if (type == type_LinkList)
+                m_lists.push_back({&observer, {}, i, size_t(-1)});
+            else if (type == type_Table)
+                m_lists.push_back({&observer, {}, i, table->get_subtable_size(i, observer.row_ndx)});
+        }
+    }
+
+    auto max = std::max_element(begin(tables_needed), end(tables_needed));
+    if (*max >= table_modifications_needed.size())
+        table_modifications_needed.resize(*max + 1, false);
+    if (*max >= table_moves_needed.size())
+        table_moves_needed.resize(*max + 1, false);
+    for (auto& tbl : tables_needed) {
+        table_modifications_needed[tbl] = true;
+        table_moves_needed[tbl] = true;
+    }
+    for (auto& list : m_lists)
+        lists.push_back({list.observer->table_ndx, list.observer->row_ndx, list.col, &list.builder});
+}
+
+void KVOAdapter::before(SharedGroup& sg)
+{
+    if (!m_context)
+        return;
+
+    m_version = sg.get_version_of_current_transaction();
+    if (tables.empty())
+        return;
+
+    for (auto& observer : m_observers) {
+        size_t table_ndx = new_table_ndx(observer.table_ndx);
+        if (table_ndx >= tables.size())
+            continue;
+
+        auto const& table = tables[table_ndx];
+        auto const& moves = table.moves;
+        auto idx = observer.row_ndx;
+        auto it = lower_bound(begin(moves), end(moves), idx,
+                              [](auto const& a, auto b) { return a.from < b; });
+        if (it != moves.end() && it->from == idx)
+            idx = it->to;
+        else if (table.deletions.contains(idx)) {
+            m_invalidated.push_back(observer.info);
+            continue;
+        }
+        else
+            idx = table.insertions.shift(table.deletions.unshift(idx));
+        if (table.modifications.contains(idx)) {
+            observer.changes.resize(table.columns.size());
+            size_t i = 0;
+            for (auto& c : table.columns) {
+                auto& change = observer.changes[i];
+                if (table_ndx >= column_indices.size() || column_indices[table_ndx].empty())
+                    change.initial_column_index = i;
+                else if (i >= column_indices[table_ndx].size())
+                    change.initial_column_index = i - column_indices[table_ndx].size() + column_indices[table_ndx].back() + 1;
+                else
+                    change.initial_column_index = column_indices[table_ndx][i];
+                if (change.initial_column_index != npos && c.contains(idx))
+                    change.kind = BindingContext::ColumnInfo::Kind::Set;
+                ++i;
+            }
+        }
+    }
+
+    for (auto& list : m_lists) {
+        if (list.builder.empty()) {
+            // We may have pre-emptively marked the column as modified if the
+            // LinkList was selected but the actual changes made ended up being
+            // a no-op
+            if (list.col < list.observer->changes.size())
+                list.observer->changes[list.col].kind = BindingContext::ColumnInfo::Kind::None;
+            continue;
+        }
+        // If the containing row was deleted then changes will be empty
+        if (list.observer->changes.empty()) {
+            REALM_ASSERT_DEBUG(tables[new_table_ndx(list.observer->table_ndx)].deletions.contains(list.observer->row_ndx));
+            continue;
+        }
+        // otherwise the column should have been marked as modified
+        REALM_ASSERT(list.col < list.observer->changes.size());
+        auto& builder = list.builder;
+        auto& changes = list.observer->changes[list.col];
+
+        builder.modifications.remove(builder.insertions);
+
+        // KVO can't express moves (becaue NSArray doesn't have them), so
+        // transform them into a series of sets on each affected index when possible
+        if (!builder.moves.empty() && builder.insertions.count() == builder.moves.size() && builder.deletions.count() == builder.moves.size()) {
+            changes.kind = BindingContext::ColumnInfo::Kind::Set;
+            changes.indices = builder.modifications;
+            changes.indices.add(builder.deletions);
+
+            // Iterate over each of the rows which may have been shifted by
+            // the moves and check if it actually has been, or if it's ended
+            // up in the same place as it started (either because the moves were
+            // actually a swap that doesn't effect the rows in between, or the
+            // combination of moves happen to leave some intermediate rows in
+            // the same place)
+            auto in_range = [](auto& it, auto end, size_t i) {
+                if (it != end && i >= it->second)
+                    ++it;
+                return it != end && i >= it->first && i < it->second;
+            };
+
+            auto del_it = builder.deletions.begin(), del_end = builder.deletions.end();
+            auto ins_it = builder.insertions.begin(), ins_end = builder.insertions.end();
+            size_t start = std::min(ins_it->first, del_it->first);
+            size_t end = std::max(std::prev(ins_end)->second, std::prev(del_end)->second);
+            ptrdiff_t shift = 0;
+            for (size_t i = start; i < end; ++i) {
+                if (in_range(del_it, del_end, i))
+                    --shift;
+                else if (in_range(ins_it, ins_end, i + shift))
+                    ++shift;
+                if (shift != 0)
+                    changes.indices.add(i);
+            }
+        }
+        // KVO can't express multiple types of changes at once
+        else if (builder.insertions.empty() + builder.modifications.empty() + builder.deletions.empty() < 2) {
+            changes.kind = BindingContext::ColumnInfo::Kind::SetAll;
+        }
+        else if (!builder.insertions.empty()) {
+            changes.kind = BindingContext::ColumnInfo::Kind::Insert;
+            changes.indices = builder.insertions;
+        }
+        else if (!builder.modifications.empty()) {
+            changes.kind = BindingContext::ColumnInfo::Kind::Set;
+            changes.indices = builder.modifications;
+        }
+        else {
+            REALM_ASSERT(!builder.deletions.empty());
+            changes.kind = BindingContext::ColumnInfo::Kind::Remove;
+            // Table clears don't come with the size, so we need to fix up the
+            // notification to make it just delete all rows that actually existed
+            if (std::prev(builder.deletions.end())->second > list.initial_size)
+                changes.indices.set(list.initial_size);
+            else
+                changes.indices = builder.deletions;
+        }
+    }
+    m_context->will_change(m_observers, m_invalidated);
+}
+
+void KVOAdapter::after(SharedGroup& sg)
+{
+    if (!m_context)
+        return;
+    m_context->did_change(m_observers, m_invalidated,
+                          m_version != VersionID{} &&
+                          m_version != sg.get_version_of_current_transaction());
+}
+
+template<typename Derived>
+struct MarkDirtyMixin  {
+    bool mark_dirty(size_t row, size_t col, _impl::Instruction instr=_impl::instr_Set)
+    {
+        // Ignore SetDefault and SetUnique as those conceptually cannot be
+        // changes to existing rows
+        if (instr == _impl::instr_Set)
+            static_cast<Derived *>(this)->mark_dirty(row, col);
+        return true;
+    }
+
+    bool set_int(size_t c, size_t r, int_fast64_t, _impl::Instruction i, size_t) { return mark_dirty(r, c, i); }
+    bool set_bool(size_t c, size_t r, bool, _impl::Instruction i) { return mark_dirty(r, c, i); }
+    bool set_float(size_t c, size_t r, float, _impl::Instruction i) { return mark_dirty(r, c, i); }
+    bool set_double(size_t c, size_t r, double, _impl::Instruction i) { return mark_dirty(r, c, i); }
+    bool set_string(size_t c, size_t r, StringData, _impl::Instruction i, size_t) { return mark_dirty(r, c, i); }
+    bool set_binary(size_t c, size_t r, BinaryData, _impl::Instruction i) { return mark_dirty(r, c, i); }
+    bool set_olddatetime(size_t c, size_t r, OldDateTime, _impl::Instruction i) { return mark_dirty(r, c, i); }
+    bool set_timestamp(size_t c, size_t r, Timestamp, _impl::Instruction i) { return mark_dirty(r, c, i); }
+    bool set_table(size_t c, size_t r, _impl::Instruction i) { return mark_dirty(r, c, i); }
+    bool set_mixed(size_t c, size_t r, const Mixed&, _impl::Instruction i) { return mark_dirty(r, c, i); }
+    bool set_link(size_t c, size_t r, size_t, size_t, _impl::Instruction i) { return mark_dirty(r, c, i); }
+    bool set_null(size_t c, size_t r, _impl::Instruction i, size_t) { return mark_dirty(r, c, i); }
+
+    bool add_int(size_t col, size_t row, int_fast64_t) { return mark_dirty(row, col); }
+    bool nullify_link(size_t col, size_t row, size_t) { return mark_dirty(row, col); }
+    bool insert_substring(size_t col, size_t row, size_t, StringData) { return mark_dirty(row, col); }
+    bool erase_substring(size_t col, size_t row, size_t, size_t) { return mark_dirty(row, col); }
+
+    bool set_int_unique(size_t, size_t, size_t, int_fast64_t) { return true; }
+    bool set_string_unique(size_t, size_t, size_t, StringData) { return true; }
+
+    bool add_row_with_key(size_t, size_t, size_t, int64_t) { return true; }
+};
+
+class TransactLogValidationMixin {
+    // Index of currently selected table
+    size_t m_current_table = 0;
+
+    REALM_NORETURN
+    REALM_NOINLINE
+    void schema_error()
+    {
+        throw std::logic_error("Schema mismatch detected: another process has modified the Realm file's schema in an incompatible way");
+    }
+
+protected:
+    size_t current_table() const noexcept { return m_current_table; }
+
+public:
+
+    bool select_table(size_t group_level_ndx, int, const size_t*) noexcept
+    {
+        m_current_table = group_level_ndx;
+        return true;
+    }
+
+    // Removing or renaming things while a Realm is open is never supported
+    bool erase_group_level_table(size_t, size_t) { schema_error(); }
+    bool rename_group_level_table(size_t, StringData) { schema_error(); }
+    bool erase_column(size_t) { schema_error(); }
+    bool erase_link_column(size_t, size_t, size_t) { schema_error(); }
+    bool rename_column(size_t, StringData) { schema_error(); }
+
+    // Schema changes which don't involve a change in the schema version are
+    // allowed
+    bool add_search_index(size_t) { return true; }
+    bool remove_search_index(size_t) { return true; }
+
+    // Additive changes and reorderings are supported
+    bool insert_group_level_table(size_t, size_t, StringData) { return true; }
+    bool insert_column(size_t, DataType, StringData, bool) { return true; }
+    bool insert_link_column(size_t, DataType, StringData, size_t, size_t) { return true; }
+    bool set_link_type(size_t, LinkType) { return true; }
+    bool move_column(size_t, size_t) { return true; }
+    bool move_group_level_table(size_t, size_t) { return true; }
+
+    // Non-schema changes are all allowed
+    void parse_complete() { }
+    bool select_descriptor(int, const size_t*) { return true; }
+    bool select_link_list(size_t, size_t, size_t) { return true; }
+    bool insert_empty_rows(size_t, size_t, size_t, bool) { return true; }
+    bool erase_rows(size_t, size_t, size_t, bool) { return true; }
+    bool swap_rows(size_t, size_t) { return true; }
+    bool move_row(size_t, size_t) { return true; }
+    bool clear_table(size_t=0) noexcept { return true; }
+    bool link_list_set(size_t, size_t, size_t) { return true; }
+    bool link_list_insert(size_t, size_t, size_t) { return true; }
+    bool link_list_erase(size_t, size_t) { return true; }
+    bool link_list_nullify(size_t, size_t) { return true; }
+    bool link_list_clear(size_t) { return true; }
+    bool link_list_move(size_t, size_t) { return true; }
+    bool link_list_swap(size_t, size_t) { return true; }
+    bool merge_rows(size_t, size_t) { return true; }
+    bool optimize_table() { return true; }
+};
+
+
+// A transaction log handler that just validates that all operations made are
+// ones supported by the object store
+struct TransactLogValidator : public TransactLogValidationMixin, public MarkDirtyMixin<TransactLogValidator> {
+    void mark_dirty(size_t, size_t) { }
+};
+
+// Move the value at container[from] to container[to], shifting everything in
+// between, or do nothing if either are out of bounds
+template<typename Container>
+void rotate(Container& container, size_t from, size_t to)
+{
+    REALM_ASSERT(from != to);
+    if (from >= container.size() && to >= container.size())
+        return;
+    if (from >= container.size() || to >= container.size())
+        container.resize(std::max(from, to) + 1);
+    if (from < to)
+        std::rotate(begin(container) + from, begin(container) + from + 1, begin(container) + to + 1);
+    else
+        std::rotate(begin(container) + to, begin(container) + from, begin(container) + from + 1);
+}
+
+// Insert a default-initialized value at pos if there is anything after pos in the container.
+template<typename Container>
+void insert_empty_at(Container& container, size_t pos)
+{
+    if (pos < container.size())
+        container.emplace(container.begin() + pos);
+}
+
+// Shift `value` to reflect a move from `from` to `to`
+void adjust_for_move(size_t& value, size_t from, size_t to)
+{
+    if (value == from)
+        value = to;
+    else if (value > from && value <= to)
+        --value;
+    else if (value < from && value >= to)
+        ++value;
+}
+
+void adjust_for_move(std::vector<size_t>& values, size_t from, size_t to)
+{
+    for (auto& value : values)
+        adjust_for_move(value, from, to);
+}
+
+void expand_to(std::vector<size_t>& cols, size_t i)
+{
+    auto old_size = cols.size();
+    if (old_size > i)
+        return;
+
+    cols.resize(std::max(old_size * 2, i + 1));
+    std::iota(begin(cols) + old_size, end(cols), old_size == 0 ? 0 : cols[old_size - 1] + 1);
+}
+
+void adjust_ge(std::vector<size_t>& values, size_t i)
+{
+    for (auto& value : values) {
+        if (value >= i)
+            ++value;
+    }
+}
+
+// Extends TransactLogValidator to track changes made to LinkViews
+class TransactLogObserver : public TransactLogValidationMixin, public MarkDirtyMixin<TransactLogObserver> {
+    _impl::TransactionChangeInfo& m_info;
+    _impl::CollectionChangeBuilder* m_active_list = nullptr;
+    _impl::CollectionChangeBuilder* m_active_table = nullptr;
+    _impl::CollectionChangeBuilder* m_active_descriptor = nullptr;
+
+    bool m_need_move_info = false;
+    bool m_is_top_level_table = true;
+
+    _impl::CollectionChangeBuilder* find_list(size_t tbl, size_t col, size_t row)
+    {
+        // When there are multiple source versions there could be multiple
+        // change objects for a single LinkView, in which case we need to use
+        // the last one
+        for (auto it = m_info.lists.rbegin(), end = m_info.lists.rend(); it != end; ++it) {
+            if (it->table_ndx == tbl && it->row_ndx == row && it->col_ndx == col)
+                return it->changes;
+        }
+        return nullptr;
+    }
+
+public:
+    TransactLogObserver(_impl::TransactionChangeInfo& info)
+    : m_info(info) { }
+
+    void mark_dirty(size_t row, size_t col)
+    {
+        if (m_active_table)
+            m_active_table->modify(row, col);
+    }
+
+    void parse_complete()
+    {
+        for (auto& table : m_info.tables)
+            table.parse_complete();
+        for (auto& list : m_info.lists)
+            list.changes->clean_up_stale_moves();
+    }
+
+    bool select_descriptor(int levels, const size_t*) noexcept
+    {
+        if (levels == 0) // schema of selected table is being modified
+            m_active_descriptor = m_active_table;
+        else // schema of subtable is being modified; currently don't need to track this
+            m_active_descriptor = nullptr;
+        return true;
+    }
+
+    bool select_table(size_t group_level_ndx, int len, size_t const* path) noexcept
+    {
+        TransactLogValidationMixin::select_table(group_level_ndx, len, path);
+        m_active_table = nullptr;
+        m_is_top_level_table = true;
+
+        // Nested subtables currently not supported
+        if (len > 1) {
+            m_is_top_level_table = false;
+            return true;
+        }
+
+        auto tbl_ndx = current_table();
+        if (!m_info.track_all && (tbl_ndx >= m_info.table_modifications_needed.size() || !m_info.table_modifications_needed[tbl_ndx]))
+            return true;
+
+        m_need_move_info = m_info.track_all || (tbl_ndx < m_info.table_moves_needed.size() &&
+                                                m_info.table_moves_needed[tbl_ndx]);
+        if (m_info.tables.size() <= tbl_ndx)
+            m_info.tables.resize(std::max(m_info.tables.size() * 2, tbl_ndx + 1));
+        m_active_table = &m_info.tables[tbl_ndx];
+
+        if (len == 1) {
+            // Mark the cell containing the subtable as modified since selecting
+            // a table is always followed by a modification of some sort
+            size_t col = path[0];
+            size_t row = path[1];
+            mark_dirty(row, col);
+
+            m_active_table = nullptr;
+            m_is_top_level_table = false;
+            if (auto table = find_list(current_table(), col, row)) {
+                m_active_table = table;
+                m_need_move_info = true;
+            }
+        }
+        return true;
+    }
+
+    bool select_link_list(size_t col, size_t row, size_t)
+    {
+        mark_dirty(row, col);
+        m_active_list = find_list(current_table(), col, row);
+        return true;
+    }
+
+    bool link_list_set(size_t index, size_t, size_t)
+    {
+        if (m_active_list)
+            m_active_list->modify(index);
+        return true;
+    }
+
+    bool link_list_insert(size_t index, size_t, size_t)
+    {
+        if (m_active_list)
+            m_active_list->insert(index);
+        return true;
+    }
+
+    bool link_list_erase(size_t index, size_t)
+    {
+        if (m_active_list)
+            m_active_list->erase(index);
+        return true;
+    }
+
+    bool link_list_nullify(size_t index, size_t prior_size)
+    {
+        return link_list_erase(index, prior_size);
+    }
+
+    bool link_list_swap(size_t index1, size_t index2)
+    {
+        link_list_set(index1, 0, npos);
+        link_list_set(index2, 0, npos);
+        return true;
+    }
+
+    bool link_list_clear(size_t old_size)
+    {
+        if (m_active_list)
+            m_active_list->clear(old_size);
+        return true;
+    }
+
+    bool link_list_move(size_t from, size_t to)
+    {
+        if (m_active_list)
+            m_active_list->move(from, to);
+        return true;
+    }
+
+    bool insert_empty_rows(size_t row_ndx, size_t num_rows_to_insert, size_t, bool)
+    {
+        if (m_active_table)
+            m_active_table->insert(row_ndx, num_rows_to_insert, m_need_move_info);
+        if (!m_is_top_level_table)
+            return true;
+        for (auto& list : m_info.lists) {
+            if (list.table_ndx == current_table() && list.row_ndx >= row_ndx)
+                list.row_ndx += num_rows_to_insert;
+        }
+        return true;
+    }
+
+    bool add_row_with_key(size_t row_ndx, size_t prior_num_rows, size_t, int64_t)
+    {
+        insert_empty_rows(row_ndx, 1, prior_num_rows, false);
+        return true;
+    }
+
+    bool erase_rows(size_t row_ndx, size_t, size_t prior_num_rows, bool unordered)
+    {
+        if (!unordered) {
+            if (m_active_table)
+                m_active_table->erase(row_ndx);
+            return true;
+        }
+        size_t last_row = prior_num_rows - 1;
+        if (m_active_table)
+            m_active_table->move_over(row_ndx, last_row, m_need_move_info);
+
+        if (!m_is_top_level_table)
+            return true;
+        for (size_t i = 0; i < m_info.lists.size(); ++i) {
+            auto& list = m_info.lists[i];
+            if (list.table_ndx != current_table())
+                continue;
+            if (list.row_ndx == row_ndx) {
+                if (i + 1 < m_info.lists.size())
+                    m_info.lists[i] = std::move(m_info.lists.back());
+                m_info.lists.pop_back();
+                continue;
+            }
+            if (list.row_ndx == last_row)
+                list.row_ndx = row_ndx;
+        }
+
+        return true;
+    }
+
+    bool swap_rows(size_t row_ndx_1, size_t row_ndx_2) {
+        REALM_ASSERT(row_ndx_1 < row_ndx_2);
+        if (!m_is_top_level_table) {
+            if (m_active_table) {
+                m_active_table->move(row_ndx_1, row_ndx_2);
+                if (row_ndx_1 + 1 != row_ndx_2)
+                    m_active_table->move(row_ndx_2 - 1, row_ndx_1);
+            }
+            return true;
+        }
+
+        if (m_active_table)
+            m_active_table->swap(row_ndx_1, row_ndx_2, m_need_move_info);
+        for (auto& list : m_info.lists) {
+            if (list.table_ndx == current_table()) {
+                if (list.row_ndx == row_ndx_1)
+                    list.row_ndx = row_ndx_2;
+                else if (list.row_ndx == row_ndx_2)
+                    list.row_ndx = row_ndx_1;
+            }
+        }
+        return true;
+    }
+
+    bool move_row(size_t from_ndx, size_t to_ndx) {
+        // Move row is not supported for top level tables:
+        REALM_ASSERT(!m_active_table || !m_is_top_level_table);
+
+        if (m_active_table)
+            m_active_table->move(from_ndx, to_ndx);
+        return true;
+    }
+
+    bool merge_rows(size_t from, size_t to)
+    {
+        if (m_active_table)
+            m_active_table->subsume(from, to, m_need_move_info);
+        if (!m_is_top_level_table)
+            return true;
+        for (auto& list : m_info.lists) {
+            if (list.table_ndx == current_table() && list.row_ndx == from)
+                list.row_ndx = to;
+        }
+        return true;
+    }
+
+    bool clear_table(size_t=0)
+    {
+        auto tbl_ndx = current_table();
+        if (m_active_table)
+            m_active_table->clear(std::numeric_limits<size_t>::max());
+        if (!m_is_top_level_table)
+            return true;
+        auto it = remove_if(begin(m_info.lists), end(m_info.lists),
+                            [&](auto const& lv) { return lv.table_ndx == tbl_ndx; });
+        m_info.lists.erase(it, end(m_info.lists));
+        return true;
+    }
+
+    bool insert_column(size_t ndx, DataType, StringData, bool)
+    {
+        m_info.schema_changed = true;
+
+        if (m_active_descriptor)
+            m_active_descriptor->insert_column(ndx);
+        if (m_active_descriptor != m_active_table || !m_is_top_level_table)
+            return true;
+        for (auto& list : m_info.lists) {
+            if (list.table_ndx == current_table() && list.col_ndx >= ndx)
+                ++list.col_ndx;
+        }
+        if (m_info.column_indices.size() <= current_table())
+            m_info.column_indices.resize(current_table() + 1);
+        auto& indices = m_info.column_indices[current_table()];
+        expand_to(indices, ndx);
+        insert_empty_at(indices, ndx);
+        indices[ndx] = npos;
+        return true;
+    }
+
+    void prepare_table_indices()
+    {
+        if (m_info.table_indices.empty() && !m_info.table_modifications_needed.empty()) {
+            m_info.table_indices.resize(m_info.table_modifications_needed.size());
+            std::iota(begin(m_info.table_indices), end(m_info.table_indices), 0);
+        }
+    }
+
+    bool insert_group_level_table(size_t ndx, size_t, StringData)
+    {
+        m_info.schema_changed = true;
+
+        for (auto& list : m_info.lists) {
+            if (list.table_ndx >= ndx)
+                ++list.table_ndx;
+        }
+        prepare_table_indices();
+        adjust_ge(m_info.table_indices, ndx);
+        insert_empty_at(m_info.tables, ndx);
+        insert_empty_at(m_info.table_moves_needed, ndx);
+        insert_empty_at(m_info.table_modifications_needed, ndx);
+        return true;
+    }
+
+    bool move_column(size_t from, size_t to)
+    {
+        m_info.schema_changed = true;
+
+        if (m_active_descriptor)
+            m_active_descriptor->move_column(from, to);
+        if (m_active_descriptor != m_active_table || !m_is_top_level_table)
+            return true;
+        for (auto& list : m_info.lists) {
+            if (list.table_ndx == current_table())
+                adjust_for_move(list.col_ndx, from, to);
+        }
+        if (m_info.column_indices.size() <= current_table())
+            m_info.column_indices.resize(current_table() + 1);
+        expand_to(m_info.column_indices[current_table()], std::max(from, to) + 1);
+        rotate(m_info.column_indices[current_table()], from, to);
+        return true;
+    }
+
+    bool move_group_level_table(size_t from, size_t to)
+    {
+        m_info.schema_changed = true;
+
+        for (auto& list : m_info.lists)
+            adjust_for_move(list.table_ndx, from, to);
+
+        prepare_table_indices();
+        adjust_for_move(m_info.table_indices, from, to);
+        rotate(m_info.tables, from, to);
+        rotate(m_info.table_modifications_needed, from, to);
+        rotate(m_info.table_moves_needed, from, to);
+        return true;
+    }
+
+    bool insert_link_column(size_t ndx, DataType type, StringData name, size_t, size_t) { return insert_column(ndx, type, name, false); }
+};
+
+class KVOTransactLogObserver : public TransactLogObserver {
+    KVOAdapter m_adapter;
+    _impl::NotifierPackage& m_notifiers;
+    SharedGroup& m_sg;
+
+public:
+    KVOTransactLogObserver(std::vector<BindingContext::ObserverState>& observers,
+                           BindingContext* context,
+                           _impl::NotifierPackage& notifiers,
+                           SharedGroup& sg)
+    : TransactLogObserver(m_adapter)
+    , m_adapter(observers, context)
+    , m_notifiers(notifiers)
+    , m_sg(sg)
+    {
+    }
+
+    ~KVOTransactLogObserver()
+    {
+        m_adapter.after(m_sg);
+    }
+
+    void parse_complete()
+    {
+        TransactLogObserver::parse_complete();
+        m_adapter.before(m_sg);
+
+        using sgf = _impl::SharedGroupFriend;
+        m_notifiers.package_and_wait(sgf::get_version_of_latest_snapshot(m_sg));
+        m_notifiers.before_advance();
+    }
+};
+
+template<typename Func>
+void advance_with_notifications(BindingContext* context, const std::unique_ptr<SharedGroup>& sg,
+                                Func&& func, _impl::NotifierPackage& notifiers)
+{
+    auto old_version = sg->get_version_of_current_transaction();
+    std::vector<BindingContext::ObserverState> observers;
+    if (context) {
+        observers = context->get_observed_rows();
+    }
+
+    // Advancing to the latest version with notifiers requires using the full
+    // transaction log observer so that we have a point where we know what
+    // version we're going to before we actually advance to that version
+    if (observers.empty() && (!notifiers || notifiers.version())) {
+        notifiers.before_advance();
+        func(TransactLogValidator());
+        auto new_version = sg->get_version_of_current_transaction();
+        if (context && old_version != new_version)
+            context->did_change({}, {});
+        // did_change() could close the Realm. Just return if it does.
+        if (!sg)
+            return;
+        // did_change() can change the read version, and if it does we can't
+        // deliver notifiers
+        if (new_version == sg->get_version_of_current_transaction())
+            notifiers.deliver(*sg);
+        notifiers.after_advance();
+        return;
+    }
+
+    func(KVOTransactLogObserver(observers, context, notifiers, *sg));
+    notifiers.package_and_wait(sg->get_version_of_current_transaction().version); // is a no-op if parse_complete() was called
+    notifiers.deliver(*sg);
+    notifiers.after_advance();
+}
+
+} // anonymous namespace
+
+namespace realm {
+namespace _impl {
+
+namespace transaction {
+void advance(SharedGroup& sg, BindingContext*, VersionID version)
+{
+    LangBindHelper::advance_read(sg, TransactLogValidator(), version);
+}
+
+void advance(const std::unique_ptr<SharedGroup>& sg, BindingContext* context, NotifierPackage& notifiers)
+{
+    advance_with_notifications(context, sg, [&](auto&&... args) {
+        LangBindHelper::advance_read(*sg, std::move(args)..., notifiers.version().value_or(VersionID{}));
+    }, notifiers);
+}
+
+void begin_without_validation(SharedGroup& sg)
+{
+    LangBindHelper::promote_to_write(sg);
+}
+
+void begin(const std::unique_ptr<SharedGroup>& sg, BindingContext* context, NotifierPackage& notifiers)
+{
+    advance_with_notifications(context, sg, [&](auto&&... args) {
+        LangBindHelper::promote_to_write(*sg, std::move(args)...);
+    }, notifiers);
+}
+
+void commit(SharedGroup& sg)
+{
+    LangBindHelper::commit_and_continue_as_read(sg);
+}
+
+void cancel(SharedGroup& sg, BindingContext* context)
+{
+    std::vector<BindingContext::ObserverState> observers;
+    if (context) {
+        observers = context->get_observed_rows();
+    }
+    if (observers.empty()) {
+        LangBindHelper::rollback_and_continue_as_read(sg);
+        return;
+    }
+
+    _impl::NotifierPackage notifiers;
+    LangBindHelper::rollback_and_continue_as_read(sg, KVOTransactLogObserver(observers, context, notifiers, sg));
+}
+
+void advance(SharedGroup& sg, TransactionChangeInfo& info, VersionID version)
+{
+    if (!info.track_all && info.table_modifications_needed.empty() && info.lists.empty()) {
+        LangBindHelper::advance_read(sg, version);
+    }
+    else {
+        LangBindHelper::advance_read(sg, TransactLogObserver(info), version);
+    }
+
+}
+
+} // namespace transaction
+} // namespace _impl
+} // namespace realm