Esempio n. 1
0
def migration3(tdset):
    """
  There is no longer a "Derived" type for columns, and summary tables use the type suitable for
  the column being summarized. For old documents, convert "Derived" type to "Any", and adjust the
  usage of "lookupOrAddDerived()" function.
  """
    # Note that this is a complicated migration, and mainly acceptable because it is before our very
    # first release. For a released product, a change like this should be done in a backwards
    # compatible way: keep but deprecate 'Derived'; introduce a lookupOrAddDerived2() to use for new
    # summary tables, but keep the old interface as well for existing ones. The reason is that such
    # migrations are error-prone and may mess up customers' data.
    doc_actions = []
    tables = list(
        actions.transpose_bulk_action(tdset.all_tables['_grist_Tables']))
    tables_map = {t.id: t for t in tables}
    columns = list(
        actions.transpose_bulk_action(
            tdset.all_tables['_grist_Tables_column']))

    # Convert columns from type 'Derived' to type 'Any'
    affected_cols = [c for c in columns if c.type == 'Derived']
    if affected_cols:
        doc_actions.extend(
            actions.ModifyColumn(tables_map[c.parentId].tableId, c.colId,
                                 {'type': 'Any'}) for c in affected_cols)
        doc_actions.append(
            actions.BulkUpdateRecord('_grist_Tables_column',
                                     [c.id for c in affected_cols],
                                     {'type': ['Any' for c in affected_cols]}))

    # Convert formulas of the form '.lookupOrAddDerived($x,$y)' to '.lookupOrAddDerived(x=$x,y=$y)'
    formula_re = re.compile(r'(\w+).lookupOrAddDerived\((.*?)\)')
    arg_re = re.compile(r'^\$(\w+)$')

    def replace(match):
        args = ", ".join(
            arg_re.sub(r'\1=$\1', arg.strip())
            for arg in match.group(2).split(","))
        return '%s.lookupOrAddDerived(%s)' % (match.group(1), args)

    formula_updates = []
    for c in columns:
        new_formula = c.formula and formula_re.sub(replace, c.formula)
        if new_formula != c.formula:
            formula_updates.append((c, new_formula))

    if formula_updates:
        doc_actions.extend(
            actions.ModifyColumn(tables_map[c.parentId].tableId, c.colId,
                                 {'formula': f}) for c, f in formula_updates)
        doc_actions.append(
            actions.BulkUpdateRecord(
                '_grist_Tables_column', [c.id for c, f in formula_updates],
                {'formula': [f for c, f in formula_updates]}))
    return tdset.apply_doc_actions(doc_actions)
Esempio n. 2
0
  def test_migrations(self):
    tdset = table_data_set.TableDataSet()
    tdset.apply_doc_actions(schema_version0())
    migration_actions = migrations.create_migrations(tdset.all_tables)
    tdset.apply_doc_actions(migration_actions)

    # Compare schema derived from migrations to the current schema.
    migrated_schema = tdset.get_schema()
    current_schema = {a.table_id: {c['id']: c for c in a.columns}
                      for a in schema.schema_create_actions()}
    # pylint: disable=too-many-nested-blocks
    if migrated_schema != current_schema:
      # Figure out the version of new migration to suggest, and whether to update SCHEMA_VERSION.
      new_version = max(schema.SCHEMA_VERSION, migrations.get_last_migration_version() + 1)

      # Figure out the missing actions.
      doc_actions = []
      for table_id in sorted(six.viewkeys(current_schema) | six.viewkeys(migrated_schema)):
        if table_id not in migrated_schema:
          doc_actions.append(actions.AddTable(table_id, current_schema[table_id].values()))
        elif table_id not in current_schema:
          doc_actions.append(actions.RemoveTable(table_id))
        else:
          current_cols = current_schema[table_id]
          migrated_cols = migrated_schema[table_id]
          for col_id in sorted(six.viewkeys(current_cols) | six.viewkeys(migrated_cols)):
            if col_id not in migrated_cols:
              doc_actions.append(actions.AddColumn(table_id, col_id, current_cols[col_id]))
            elif col_id not in current_cols:
              doc_actions.append(actions.RemoveColumn(table_id, col_id))
            else:
              current_info = current_cols[col_id]
              migrated_info = migrated_cols[col_id]
              delta = {k: v for k, v in six.iteritems(current_info) if v != migrated_info.get(k)}
              if delta:
                doc_actions.append(actions.ModifyColumn(table_id, col_id, delta))

      suggested_migration = (
        "----------------------------------------------------------------------\n" +
        "*** migrations.py ***\n" +
        "----------------------------------------------------------------------\n" +
        "@migration(schema_version=%s)\n" % new_version +
        "def migration%s(tdset):\n" % new_version +
        "  return tdset.apply_doc_actions([\n" +
        "".join(stringify(a) + ",\n" for a in doc_actions) +
        "  ])\n"
      )

      if new_version != schema.SCHEMA_VERSION:
        suggested_schema_update = (
          "----------------------------------------------------------------------\n" +
          "*** schema.py ***\n" +
          "----------------------------------------------------------------------\n" +
          "SCHEMA_VERSION = %s\n" % new_version
        )
      else:
        suggested_schema_update = ""

      self.fail("Migrations are incomplete. Suggested migration to add:\n" +
                suggested_schema_update + suggested_migration)
