mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
e2226c3ab7
Summary: - Introduce a new SQLiteDB migration, which adds DB columns for formula columns - Newly added columns have the special ['P'] (pending) value in them (in order to show the usual "Loading..." on the first load that triggers the migration) - Calculated values are added to .stored/.undo fields of user actions. - Various changes made in the sandbox to include .stored/.undo in the right order. - OnDemand tables ignore stored formula columns, replacing them with special SQL as before - In particular, converting to OnDemand table leaves stale values in those columns, we should maybe clean those out. Some tweaks on the side: - Allow overriding chai assertion truncateThreshold with CHAI_TRUNCATE_THRESHOLD - Rebuild python automatically in watch mode Test Plan: Fixed various tests, updated some fixtures. Many python tests that check actions needed adjustments because actions moved from .stored to .undo. Some checks added to catch situations previously only caught in browser tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2645
257 lines
11 KiB
Python
257 lines
11 KiB
Python
import actions
|
|
import schema
|
|
import logger
|
|
from objtypes import strict_equal
|
|
|
|
log = logger.Logger(__name__, logger.INFO)
|
|
|
|
class DocActions(object):
|
|
def __init__(self, engine):
|
|
self._engine = engine
|
|
|
|
#----------------------------------------
|
|
# Actions on records.
|
|
#----------------------------------------
|
|
|
|
def AddRecord(self, table_id, row_id, column_values):
|
|
self.BulkAddRecord(
|
|
table_id, [row_id], {key: [val] for key, val in column_values.iteritems()})
|
|
|
|
def BulkAddRecord(self, table_id, row_ids, column_values):
|
|
table = self._engine.tables[table_id]
|
|
for row_id in row_ids:
|
|
assert row_id not in table.row_ids, \
|
|
"docactions.[Bulk]AddRecord for existing record #%s" % row_id
|
|
|
|
self._engine.out_actions.undo.append(actions.BulkRemoveRecord(table_id, row_ids).simplify())
|
|
self._engine.out_actions.summary.add_records(table_id, row_ids)
|
|
|
|
self._engine.add_records(table_id, row_ids, column_values)
|
|
|
|
def RemoveRecord(self, table_id, row_id):
|
|
return self.BulkRemoveRecord(table_id, [row_id])
|
|
|
|
def BulkRemoveRecord(self, table_id, row_ids):
|
|
table = self._engine.tables[table_id]
|
|
|
|
# Collect the undo values, and unset all values in the column (i.e. set to defaults), just to
|
|
# make sure we don't have stale values hanging around.
|
|
undo_values = {}
|
|
for column in table.all_columns.itervalues():
|
|
if not column.is_private() and column.col_id != "id":
|
|
col_values = map(column.raw_get, row_ids)
|
|
default = column.getdefault()
|
|
# If this column had all default values, don't include it into the undo BulkAddRecord.
|
|
if not all(strict_equal(val, default) for val in col_values):
|
|
undo_values[column.col_id] = col_values
|
|
for row_id in row_ids:
|
|
column.unset(row_id)
|
|
|
|
# Generate the undo action.
|
|
self._engine.out_actions.undo.append(
|
|
actions.BulkAddRecord(table_id, row_ids, undo_values).simplify())
|
|
self._engine.out_actions.summary.remove_records(table_id, row_ids)
|
|
|
|
# Invalidate the deleted rows, so that anything that depends on them gets recomputed.
|
|
self._engine.invalidate_records(table_id, row_ids)
|
|
|
|
def UpdateRecord(self, table_id, row_id, columns):
|
|
self.BulkUpdateRecord(
|
|
table_id, [row_id], {key: [val] for key, val in columns.iteritems()})
|
|
|
|
def BulkUpdateRecord(self, table_id, row_ids, columns):
|
|
table = self._engine.tables[table_id]
|
|
for row_id in row_ids:
|
|
assert row_id in table.row_ids, \
|
|
"docactions.[Bulk]UpdateRecord for non-existent record #%s" % row_id
|
|
|
|
# Load the updated values.
|
|
undo_values = {}
|
|
for col_id, values in columns.iteritems():
|
|
col = table.get_column(col_id)
|
|
undo_values[col_id] = map(col.raw_get, row_ids)
|
|
for (row_id, value) in zip(row_ids, values):
|
|
col.set(row_id, value)
|
|
|
|
# Generate the undo action.
|
|
self._engine.out_actions.undo.append(
|
|
actions.BulkUpdateRecord(table_id, row_ids, undo_values).simplify())
|
|
|
|
# Invalidate the updated rows, just for the columns that got changed (and, as always,
|
|
# anything that depends on them).
|
|
self._engine.invalidate_records(table_id, row_ids, col_ids=columns.keys())
|
|
|
|
|
|
def ReplaceTableData(self, table_id, row_ids, column_values):
|
|
old_data = self._engine.fetch_table(table_id, formulas=False)
|
|
self._engine.out_actions.undo.append(actions.ReplaceTableData(*old_data))
|
|
self._engine.out_actions.summary.remove_records(table_id, old_data[1])
|
|
self._engine.out_actions.summary.add_records(table_id, row_ids)
|
|
self._engine.load_table(actions.TableData(table_id, row_ids, column_values))
|
|
|
|
#----------------------------------------
|
|
# Actions on columns.
|
|
#----------------------------------------
|
|
|
|
def AddColumn(self, table_id, col_id, col_info):
|
|
table = self._engine.tables[table_id]
|
|
assert not table.has_column(col_id), "Column %s already exists in %s" % (col_id, table_id)
|
|
|
|
# Add the new column to the schema object maintained in the engine.
|
|
self._engine.schema[table_id].columns[col_id] = schema.dict_to_col(col_info, col_id=col_id)
|
|
self._engine.rebuild_usercode()
|
|
self._engine.new_column_name(table)
|
|
|
|
# Generate the undo action.
|
|
self._engine.out_actions.undo.append(actions.RemoveColumn(table_id, col_id))
|
|
self._engine.out_actions.summary.add_column(table_id, col_id)
|
|
|
|
def RemoveColumn(self, table_id, col_id):
|
|
table = self._engine.tables[table_id]
|
|
assert table.has_column(col_id), "Column %s not in table %s" % (col_id, table_id)
|
|
|
|
# Generate (if needed) the undo action to restore the data.
|
|
undo_action = None
|
|
column = table.get_column(col_id)
|
|
if not column.is_private():
|
|
default = column.getdefault()
|
|
# Add to undo a BulkUpdateRecord for non-default values in the column being removed.
|
|
undo_values = [(r, column.raw_get(r)) for r in table.row_ids
|
|
if not strict_equal(column.raw_get(r), default)]
|
|
|
|
# Remove the specified column from the schema object.
|
|
colinfo = self._engine.schema[table_id].columns.pop(col_id)
|
|
self._engine.rebuild_usercode()
|
|
|
|
# Generate the undo action(s); if for a formula column, add them to the calc summary.
|
|
if undo_values:
|
|
if column.is_formula():
|
|
changes = [(r, v, default) for (r, v) in undo_values]
|
|
self._engine.out_actions.summary.add_changes(table_id, col_id, changes)
|
|
else:
|
|
row_ids = [r for (r, v) in undo_values]
|
|
values = [v for (r, v) in undo_values]
|
|
undo_action = actions.BulkUpdateRecord(table_id, row_ids, {col_id: values}).simplify()
|
|
self._engine.out_actions.undo.append(undo_action)
|
|
|
|
self._engine.out_actions.undo.append(actions.AddColumn(
|
|
table_id, col_id, schema.col_to_dict(colinfo, include_id=False)))
|
|
self._engine.out_actions.summary.remove_column(table_id, col_id)
|
|
|
|
def RenameColumn(self, table_id, old_col_id, new_col_id):
|
|
table = self._engine.tables[table_id]
|
|
|
|
assert table.has_column(old_col_id), "Column %s not in table %s" % (old_col_id, table_id)
|
|
assert not table.has_column(new_col_id), \
|
|
"Column %s already exists in %s" % (new_col_id, table_id)
|
|
old_column = table.get_column(old_col_id)
|
|
|
|
# Replace the renamed column in the schema object.
|
|
schema_table_info = self._engine.schema[table_id]
|
|
colinfo = schema_table_info.columns.pop(old_col_id)
|
|
schema_table_info.columns[new_col_id] = schema.SchemaColumn(
|
|
new_col_id, colinfo.type, colinfo.isFormula, colinfo.formula)
|
|
|
|
self._engine.rebuild_usercode()
|
|
self._engine.new_column_name(table)
|
|
|
|
# We replaced the old column with a new Column object (not strictly necessary, but simpler).
|
|
# For a raw data column, we need to copy over the data from the old column object.
|
|
new_column = table.get_column(new_col_id)
|
|
new_column.copy_from_column(old_column)
|
|
|
|
# Generate the undo action.
|
|
self._engine.out_actions.undo.append(actions.RenameColumn(table_id, new_col_id, old_col_id))
|
|
self._engine.out_actions.summary.rename_column(table_id, old_col_id, new_col_id)
|
|
|
|
def ModifyColumn(self, table_id, col_id, col_info):
|
|
table = self._engine.tables[table_id]
|
|
assert table.has_column(col_id), "Column %s not in table %s" % (col_id, table_id)
|
|
old_column = table.get_column(col_id)
|
|
|
|
# Modify the specified column in the schema object.
|
|
schema_table_info = self._engine.schema[table_id]
|
|
old = schema_table_info.columns[col_id]
|
|
new = schema.SchemaColumn(col_id,
|
|
col_info.get('type', old.type),
|
|
bool(col_info.get('isFormula', old.isFormula)),
|
|
col_info.get('formula', old.formula))
|
|
if new == old:
|
|
log.info("ModifyColumn called which was a noop")
|
|
return
|
|
|
|
undo_col_info = {k: v for k, v in schema.col_to_dict(old, include_id=False).iteritems()
|
|
if k in col_info}
|
|
|
|
# Remove the column from the schema, then re-add it, to force creation of a new column object.
|
|
schema_table_info.columns.pop(col_id)
|
|
self._engine.rebuild_usercode()
|
|
|
|
schema_table_info.columns[col_id] = new
|
|
self._engine.rebuild_usercode()
|
|
|
|
# Fill in the new column with the values from the old column.
|
|
new_column = table.get_column(col_id)
|
|
for row_id in table.row_ids:
|
|
new_column.set(row_id, old_column.raw_get(row_id))
|
|
|
|
# Generate the undo action.
|
|
self._engine.out_actions.undo.append(actions.ModifyColumn(table_id, col_id, undo_col_info))
|
|
|
|
#----------------------------------------
|
|
# Actions on tables.
|
|
#----------------------------------------
|
|
|
|
def AddTable(self, table_id, columns):
|
|
assert table_id not in self._engine.tables, "Table %s already exists" % table_id
|
|
|
|
# Update schema, and re-generate the module code.
|
|
self._engine.schema[table_id] = schema.SchemaTable(table_id, schema.dict_list_to_cols(columns))
|
|
self._engine.rebuild_usercode()
|
|
|
|
# Generate the undo action.
|
|
self._engine.out_actions.undo.append(actions.RemoveTable(table_id))
|
|
self._engine.out_actions.summary.add_table(table_id)
|
|
|
|
def RemoveTable(self, table_id):
|
|
assert table_id in self._engine.tables, "Table %s doesn't exist" % table_id
|
|
|
|
# Create undo actions to restore all the data records of this table.
|
|
table_data = self._engine.fetch_table(table_id, formulas=True)
|
|
undo_action = actions.BulkAddRecord(*table_data).simplify()
|
|
if undo_action:
|
|
self._engine.out_actions.undo.append(undo_action)
|
|
|
|
# Update schema, and re-generate the module code.
|
|
schema_table = self._engine.schema.pop(table_id)
|
|
self._engine.rebuild_usercode()
|
|
|
|
# Generate the undo action.
|
|
self._engine.out_actions.undo.append(actions.AddTable(
|
|
table_id, schema.cols_to_dict_list(schema_table.columns)))
|
|
self._engine.out_actions.summary.remove_table(table_id)
|
|
|
|
def RenameTable(self, old_table_id, new_table_id):
|
|
assert old_table_id in self._engine.tables, "Table %s doesn't exist" % old_table_id
|
|
assert new_table_id not in self._engine.tables, "Table %s already exists" % new_table_id
|
|
|
|
old_table = self._engine.tables[old_table_id]
|
|
|
|
# Update schema, and re-generate the module code.
|
|
old = self._engine.schema.pop(old_table_id)
|
|
self._engine.schema[new_table_id] = schema.SchemaTable(new_table_id, old.columns)
|
|
self._engine.rebuild_usercode()
|
|
|
|
# Copy over all columns from the old table to the new.
|
|
new_table = self._engine.tables[new_table_id]
|
|
for new_column in new_table.all_columns.itervalues():
|
|
if not new_column.is_private():
|
|
new_column.copy_from_column(old_table.get_column(new_column.col_id))
|
|
new_table.grow_to_max() # We need to bring formula columns to the right size too.
|
|
|
|
# Generate the undo action.
|
|
self._engine.out_actions.undo.append(actions.RenameTable(new_table_id, old_table_id))
|
|
self._engine.out_actions.summary.rename_table(old_table_id, new_table_id)
|
|
|
|
# end
|