gristlabs_grist-core/sandbox/grist/reverse_references.py
Jarosław Sadziński 1d2cf3de49 (core) Adding backend for 2-way references
Summary:
Adding support for 2-way references in data engine.

- Columns have an `reverseCol` field, which says "this is a reverse of the given column, update me when that one changes".
- At the time of setting `reverseCol`, we ensure that it's symmetrical to make a 2-way reference.
- Elsewhere we just implement syncing in one direction:
  - When `reverseCol` is present, user code is generated with a type like `grist.ReferenceList("Tasks", reverse_of="Assignee")`
  - On updating a ref column, we use `prepare_new_values()` method to generate corresponding updates to any column that's a reverse of it.
  - The `prepare_new_values()` approach is extended to support this.
  - We don't add (or remove) any mappings between rows, and rely on existing mappings (in a ref column's `_relation`) to create reverse updates.

NOTE This is polished version of https://phab.getgrist.com/D4307 with tests and 3 bug fixes
- Column transformation didn't work when transforming RefList to Ref, the reverse column became out of sync
- Tables with reverse columns couldn't be removed
- Setting json arrays to RefList didn't work if arrays contained other things besides ints
Those fixes are covered by new tests.

Test Plan: New tests

Reviewers: georgegevoian, paulfitz, dsagal

Reviewed By: georgegevoian, paulfitz

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D4322
2024-09-11 22:31:36 +02:00

74 lines
3.0 KiB
Python

from collections import defaultdict
import six
from usertypes import get_referenced_table_id
class _RefUpdates(object):
def __init__(self):
self.removals = set()
self.additions = set()
def get_reverse_adjustments(row_ids, old_values, new_values, value_iterator, relation):
"""
Generates data for updating reverse columns, based on changes to this column
"""
# Stores removals and addons for each target row
affected_target_rows = defaultdict(_RefUpdates)
# Iterate over changes to source column (my column)
for (source_row_id, old_value, new_value) in zip(row_ids, old_values, new_values):
if new_value != old_value:
# Treat old_values as removals, and new_values as additions
for target_row_id in value_iterator(old_value):
affected_target_rows[target_row_id].removals.add(source_row_id)
for target_row_id in value_iterator(new_value):
affected_target_rows[target_row_id].additions.add(source_row_id)
# Now in affected_target_rows, we have the changes (deltas), now we are going to convert them
# to updates (full list of values) in target columns.
adjustments = []
# For each target row (that needs to be updated, and was change in our column)
for target_row_id, updates in six.iteritems(affected_target_rows):
# Get the value stored in that column by using our own relation object (which should store
# correct values - the same that are stored in that reverse column). `reverse_value` is the
# value in that reverse cell
reverse_value = relation.get_affected_rows((target_row_id,))
# Now make the adjustments using calculated deltas
for source_row_id in updates.removals:
reverse_value.discard(source_row_id)
for source_row_id in updates.additions:
reverse_value.add(source_row_id)
adjustments.append((target_row_id, sorted(reverse_value)))
return adjustments
def check_desired_reverse_col(col_type, desired_reverse_col):
if not desired_reverse_col:
raise ValueError("invalid column specified in reverseCol")
if desired_reverse_col.reverseCol:
raise ValueError("reverseCol specifies an existing two-way reference column")
ref_table_id = get_referenced_table_id(col_type)
if not ref_table_id:
raise ValueError("reverseCol may only be set on a column with a reference type")
if desired_reverse_col.tableId != ref_table_id:
raise ValueError("reverseCol must be a column in the target table")
def pick_reverse_col_label(docmodel, col_rec):
ref_table_id = get_referenced_table_id(col_rec.type)
ref_table_rec = docmodel.get_table_rec(ref_table_id)
# First try the source table title.
source_table_rec = col_rec.parentId
reverse_label = source_table_rec.rawViewSectionRef.title or source_table_rec.tableId
# If that name already exists (as a label), add the source column's name as a suffix.
avoid_set = set(c.label for c in ref_table_rec.columns)
if reverse_label in avoid_set:
return reverse_label + "-" + (col_rec.label or col_rec.colId)
return reverse_label