(core) Nice summary table IDs

Summary:
Changes auto-generated summary table IDs from e.g. `GristSummary_6_Table1` to `Table1_summary_A_B` (meaning `Table1` grouped by `A` and `B`). This makes it easier to write formulas involving summary tables, make API requests, understand logs, etc.

Because these don't encode the source table ID as reliably as before, `decode_summary_table_name` now uses the summary table schema info, not just the summary table ID. Specifically, it looks at the type of the `group` column, which is `RefList:<source table id>`.

Renaming a source table renames the summary table as before, and now renaming a groupby column renames the summary table as well.

Conflicting table names are resolved in the usual way by adding a number at the end, e.g. `Table1_summary_A_B2`. These summary tables are not automatically renamed when the disambiguation is no longer needed.

A new migration renames all summary tables to the new scheme, and updates formulas using summary tables with a simple regex.

Test Plan:
Updated many tests to use the new style of name.

Added new Python tests to for resolving conflicts when renaming source tables and groupby columns.

Added a test for the migration, including renames in formulas.

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3508
This commit is contained in:
Alex Hall
2022-07-11 20:00:25 +02:00
parent f1df6c0a46
commit b8486dcdba
18 changed files with 507 additions and 311 deletions

View File

@@ -1,6 +1,5 @@
from collections import namedtuple
import json
import re
import six
@@ -79,38 +78,34 @@ def _copy_widget_options(options):
return options
return json.dumps({k: v for k, v in options.items() if k != "rulesOptions"})
# To generate code, we need to know for each summary table, what its source table is. It would be
# easy if we had access to metadata records, but (at least for now) we generate all code based on
# schema only. So we encode the source table name inside of the summary table name.
#
# The encoding includes the length of the source table name, to avoid the possibility of ambiguity
# between the second summary table for "Foo", and the first summary table for "Foo2".
#
# Note that it means we need to rename summary tables when the source table is renamed.
def encode_summary_table_name(source_table_name):
def encode_summary_table_name(source_table_id, groupby_col_ids):
"""
Create a summary table name that reliably encodes the source table name. It can be decoded even
if a suffix is added to the returned name.
Create a summary table name based on the source table ID and the groupby column IDs.
"""
return "GristSummary_%d_%s" % (len(source_table_name), source_table_name)
result = source_table_id + '_summary'
if groupby_col_ids:
result += '_' + '_'.join(sorted(groupby_col_ids))
return result
_summary_re = re.compile(r'GristSummary_(\d+)_')
def decode_summary_table_name(summary_table_name):
def decode_summary_table_name(summary_table_info):
"""
Extract the name of the source table from the summary table name.
Extract the name of the source table from the summary table schema info.
"""
m = _summary_re.match(summary_table_name)
if m:
start = m.end(0)
length = int(m.group(1))
source_name = summary_table_name[start : start + length]
if len(source_name) == length:
return source_name
# To generate code, we need to know for each summary table, what its source table is. It would be
# easy if we had access to metadata records, but (at least for now) we generate all code based on
# schema only. So we use the type of special 'group' column in the summary table.
group_col = summary_table_info.columns.get('group')
if (
group_col
and 'getSummarySourceGroup' in group_col.formula
and group_col.type.startswith('RefList:')
):
return group_col.type[8:]
return None
def _group_colinfo(source_table):
"""Returns ColInfo() for the 'group' column that must be present in every summary table."""
return _make_col_info(colId='group', type='RefList:%s' % source_table.tableId,
@@ -202,8 +197,9 @@ class SummaryActions(object):
summary_table = next((t for t in source_table.summaryTables if t.summaryKey == key), None)
created = False
if not summary_table:
groupby_col_ids = [c.colId for c in groupby_colinfo]
result = self.useractions.doAddTable(
encode_summary_table_name(source_table.tableId),
encode_summary_table_name(source_table.tableId, groupby_col_ids),
[_get_colinfo_dict(ci, with_id=True) for ci in groupby_colinfo + formula_colinfo],
summarySourceTableRef=source_table.id,
raw_section=True)