mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) move data engine code to core
Summary: this moves sandbox/grist to core, and adds a requirements.txt file for reconstructing the content of sandbox/thirdparty. Test Plan: existing tests pass. Tested core functionality manually. Tested docker build manually. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2563
This commit is contained in:
122
sandbox/grist/relation.py
Normal file
122
sandbox/grist/relation.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
A Relation represent mapping between rows, and used in determining which rows need to be
|
||||
recomputed when something changes.
|
||||
|
||||
Relations can be determined by a foreign key or another form of lookup, and they may be composed.
|
||||
|
||||
For example, if Person.zip is the formula 'rec.school.address.zip', it involves three Relations:
|
||||
ReferenceRelation between Person and School tables, another ReferenceRelation between School and
|
||||
Address tables. Together, they form ComposedRelation relation between Person and Address tables.
|
||||
|
||||
"""
|
||||
import depend
|
||||
|
||||
class Relation(object):
|
||||
"""
|
||||
Represents a row mapping between two tables. The arguments are table IDs (not actual tables).
|
||||
"""
|
||||
def __init__(self, referring_table, target_table):
|
||||
self.referring_table = referring_table
|
||||
self.target_table = target_table
|
||||
|
||||
# Maps the relation objects that we wrap to the resulting composed relations.
|
||||
self._target_relations = {}
|
||||
|
||||
def get_affected_rows(self, input_rows):
|
||||
"""
|
||||
Given an iterable over input (dependency) rows, returns a `set` of output (dependent) rows.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def reset_all(self):
|
||||
"""
|
||||
Called when the dependency using this relation is reset, and this relation is no longer used.
|
||||
"""
|
||||
self.reset_rows(depend.ALL_ROWS)
|
||||
|
||||
def reset_rows(self, referring_rows):
|
||||
"""
|
||||
Call when starting to compute a formula to tell a Relation that it can start with a clean
|
||||
slate for all row_ids in the passed-in iterable.
|
||||
"""
|
||||
pass
|
||||
|
||||
def compose(self, other_relation):
|
||||
r = self._target_relations.get(other_relation)
|
||||
if r is None:
|
||||
r = self._target_relations[other_relation] = ComposedRelation(self, other_relation)
|
||||
return r
|
||||
|
||||
class IdentityRelation(Relation):
|
||||
"""
|
||||
The trivial mapping, used to represent the relation between fields of the same record.
|
||||
"""
|
||||
def __init__(self, table_id):
|
||||
super(IdentityRelation, self).__init__(table_id, table_id)
|
||||
|
||||
def get_affected_rows(self, input_rows):
|
||||
return input_rows
|
||||
|
||||
def __str__(self):
|
||||
return "Identity(%s)" % self.referring_table
|
||||
|
||||
# Important: we intentionally do not optimize compose() for an IdentityRelation, since
|
||||
# (Identity + Rel) is not the same Rel when it comes to reset_rows() calls. [See test_lookups.py
|
||||
# test_dependencies_relations_bug for a detailed description of a bug this can cause.]
|
||||
|
||||
|
||||
class ComposedRelation(Relation):
|
||||
"""
|
||||
Represents a composition of two Relations. E.g. if referring side maps Students to Schools, and
|
||||
target_side maps Schools to Addresses, then the composition maps Students to Addresses (so a
|
||||
Student records depend on Address records, and changes to Address records affect Students).
|
||||
"""
|
||||
def __init__(self, referring_side, target_side):
|
||||
assert referring_side.target_table == target_side.referring_table
|
||||
super(ComposedRelation, self).__init__(referring_side.referring_table,
|
||||
target_side.target_table)
|
||||
self.source_relation = referring_side
|
||||
self.target_relation = target_side
|
||||
|
||||
def get_affected_rows(self, input_rows):
|
||||
return self.source_relation.get_affected_rows(
|
||||
self.target_relation.get_affected_rows(input_rows))
|
||||
|
||||
def reset_rows(self, referring_rows):
|
||||
# In the example from the doc-string, this says that certain Students are being recomputed, so
|
||||
# no longer refer to any Schools. It doesn't say anything about Schools' dependence on
|
||||
# Addresses, so there is nothing to reset in self.target_relation.
|
||||
self.source_relation.reset_rows(referring_rows)
|
||||
|
||||
def __str__(self):
|
||||
return "%s + %s" % (self.source_relation, self.target_relation)
|
||||
|
||||
|
||||
class ReferenceRelation(Relation):
|
||||
"""
|
||||
Base class for Relations between records in two tables.
|
||||
"""
|
||||
def __init__(self, referring_table, target_table, ref_col_id):
|
||||
super(ReferenceRelation, self).__init__(referring_table, target_table)
|
||||
self.inverse_map = {} # maps target rows to sets of referring rows
|
||||
self._ref_col_id = ref_col_id
|
||||
|
||||
def __str__(self):
|
||||
return "ReferenceRelation(%s.%s)" % (self.referring_table, self._ref_col_id)
|
||||
|
||||
def get_affected_rows(self, input_rows):
|
||||
# Each input row (target of the reference link) may be pointed to by multiple references,
|
||||
# so we need to take the union of all of those sets.
|
||||
affected_rows = set()
|
||||
for target_row_id in input_rows:
|
||||
affected_rows.update(self.inverse_map.get(target_row_id, ()))
|
||||
return affected_rows
|
||||
|
||||
def add_reference(self, referring_row_id, target_row_id):
|
||||
self.inverse_map.setdefault(target_row_id, set()).add(referring_row_id)
|
||||
|
||||
def remove_reference(self, referring_row_id, target_row_id):
|
||||
self.inverse_map[target_row_id].remove(referring_row_id)
|
||||
|
||||
def clear(self):
|
||||
self.inverse_map.clear()
|
||||
Reference in New Issue
Block a user