Esempio n. 3
0
def migration17(tdset):
    """
  There is no longer an "Image" type for columns, as "Attachments" now serves as a
  display type for arbitrary files including images. Convert "Image" columns to "Attachments"
  columns.
  """
    doc_actions = []
    tables = list(
        actions.transpose_bulk_action(tdset.all_tables['_grist_Tables']))
    tables_map = {t.id: t for t in tables}
    columns = list(
        actions.transpose_bulk_action(
            tdset.all_tables['_grist_Tables_column']))

    # Convert columns from type 'Image' to type 'Attachments'
    affected_cols = [c for c in columns if c.type == 'Image']
    conv = lambda val: [val] if isinstance(val, int) and val > 0 else []
    if affected_cols:
        # Update the types in the data tables
        doc_actions.extend(
            actions.ModifyColumn(tables_map[c.parentId].tableId, c.colId,
                                 {'type': 'Attachments'})
            for c in affected_cols)
        # Update the values to lists
        for c in affected_cols:
            if c.isFormula:
                # Formula columns don't have data stored in DB, should not have data changes.
                continue
            table_id = tables_map[c.parentId].tableId
            table = tdset.all_tables[table_id]
            doc_actions.append(
                actions.BulkUpdateRecord(
                    table_id, table.row_ids,
                    {c.colId: [conv(val) for val in table.columns[c.colId]]}))
        # Update the types in the metadata tables
        doc_actions.append(
            actions.BulkUpdateRecord(
                '_grist_Tables_column', [c.id for c in affected_cols],
                {'type': ['Attachments' for c in affected_cols]}))

    return tdset.apply_doc_actions(doc_actions)
Esempio n. 4
0
    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 six.iteritems(schema.col_to_dict(
                old, include_id=False)) 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))
Esempio n. 5
0
 def alist():
     return [
         actions.BulkUpdateRecord("Table1", [1, 2, 3],
                                  {'Foo': [10, 20, 30]}),
         actions.BulkUpdateRecord("Table2", [1, 2, 3], {
             'Foo': [10, 20, 30],
             'Bar': ['a', 'b', 'c']
         }),
         actions.UpdateRecord("Table1", 17, {'Foo': 10}),
         actions.UpdateRecord("Table2", 18, {
             'Foo': 10,
             'Bar': 'a'
         }),
         actions.AddRecord("Table1", 17, {'Foo': 10}),
         actions.BulkAddRecord("Table2", 18, {
             'Foo': 10,
             'Bar': 'a'
         }),
         actions.ReplaceTableData("Table2", 18, {
             'Foo': 10,
             'Bar': 'a'
         }),
         actions.RemoveRecord("Table1", 17),
         actions.BulkRemoveRecord("Table2", [17, 18]),
         actions.AddColumn("Table1", "Foo", {"type": "Text"}),
         actions.RenameColumn("Table1", "Foo", "Bar"),
         actions.ModifyColumn("Table1", "Foo", {"type": "Text"}),
         actions.RemoveColumn("Table1", "Foo"),
         actions.AddTable("THello", [{
             "id": "Foo"
         }, {
             "id": "Bar"
         }]),
         actions.RemoveTable("THello"),
         actions.RenameTable("THello", "TWorld"),
     ]
