mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
b82eec714a
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
647 lines
27 KiB
Python
647 lines
27 KiB
Python
import actions
|
|
import logger
|
|
|
|
import testsamples
|
|
import testutil
|
|
import test_engine
|
|
|
|
log = logger.Logger(__name__, logger.INFO)
|
|
|
|
def _bulk_update(table_name, col_names, row_data):
|
|
return actions.BulkUpdateRecord(
|
|
*testutil.table_data_from_rows(table_name, col_names, row_data))
|
|
|
|
class TestLookups(test_engine.EngineTestCase):
|
|
|
|
def test_verify_sample(self):
|
|
self.load_sample(testsamples.sample_students)
|
|
self.assertPartialData("Students", ["id", "schoolIds", "schoolCities" ], [
|
|
[1, "1:2", "New York:Colombia" ],
|
|
[2, "3:4", "New Haven:West Haven" ],
|
|
[3, "1:2", "New York:Colombia" ],
|
|
[4, "3:4", "New Haven:West Haven" ],
|
|
[5, "", ""],
|
|
[6, "3:4", "New Haven:West Haven" ]
|
|
])
|
|
|
|
|
|
#----------------------------------------
|
|
def test_lookup_dependencies(self, pre_loaded=False):
|
|
"""
|
|
Test changes to records accessed via lookup.
|
|
"""
|
|
if not pre_loaded:
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
out_actions = self.update_record("Address", 14, city="Bedford")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [_bulk_update("Students", ["id", "schoolCities" ], [
|
|
[2, "New Haven:Bedford" ],
|
|
[4, "New Haven:Bedford" ],
|
|
[6, "New Haven:Bedford" ]]
|
|
)],
|
|
"calls": {"Students": {"schoolCities": 3}}
|
|
})
|
|
|
|
out_actions = self.update_record("Schools", 4, address=13)
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [_bulk_update("Students", ["id", "schoolCities" ], [
|
|
[2, "New Haven:New Haven" ],
|
|
[4, "New Haven:New Haven" ],
|
|
[6, "New Haven:New Haven" ]]
|
|
)],
|
|
"calls": {"Students": {"schoolCities": 3}}
|
|
})
|
|
|
|
out_actions = self.update_record("Address", 14, city="Hartford")
|
|
# No schoolCities need to be recalculatd here, since nothing depends on Address 14 any more.
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calls": {}
|
|
})
|
|
|
|
# Confirm the final result.
|
|
self.assertPartialData("Students", ["id", "schoolIds", "schoolCities" ], [
|
|
[1, "1:2", "New York:Colombia" ],
|
|
[2, "3:4", "New Haven:New Haven" ],
|
|
[3, "1:2", "New York:Colombia" ],
|
|
[4, "3:4", "New Haven:New Haven" ],
|
|
[5, "", ""],
|
|
[6, "3:4", "New Haven:New Haven" ]
|
|
])
|
|
|
|
#----------------------------------------
|
|
def test_dependency_reset(self, pre_loaded=False):
|
|
"""
|
|
A somewhat tricky case. We know that Student 2 depends on Schools 3,4 and on Address 13,14.
|
|
If we change Student 2 to depend on nothing, then changing Address 13 should not cause it to
|
|
recompute.
|
|
"""
|
|
if not pre_loaded:
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
out_actions = self.update_record("Address", 13, city="AAA")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calls": {"Students": {"schoolCities": 3}} # Initially 3 students depend on Address 13.
|
|
})
|
|
|
|
out_actions = self.update_record("Students", 2, schoolName="Invalid")
|
|
|
|
out_actions = self.update_record("Address", 13, city="BBB")
|
|
# If the count below is 3, then the engine forgot to reset the dependencies of Students 2.
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calls": {"Students": {"schoolCities": 2}} # Now only 2 Students depend on Address 13.
|
|
})
|
|
|
|
#----------------------------------------
|
|
def test_lookup_key_changes(self, pre_loaded=False):
|
|
"""
|
|
Test changes to lookup values in the target table. Note that student #3 does not depend on
|
|
any records, but depends on the value "Eureka", so gets updated when this value appears.
|
|
"""
|
|
if not pre_loaded:
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
out_actions = self.update_record("Schools", 2, name="Eureka")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [
|
|
actions.BulkUpdateRecord("Students", [1,3,5], {
|
|
'schoolCities': ["New York", "New York", "Colombia"]
|
|
}),
|
|
actions.BulkUpdateRecord("Students", [1,3,5], {
|
|
'schoolIds': ["1", "1","2"]
|
|
}),
|
|
],
|
|
"calls": {"Students": { 'schoolCities': 3, 'schoolIds': 3 },
|
|
"Schools": {'#lookup#name': 1} },
|
|
})
|
|
|
|
# Test changes to lookup values in the table doing the lookup.
|
|
out_actions = self.update_records("Students", ["id", "schoolName"], [
|
|
[3, ""],
|
|
[5, "Yale"]
|
|
])
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [
|
|
actions.BulkUpdateRecord("Students", [3,5], {'schoolCities': ["", "New Haven:West Haven"]}),
|
|
actions.BulkUpdateRecord("Students", [3,5], {'schoolIds': ["", "3:4"]}),
|
|
],
|
|
"calls": { "Students": { 'schoolCities': 2, 'schoolIds': 2 } },
|
|
})
|
|
|
|
# Confirm the final result.
|
|
self.assertPartialData("Students", ["id", "schoolIds", "schoolCities" ], [
|
|
[1, "1", "New York" ],
|
|
[2, "3:4", "New Haven:West Haven" ],
|
|
[3, "", "" ],
|
|
[4, "3:4", "New Haven:West Haven" ],
|
|
[5, "3:4", "New Haven:West Haven" ],
|
|
[6, "3:4", "New Haven:West Haven" ]
|
|
])
|
|
|
|
|
|
#----------------------------------------
|
|
def test_lookup_formula_after_schema_change(self):
|
|
self.load_sample(testsamples.sample_students)
|
|
self.add_column("Schools", "state", type="Text")
|
|
|
|
# Make a change that causes recomputation of a lookup formula after a schema change.
|
|
# We should NOT get attribute errors in the values.
|
|
out_actions = self.update_record("Schools", 4, address=13)
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [_bulk_update("Students", ["id", "schoolCities" ], [
|
|
[2, "New Haven:New Haven" ],
|
|
[4, "New Haven:New Haven" ],
|
|
[6, "New Haven:New Haven" ]]
|
|
)],
|
|
"calls": { "Students": { 'schoolCities': 3 } }
|
|
})
|
|
|
|
|
|
#----------------------------------------
|
|
def test_lookup_formula_changes(self):
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
self.add_column("Schools", "state", type="Text")
|
|
self.update_records("Schools", ["id", "state"], [
|
|
[1, "NY"],
|
|
[2, "MO"],
|
|
[3, "CT"],
|
|
[4, "CT"]
|
|
])
|
|
|
|
# Verify that when we change a formula, we get appropriate changes.
|
|
out_actions = self.modify_column("Students", "schoolCities", formula=(
|
|
"','.join(Schools.lookupRecords(name=$schoolName).state)"))
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [_bulk_update("Students", ["id", "schoolCities" ], [
|
|
[1, "NY,MO" ],
|
|
[2, "CT,CT" ],
|
|
[3, "NY,MO" ],
|
|
[4, "CT,CT" ],
|
|
[6, "CT,CT" ]]
|
|
)],
|
|
# Note that it got computed 6 times (once for each record), but one value remained unchanged
|
|
# (because no schools matched).
|
|
"calls": { "Students": { 'schoolCities': 6 } }
|
|
})
|
|
|
|
# Check that we've created new dependencies, and removed old ones.
|
|
out_actions = self.update_record("Schools", 4, address=13)
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calls": {}
|
|
})
|
|
|
|
out_actions = self.update_record("Schools", 4, state="MA")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [_bulk_update("Students", ["id", "schoolCities" ], [
|
|
[2, "CT,MA" ],
|
|
[4, "CT,MA" ],
|
|
[6, "CT,MA" ]]
|
|
)],
|
|
"calls": { "Students": { 'schoolCities': 3 } }
|
|
})
|
|
|
|
# If we change to look up uppercase values, we shouldn't find anything.
|
|
out_actions = self.modify_column("Students", "schoolCities", formula=(
|
|
"','.join(Schools.lookupRecords(name=$schoolName.upper()).state)"))
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [actions.BulkUpdateRecord("Students", [1,2,3,4,6],
|
|
{'schoolCities': ["","","","",""]})],
|
|
"calls": { "Students": { 'schoolCities': 6 } }
|
|
})
|
|
|
|
# Changes to dependencies should cause appropriate recalculations.
|
|
out_actions = self.update_record("Schools", 4, state="KY", name="EUREKA")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [
|
|
actions.UpdateRecord("Students", 5, {'schoolCities': "KY"}),
|
|
actions.BulkUpdateRecord("Students", [2,4,6], {'schoolIds': ["3","3","3"]}),
|
|
],
|
|
"calls": {"Students": { 'schoolCities': 1, 'schoolIds': 3 },
|
|
'Schools': {'#lookup#name': 1 } }
|
|
})
|
|
|
|
self.assertPartialData("Students", ["id", "schoolIds", "schoolCities" ], [
|
|
# schoolCities aren't found here because we changed formula to lookup uppercase names.
|
|
[1, "1:2", "" ],
|
|
[2, "3", "" ],
|
|
[3, "1:2", "" ],
|
|
[4, "3", "" ],
|
|
[5, "", "KY" ],
|
|
[6, "3", "" ]
|
|
])
|
|
|
|
def test_add_remove_lookup(self):
|
|
# Verify that when we add or remove a lookup formula, we get appropriate changes.
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
# Add another lookup formula.
|
|
out_actions = self.add_column("Schools", "lastNames", formula=(
|
|
"','.join(Students.lookupRecords(schoolName=$name).lastName)"))
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [_bulk_update("Schools", ["id", "lastNames"], [
|
|
[1, "Obama,Clinton"],
|
|
[2, "Obama,Clinton"],
|
|
[3, "Bush,Bush,Ford"],
|
|
[4, "Bush,Bush,Ford"]]
|
|
)],
|
|
"calls": {"Schools": {"lastNames": 4}, "Students": {"#lookup#schoolName": 6}},
|
|
})
|
|
|
|
# Make sure it responds to changes.
|
|
out_actions = self.update_record("Students", 5, schoolName="Columbia")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [
|
|
_bulk_update("Schools", ["id", "lastNames"], [
|
|
[1, "Obama,Clinton,Reagan"],
|
|
[2, "Obama,Clinton,Reagan"]]
|
|
),
|
|
actions.UpdateRecord("Students", 5, {"schoolCities": "New York:Colombia"}),
|
|
actions.UpdateRecord("Students", 5, {"schoolIds": "1:2"}),
|
|
],
|
|
"calls": {"Students": {'schoolCities': 1, 'schoolIds': 1, '#lookup#schoolName': 1},
|
|
"Schools": { 'lastNames': 2 }},
|
|
})
|
|
|
|
# Modify the column: in the process, the LookupMapColumn on Students.schoolName becomes unused
|
|
# while the old formula column is removed, but used again when it's added. It should not have
|
|
# to be rebuilt (so there should be no calls to recalculate the LookupMapColumn.
|
|
out_actions = self.modify_column("Schools", "lastNames", formula=(
|
|
"','.join(Students.lookupRecords(schoolName=$name).firstName)"))
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [_bulk_update("Schools", ["id", "lastNames"], [
|
|
[1, "Barack,Bill,Ronald"],
|
|
[2, "Barack,Bill,Ronald"],
|
|
[3, "George W,George H,Gerald"],
|
|
[4, "George W,George H,Gerald"]]
|
|
)],
|
|
"calls": {"Schools": {"lastNames": 4}}
|
|
})
|
|
|
|
# Remove the new lookup formula.
|
|
out_actions = self.remove_column("Schools", "lastNames")
|
|
self.assertPartialOutActions(out_actions, {}) # No calc actions
|
|
|
|
# Make sure that changes still work without errors.
|
|
out_actions = self.update_record("Students", 5, schoolName="Eureka")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [
|
|
actions.UpdateRecord("Students", 5, {"schoolCities": ""}),
|
|
actions.UpdateRecord("Students", 5, {"schoolIds": ""}),
|
|
],
|
|
# This should NOT have '#lookup#schoolName' recalculation because there are no longer any
|
|
# formulas which do such a lookup.
|
|
"calls": { "Students": {'schoolCities': 1, 'schoolIds': 1}}
|
|
})
|
|
|
|
|
|
def test_multi_column_lookups(self):
|
|
"""
|
|
Check that we can do lookups by multiple columns.
|
|
"""
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
# Add a lookup formula which looks up a student matching on both first and last names.
|
|
self.add_column("Schools", "bestStudent", type="Text")
|
|
self.update_record("Schools", 1, bestStudent="Bush,George W")
|
|
self.add_column("Schools", "bestStudentId", formula=("""
|
|
if not $bestStudent: return ""
|
|
ln, fn = $bestStudent.split(",")
|
|
return ",".join(str(r.id) for r in Students.lookupRecords(firstName=fn, lastName=ln))
|
|
"""))
|
|
|
|
# Check data so far: only one record is filled.
|
|
self.assertPartialData("Schools", ["id", "bestStudent", "bestStudentId" ], [
|
|
[1, "Bush,George W", "2" ],
|
|
[2, "", "" ],
|
|
[3, "", "" ],
|
|
[4, "", "" ],
|
|
])
|
|
|
|
# Fill a few more records and check that we find records we should, and don't find those we
|
|
# shouldn't.
|
|
out_actions = self.update_records("Schools", ["id", "bestStudent"], [
|
|
[2, "Clinton,Bill"],
|
|
[3, "Norris,Chuck"],
|
|
[4, "Bush,George H"],
|
|
])
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [actions.BulkUpdateRecord("Schools", [2, 4], {"bestStudentId": ["3", "4"]})],
|
|
"calls": {"Schools": {"bestStudentId": 3}}
|
|
})
|
|
self.assertPartialData("Schools", ["id", "bestStudent", "bestStudentId" ], [
|
|
[1, "Bush,George W", "2" ],
|
|
[2, "Clinton,Bill", "3" ],
|
|
[3, "Norris,Chuck", "" ],
|
|
[4, "Bush,George H", "4" ],
|
|
])
|
|
|
|
# Now add more records, first matching only some of the lookup fields.
|
|
out_actions = self.add_record("Students", firstName="Chuck", lastName="Morris")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calls": {
|
|
# No calculations of anything Schools because nothing depends on the incomplete value.
|
|
"Students": {"#lookup#firstName:lastName": 2, "schoolIds": 1, "schoolCities": 1}
|
|
},
|
|
"retValues": [7],
|
|
})
|
|
|
|
# If we add a matching record, then we get a calculation of a record in Schools
|
|
out_actions = self.add_record("Students", firstName="Chuck", lastName="Norris")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calls": {
|
|
"Students": {"#lookup#firstName:lastName": 2, "schoolIds": 1, "schoolCities": 1},
|
|
"Schools": {"bestStudentId": 1}
|
|
},
|
|
"retValues": [8],
|
|
})
|
|
|
|
# And the data should be correct.
|
|
self.assertPartialData("Schools", ["id", "bestStudent", "bestStudentId" ], [
|
|
[1, "Bush,George W", "2" ],
|
|
[2, "Clinton,Bill", "3" ],
|
|
[3, "Norris,Chuck", "8" ],
|
|
[4, "Bush,George H", "4" ],
|
|
])
|
|
|
|
def test_record_removal(self):
|
|
# Remove a record, make sure that lookup maps get updated.
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
out_actions = self.remove_record("Schools", 3)
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [
|
|
actions.BulkUpdateRecord("Students", [2,4,6], {
|
|
"schoolCities": ["West Haven","West Haven","West Haven"]}),
|
|
actions.BulkUpdateRecord("Students", [2,4,6], {
|
|
"schoolIds": ["4","4","4"]}),
|
|
],
|
|
"calls": {
|
|
"Students": {"schoolIds": 3, "schoolCities": 3},
|
|
# LookupMapColumn is also updated but via a different path (unset() vs method() call), so
|
|
# it's not included in the count of formula calls.
|
|
}
|
|
})
|
|
|
|
self.assertPartialData("Students", ["id", "schoolIds", "schoolCities" ], [
|
|
[1, "1:2", "New York:Colombia" ],
|
|
[2, "4", "West Haven" ],
|
|
[3, "1:2", "New York:Colombia" ],
|
|
[4, "4", "West Haven" ],
|
|
[5, "", ""],
|
|
[6, "4", "West Haven" ]
|
|
])
|
|
|
|
def test_empty_relation(self):
|
|
# Make sure that when a relation becomes empty, it doesn't get messed up.
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
# Clear out dependencies.
|
|
self.update_records("Students", ["id", "schoolName"],
|
|
[ [i, ""] for i in [1,2,3,4,5,6] ])
|
|
self.assertPartialData("Students", ["id", "schoolIds", "schoolCities" ],
|
|
[ [i, "", ""] for i in [1,2,3,4,5,6] ])
|
|
|
|
# Make a number of changeas, to ensure they reuse rather than re-create _LookupRelations.
|
|
self.update_record("Students", 2, schoolName="Yale")
|
|
self.update_record("Students", 2, schoolName="Columbia")
|
|
self.update_record("Students", 3, schoolName="Columbia")
|
|
self.assertPartialData("Students", ["id", "schoolIds", "schoolCities" ], [
|
|
[1, "", ""],
|
|
[2, "1:2", "New York:Colombia" ],
|
|
[3, "1:2", "New York:Colombia" ],
|
|
[4, "", ""],
|
|
[5, "", ""],
|
|
[6, "", ""],
|
|
])
|
|
|
|
# When we messed up the dependencies, this change didn't cause a corresponding update. Check
|
|
# that it now does.
|
|
self.remove_record("Schools", 2)
|
|
self.assertPartialData("Students", ["id", "schoolIds", "schoolCities" ], [
|
|
[1, "", ""],
|
|
[2, "1", "New York" ],
|
|
[3, "1", "New York" ],
|
|
[4, "", ""],
|
|
[5, "", ""],
|
|
[6, "", ""],
|
|
])
|
|
|
|
def test_lookups_of_computed_values(self):
|
|
"""
|
|
Make sure that lookups get updated when the value getting looked up is a formula result.
|
|
"""
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
# Add a column like Schools.name, but computed, and change schoolIds to use that one instead.
|
|
self.add_column("Schools", "cname", formula="$name")
|
|
self.modify_column("Students", "schoolIds", formula=
|
|
"':'.join(str(id) for id in Schools.lookupRecords(cname=$schoolName).id)")
|
|
|
|
self.assertPartialData("Students", ["id", "schoolIds" ], [
|
|
[1, "1:2" ],
|
|
[2, "3:4" ],
|
|
[3, "1:2" ],
|
|
[4, "3:4" ],
|
|
[5, "" ],
|
|
[6, "3:4" ],
|
|
])
|
|
|
|
# Check that a change to School.name, which triggers a change to School.cname, causes a change
|
|
# to the looked-up ids. The changes here should be the same as in test_lookup_key_changes
|
|
# test, even though schoolIds depends on name indirectly.
|
|
out_actions = self.update_record("Schools", 2, name="Eureka")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [
|
|
actions.UpdateRecord("Schools", 2, {"cname": "Eureka"}),
|
|
actions.BulkUpdateRecord("Students", [1,3,5], {
|
|
'schoolCities': ["New York", "New York", "Colombia"]
|
|
}),
|
|
actions.BulkUpdateRecord("Students", [1,3,5], {
|
|
'schoolIds': ["1", "1","2"]
|
|
}),
|
|
],
|
|
"calls": {"Students": { 'schoolCities': 3, 'schoolIds': 3 },
|
|
"Schools": {'#lookup#name': 1, '#lookup#cname': 1, "cname": 1} },
|
|
})
|
|
|
|
def use_saved_lookup_results(self):
|
|
"""
|
|
This sets up data so that lookupRecord results are stored in a column and used in another. Key
|
|
tests that check lookup dependencies should work unchanged with this setup.
|
|
"""
|
|
self.load_sample(testsamples.sample_students)
|
|
|
|
# Split up Students.schoolCities into Students.schools and Students.schoolCities.
|
|
self.add_column("Students", "schools", formula="Schools.lookupRecords(name=$schoolName)",
|
|
type="RefList:Schools")
|
|
self.modify_column("Students", "schoolCities",
|
|
formula="':'.join(r.address.city for r in $schools)")
|
|
|
|
# The following tests check correctness of dependencies when lookupResults are stored in one
|
|
# column and used in another. They reuse existing test cases with modified data.
|
|
def test_lookup_dependencies_reflist(self):
|
|
self.use_saved_lookup_results()
|
|
self.test_lookup_dependencies(pre_loaded=True)
|
|
|
|
# Confirm the final result including the additional 'schools' column.
|
|
self.assertPartialData("Students", ["id", "schools", "schoolIds", "schoolCities" ], [
|
|
[1, [1,2], "1:2", "New York:Colombia" ],
|
|
[2, [3,4], "3:4", "New Haven:New Haven" ],
|
|
[3, [1,2], "1:2", "New York:Colombia" ],
|
|
[4, [3,4], "3:4", "New Haven:New Haven" ],
|
|
[5, [], "", ""],
|
|
[6, [3,4], "3:4", "New Haven:New Haven" ]
|
|
])
|
|
|
|
def test_dependency_reset_reflist(self):
|
|
self.use_saved_lookup_results()
|
|
self.test_dependency_reset(pre_loaded=True)
|
|
|
|
def test_lookup_key_changes_reflist(self):
|
|
# We can't run this test case unchanged since our new column changes too in this test.
|
|
self.use_saved_lookup_results()
|
|
out_actions = self.update_record("Schools", 2, name="Eureka")
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [
|
|
actions.BulkUpdateRecord('Students', [1,3,5], {'schools': [[1],[1],[2]]}),
|
|
actions.BulkUpdateRecord("Students", [1,3,5], {
|
|
'schoolCities': ["New York", "New York", "Colombia"]
|
|
}),
|
|
actions.BulkUpdateRecord("Students", [1,3,5], {
|
|
'schoolIds': ["1", "1","2"]
|
|
}),
|
|
],
|
|
"calls": {"Students": { 'schools': 3, 'schoolCities': 3, 'schoolIds': 3 },
|
|
"Schools": {'#lookup#name': 1} },
|
|
})
|
|
|
|
# Test changes to lookup values in the table doing the lookup.
|
|
out_actions = self.update_records("Students", ["id", "schoolName"], [
|
|
[3, ""],
|
|
[5, "Yale"]
|
|
])
|
|
self.assertPartialOutActions(out_actions, {
|
|
"calc": [
|
|
actions.BulkUpdateRecord("Students", [3,5], {'schools': [[], [3,4]]}),
|
|
actions.BulkUpdateRecord("Students", [3,5], {'schoolCities': ["", "New Haven:West Haven"]}),
|
|
actions.BulkUpdateRecord("Students", [3,5], {'schoolIds': ["", "3:4"]}),
|
|
],
|
|
"calls": { "Students": { 'schools': 2, 'schoolCities': 2, 'schoolIds': 2 } },
|
|
})
|
|
|
|
# Confirm the final result.
|
|
self.assertPartialData("Students", ["id", "schools", "schoolIds", "schoolCities" ], [
|
|
[1, [1], "1", "New York" ],
|
|
[2, [3,4], "3:4", "New Haven:West Haven" ],
|
|
[3, [], "", "" ],
|
|
[4, [3,4], "3:4", "New Haven:West Haven" ],
|
|
[5, [3,4], "3:4", "New Haven:West Haven" ],
|
|
[6, [3,4], "3:4", "New Haven:West Haven" ]
|
|
])
|
|
|
|
def test_dependencies_relations_bug(self):
|
|
# We had a serious bug with dependencies, for which this test verifies a fix. Imagine Table2
|
|
# has a formula a=Table1.lookupOne(A=$A), and b=$a.foo. When col A changes in Table1, columns
|
|
# a and b in Table2 get recomputed. Each recompute triggers reset_rows() which is there to
|
|
# clear lookup relations (it actually triggers reset_dependencies() which resets rows for the
|
|
# relation on each dependency edge).
|
|
#
|
|
# The first recompute (of a) triggers reset_rows() on the LookupRelation, then recomputes the
|
|
# lookup formula which re-populates the relation correctly. The second recompute (of b) also
|
|
# triggers reset_rows(). The bug was that it was triggering it in the same LookupRelation, but
|
|
# since it doesn't get followed with recomputing the lookup formula, the relation remains
|
|
# incomplete.
|
|
#
|
|
# It's important that a formula like "b=$a.foo" doesn't reuse the LookupRelation by itself on
|
|
# the edge between b and $a, but a composition of IdentityRelation and LookupRelation. The
|
|
# composition will correctly forward reset_rows() to only the first half of the relation.
|
|
|
|
# Set up two tables with a situation as described above. Here, the role of column Table2.a
|
|
# above is taken by "Students.schools=Schools.lookupRecords(name=$schoolName)".
|
|
self.use_saved_lookup_results()
|
|
|
|
# We intentionally try behavior with type Any formulas too, without converting to a reference
|
|
# type, in case that affects relations.
|
|
self.modify_column("Students", "schools", type="Any")
|
|
self.add_column("Students", "schoolsCount", formula="len($schools.name)")
|
|
self.add_column("Students", "oneSchool", formula="Schools.lookupOne(name=$schoolName)")
|
|
self.add_column("Students", "oneSchoolName", formula="$oneSchool.name")
|
|
|
|
# A helper for comparing Record objects below.
|
|
schools_table = self.engine.tables['Schools']
|
|
def SchoolsRec(row_id):
|
|
return schools_table.Record(schools_table, row_id, None)
|
|
|
|
# We'll play with schools "Columbia" and "Eureka", which are rows 1,3,5 in the Students table.
|
|
self.assertTableData("Students", cols="subset", rows="subset", data=[
|
|
["id", "schoolName", "schoolsCount", "oneSchool", "oneSchoolName"],
|
|
[1, "Columbia", 2, SchoolsRec(1), "Columbia"],
|
|
[3, "Columbia", 2, SchoolsRec(1), "Columbia"],
|
|
[5, "Eureka", 0, SchoolsRec(0), ""],
|
|
])
|
|
|
|
# Now change Schools.schoolName which should trigger recomputations.
|
|
self.update_record("Schools", 1, name="Eureka")
|
|
self.assertTableData("Students", cols="subset", rows="subset", data=[
|
|
["id", "schoolName", "schoolsCount", "oneSchool", "oneSchoolName"],
|
|
[1, "Columbia", 1, SchoolsRec(2), "Columbia"],
|
|
[3, "Columbia", 1, SchoolsRec(2), "Columbia"],
|
|
[5, "Eureka", 1, SchoolsRec(1), "Eureka"],
|
|
])
|
|
|
|
# The first change is expected to work. The important check is that the relations don't get
|
|
# corrupted afterwards. So we do a second change to see if that still updates.
|
|
self.update_record("Schools", 1, name="Columbia")
|
|
self.assertTableData("Students", cols="subset", rows="subset", data=[
|
|
["id", "schoolName", "schoolsCount", "oneSchool", "oneSchoolName"],
|
|
[1, "Columbia", 2, SchoolsRec(1), "Columbia"],
|
|
[3, "Columbia", 2, SchoolsRec(1), "Columbia"],
|
|
[5, "Eureka", 0, SchoolsRec(0), ""],
|
|
])
|
|
|
|
# One more time, for good measure.
|
|
self.update_record("Schools", 1, name="Eureka")
|
|
self.assertTableData("Students", cols="subset", rows="subset", data=[
|
|
["id", "schoolName", "schoolsCount", "oneSchool", "oneSchoolName"],
|
|
[1, "Columbia", 1, SchoolsRec(2), "Columbia"],
|
|
[3, "Columbia", 1, SchoolsRec(2), "Columbia"],
|
|
[5, "Eureka", 1, SchoolsRec(1), "Eureka"],
|
|
])
|
|
|
|
def test_vlookup(self):
|
|
self.load_sample(testsamples.sample_students)
|
|
self.add_column("Students", "school", formula="VLOOKUP(Schools, name=$schoolName)")
|
|
self.add_column("Students", "schoolCity",
|
|
formula="VLOOKUP(Schools, name=$schoolName).address.city")
|
|
|
|
# A helper for comparing Record objects below.
|
|
schools_table = self.engine.tables['Schools']
|
|
def SchoolsRec(row_id):
|
|
return schools_table.Record(schools_table, row_id, None)
|
|
|
|
# We'll play with schools "Columbia" and "Eureka", which are rows 1,3,5 in the Students table.
|
|
self.assertTableData("Students", cols="subset", rows="all", data=[
|
|
["id", "schoolName", "school", "schoolCity"],
|
|
[1, "Columbia", SchoolsRec(1), "New York" ],
|
|
[2, "Yale", SchoolsRec(3), "New Haven" ],
|
|
[3, "Columbia", SchoolsRec(1), "New York" ],
|
|
[4, "Yale", SchoolsRec(3), "New Haven" ],
|
|
[5, "Eureka", SchoolsRec(0), "" ],
|
|
[6, "Yale", SchoolsRec(3), "New Haven" ],
|
|
])
|
|
|
|
# Now change some values which should trigger recomputations.
|
|
self.update_record("Schools", 1, name="Eureka")
|
|
self.update_record("Students", 2, schoolName="Unknown")
|
|
|
|
self.assertTableData("Students", cols="subset", rows="all", data=[
|
|
["id", "schoolName", "school", "schoolCity"],
|
|
[1, "Columbia", SchoolsRec(2), "Colombia" ],
|
|
[2, "Unknown", SchoolsRec(0), "" ],
|
|
[3, "Columbia", SchoolsRec(2), "Colombia" ],
|
|
[4, "Yale", SchoolsRec(3), "New Haven" ],
|
|
[5, "Eureka", SchoolsRec(1), "New York" ],
|
|
[6, "Yale", SchoolsRec(3), "New Haven" ],
|
|
])
|