Esempio n. 6
0
    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, {
                "stored": [
                    actions.AddColumn(
                        "Schools", "lastNames", {
                            "formula":
                            "','.join(Students.lookupRecords(schoolName=$name).lastName)",
                            "isFormula": True,
                            "type": "Any"
                        }),
                    actions.AddRecord(
                        "_grist_Tables_column", 22, {
                            "colId": "lastNames",
                            "formula":
                            "','.join(Students.lookupRecords(schoolName=$name).lastName)",
                            "isFormula": True,
                            "label": "lastNames",
                            "parentId": 2,
                            "parentPos": 6.0,
                            "type": "Any",
                            "widgetOptions": ""
                        }),
                    _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, {
                "stored": [
                    actions.UpdateRecord("Students", 5,
                                         {"schoolName": "Columbia"}),
                    _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, {
                "stored": [
                    actions.ModifyColumn(
                        "Schools", "lastNames", {
                            "formula":
                            "','.join(Students.lookupRecords(schoolName=$name).firstName)"
                        }),
                    actions.UpdateRecord(
                        "_grist_Tables_column", 22, {
                            "formula":
                            "','.join(Students.lookupRecords(schoolName=$name).firstName)"
                        }),
                    _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,
            {
                "stored": [
                    actions.UpdateRecord("Students", 5,
                                         {"schoolName": "Eureka"}),
                    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
                    }
                }
            })
Esempio n. 7
0
    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,
            {
                "stored": [
                    actions.ModifyColumn(
                        "Students", "schoolCities", {
                            "formula":
                            "','.join(Schools.lookupRecords(name=$schoolName).state)",
                        }),
                    actions.UpdateRecord(
                        "_grist_Tables_column", 6, {
                            "formula":
                            "','.join(Schools.lookupRecords(name=$schoolName).state)",
                        }),
                    _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, {
                "stored": [
                    actions.UpdateRecord("Schools", 4, {"state": "MA"}),
                    _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, {
                "stored": [
                    actions.ModifyColumn(
                        "Students", "schoolCities", {
                            "formula":
                            "','.join(Schools.lookupRecords(name=$schoolName.upper()).state)"
                        }),
                    actions.UpdateRecord(
                        "_grist_Tables_column", 6, {
                            "formula":
                            "','.join(Schools.lookupRecords(name=$schoolName.upper()).state)"
                        }),
                    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, {
                "stored": [
                    actions.UpdateRecord("Schools", 4, {
                        "state": "KY",
                        "name": "EUREKA"
                    }),
                    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", ""]
            ])
Esempio n. 8
0
def migration7(tdset):
    """
  Add summarySourceTable/summarySourceCol fields to metadata, and adjust existing summary tables
  to correspond to the new style.
  """
    # Note: this migration has some faults.
    # - It doesn't delete viewSectionFields for columns it removes (if a user added some special
    #   columns manually.
    # - It doesn't fix types of Reference columns that refer to old-style summary tables
    #   (if the user created some such columns manually).

    doc_actions = [
        action for action in [
            maybe_add_column(tdset, '_grist_Tables', 'summarySourceTable',
                             'Ref:_grist_Tables'),
            maybe_add_column(tdset, '_grist_Tables_column', 'summarySourceCol',
                             'Ref:_grist_Tables_column')
        ] if action
    ]

    # Maps tableRef to Table object.
    tables_map = {
        t.id: t
        for t in actions.transpose_bulk_action(
            tdset.all_tables['_grist_Tables'])
    }

    # Maps tableName to tableRef
    table_name_to_ref = {t.tableId: t.id for t in six.itervalues(tables_map)}

    # List of Column objects
    columns = list(
        actions.transpose_bulk_action(
            tdset.all_tables['_grist_Tables_column']))

    # Maps columnRef to Column object.
    columns_map_by_ref = {c.id: c for c in columns}

    # Maps (tableRef, colName) to Column object.
    columns_map_by_table_colid = {(c.parentId, c.colId): c for c in columns}

    # Set of all tableNames.
    table_name_set = set(table_name_to_ref.keys())

    remove_cols = []  # List of columns to remove
    formula_updates = []  # List of (column, new_table_name, new_formula) pairs
    table_renames = []  # List of (table, new_name) pairs
    source_tables = []  # List of (table, summarySourceTable) pairs
    source_cols = []  # List of (column, summarySourceColumn) pairs

    # Summary tables used to be named as "Summary_<SourceName>_<ColRef1>_<ColRef2>". This regular
    # expression parses that.
    summary_re = re.compile(r'^Summary_(\w+?)((?:_\d+)*)$')
    for t in six.itervalues(tables_map):
        m = summary_re.match(t.tableId)
        if not m or m.group(1) not in table_name_to_ref:
            continue
        # We have a valid summary table.
        source_table_name = m.group(1)
        source_table_ref = table_name_to_ref[source_table_name]
        groupby_colrefs = [int(x) for x in m.group(2).strip("_").split("_")]
        # Prepare a new-style name for the summary table. Be sure not to conflict with existing tables
        # or with each other (i.e. don't rename multiple tables to the same name).
        new_name = summary.encode_summary_table_name(source_table_name)
        new_name = identifiers.pick_table_ident(new_name, avoid=table_name_set)
        table_name_set.add(new_name)
        log.warn("Upgrading summary table %s for %s(%s) to %s" %
                 (t.tableId, source_table_name, groupby_colrefs, new_name))

        # Remove the "lookupOrAddDerived" column from the source table (which is named using the
        # summary table name for its colId).
        remove_cols.extend(
            c for c in columns
            if c.parentId == source_table_ref and c.colId == t.tableId)

        # Upgrade the "group" formula in the summary table.
        expected_group_formula = "%s.lookupRecords(%s=$id)" % (
            source_table_name, t.tableId)
        new_formula = "table.getSummarySourceGroup(rec)"
        formula_updates.extend((c, new_name, new_formula) for c in columns
                               if (c.parentId == t.id and c.colId == "group"
                                   and c.formula == expected_group_formula))

        # Schedule a rename of the summary table.
        table_renames.append((t, new_name))

        # Set summarySourceTable fields on the metadata.
        source_tables.append((t, source_table_ref))

        # Set summarySourceCol fields in the metadata. We need to find the right summary column.
        groupby_cols = set()
        for col_ref in groupby_colrefs:
            src_col = columns_map_by_ref.get(col_ref)
            sum_col = columns_map_by_table_colid.get(
                (t.id, src_col.colId)) if src_col else None
            if sum_col:
                groupby_cols.add(sum_col)
                source_cols.append((sum_col, src_col.id))
            else:
                log.warn(
                    "Upgrading summary table %s: couldn't find column %s" %
                    (t.tableId, col_ref))

        # Finally, we have to remove all non-formula columns that are not groupby-columns (e.g.
        # 'manualSort'), because the new approach assumes ALL non-formula columns are for groupby.
        remove_cols.extend(c for c in columns if c.parentId == t.id
                           and c not in groupby_cols and not c.isFormula)

    # Create all the doc actions from the arrays we prepared.

    # Process remove_cols
    doc_actions.extend(
        actions.RemoveColumn(tables_map[c.parentId].tableId, c.colId)
        for c in remove_cols)
    doc_actions.append(
        actions.BulkRemoveRecord('_grist_Tables_column',
                                 [c.id for c in remove_cols]))

    # Process table_renames
    doc_actions.extend(
        actions.RenameTable(t.tableId, new) for (t, new) in table_renames)
    doc_actions.append(
        actions.BulkUpdateRecord(
            '_grist_Tables', [t.id for t, new in table_renames],
            {'tableId': [new for t, new in table_renames]}))

    # Process source_tables and source_cols
    doc_actions.append(
        actions.BulkUpdateRecord(
            '_grist_Tables', [t.id for t, ref in source_tables],
            {'summarySourceTable': [ref for t, ref in source_tables]}))
    doc_actions.append(
        actions.BulkUpdateRecord(
            '_grist_Tables_column', [t.id for t, ref in source_cols],
            {'summarySourceCol': [ref for t, ref in source_cols]}))

    # Process formula_updates. Do this last since recalculation of these may cause new records added
    # to summary tables, so we should have all the tables correctly set up by this time.
    doc_actions.extend(
        actions.ModifyColumn(table_id, c.colId, {'formula': f})
        for c, table_id, f in formula_updates)
    doc_actions.append(
        actions.BulkUpdateRecord(
            '_grist_Tables_column', [c.id for c, t, f in formula_updates],
            {'formula': [f for c, t, f in formula_updates]}))

    return tdset.apply_doc_actions(doc_actions)
Esempio n. 9
0
    def test_prune_actions(self):
        # prune_actions is in-place, so we make a new list every time.
        def alist():
            return [
                actions.BulkUpdateRecord("Table1", [1, 2, 3],
                                         {'Foo': [10, 20, 30]}),
                actions.BulkUpdateRecord("Table2", [1, 2, 3], {
                    'Foo': [10, 20, 30],
                    'Bar': ['a', 'b', 'c']
                }),
                actions.UpdateRecord("Table1", 17, {'Foo': 10}),
                actions.UpdateRecord("Table2", 18, {
                    'Foo': 10,
                    'Bar': 'a'
                }),
                actions.AddRecord("Table1", 17, {'Foo': 10}),
                actions.BulkAddRecord("Table2", 18, {
                    'Foo': 10,
                    'Bar': 'a'
                }),
                actions.ReplaceTableData("Table2", 18, {
                    'Foo': 10,
                    'Bar': 'a'
                }),
                actions.RemoveRecord("Table1", 17),
                actions.BulkRemoveRecord("Table2", [17, 18]),
                actions.AddColumn("Table1", "Foo", {"type": "Text"}),
                actions.RenameColumn("Table1", "Foo", "Bar"),
                actions.ModifyColumn("Table1", "Foo", {"type": "Text"}),
                actions.RemoveColumn("Table1", "Foo"),
                actions.AddTable("THello", [{
                    "id": "Foo"
                }, {
                    "id": "Bar"
                }]),
                actions.RemoveTable("THello"),
                actions.RenameTable("THello", "TWorld"),
            ]

        def prune(table_id, col_id):
            a = alist()
            actions.prune_actions(a, table_id, col_id)
            return a

        self.assertEqual(
            prune('Table1', 'Foo'),
            [
                actions.BulkUpdateRecord("Table2", [1, 2, 3], {
                    'Foo': [10, 20, 30],
                    'Bar': ['a', 'b', 'c']
                }),
                actions.UpdateRecord("Table2", 18, {
                    'Foo': 10,
                    'Bar': 'a'
                }),
                actions.BulkAddRecord("Table2", 18, {
                    'Foo': 10,
                    'Bar': 'a'
                }),
                actions.ReplaceTableData("Table2", 18, {
                    'Foo': 10,
                    'Bar': 'a'
                }),
                actions.RemoveRecord("Table1", 17),
                actions.BulkRemoveRecord("Table2", [17, 18]),
                # It doesn't do anything with column renames; it can be addressed if needed.
                actions.RenameColumn("Table1", "Foo", "Bar"),
                # It doesn't do anything with AddTable, which is expected.
                actions.AddTable("THello", [{
                    "id": "Foo"
                }, {
                    "id": "Bar"
                }]),
                actions.RemoveTable("THello"),
                actions.RenameTable("THello", "TWorld"),
            ])

        self.assertEqual(prune('Table2', 'Foo'), [
            actions.BulkUpdateRecord("Table1", [1, 2, 3],
                                     {'Foo': [10, 20, 30]}),
            actions.BulkUpdateRecord("Table2", [1, 2, 3],
                                     {'Bar': ['a', 'b', 'c']}),
            actions.UpdateRecord("Table1", 17, {'Foo': 10}),
            actions.UpdateRecord("Table2", 18, {'Bar': 'a'}),
            actions.AddRecord("Table1", 17, {'Foo': 10}),
            actions.BulkAddRecord("Table2", 18, {'Bar': 'a'}),
            actions.ReplaceTableData("Table2", 18, {'Bar': 'a'}),
            actions.RemoveRecord("Table1", 17),
            actions.BulkRemoveRecord("Table2", [17, 18]),
            actions.AddColumn("Table1", "Foo", {"type": "Text"}),
            actions.RenameColumn("Table1", "Foo", "Bar"),
            actions.ModifyColumn("Table1", "Foo", {"type": "Text"}),
            actions.RemoveColumn("Table1", "Foo"),
            actions.AddTable("THello", [{
                "id": "Foo"
            }, {
                "id": "Bar"
            }]),
            actions.RemoveTable("THello"),
            actions.RenameTable("THello", "TWorld"),
        ])