Skip to content

Commit

Permalink
Merge pull request #1314 from anyproto/go-3192-normalize-rows-children
Browse files Browse the repository at this point in the history
GO-3192 | GO-3629 - Refactor table block normalization
  • Loading branch information
KirillSto authored Aug 6, 2024
2 parents 6c215a8 + ff30ade commit de8e3d1
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 98 deletions.
159 changes: 115 additions & 44 deletions core/block/editor/table/block.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package table

import (
"errors"
"fmt"
"sort"

Expand Down Expand Up @@ -28,8 +29,8 @@ func NewBlock(b *model.Block) simple.Block {

type Block interface {
simple.Block
Normalize(s *state.State) error
Duplicate(s *state.State) (newID string, visitedIds []string, blocks []simple.Block, err error)
Normalize(s *state.State) error
}

type block struct {
Expand All @@ -40,37 +41,6 @@ func (b *block) Copy() simple.Block {
return NewBlock(pbtypes.CopyBlock(b.Model()))
}

func (b *block) Normalize(s *state.State) error {
tb, err := NewTable(s, b.Id)
if err != nil {
log.Errorf("normalize table %s: broken table state: %s", b.Model().Id, err)
return nil
}

colIdx := map[string]int{}
for i, c := range tb.ColumnIDs() {
colIdx[c] = i
}

for _, rowID := range tb.RowIDs() {
row := s.Get(rowID)
// Fix data integrity by adding missing row
if row == nil {
row = makeRow(rowID)
if !s.Add(row) {
return fmt.Errorf("add missing row block %s", rowID)
}
continue
}
normalizeRow(s, colIdx, row)
}

if err := normalizeRows(s, tb); err != nil {
return fmt.Errorf("normalize rows: %w", err)
}
return nil
}

func (b *block) Duplicate(s *state.State) (newID string, visitedIds []string, blocks []simple.Block, err error) {
tb, err := NewTable(s, b.Id)
if err != nil {
Expand Down Expand Up @@ -144,6 +114,25 @@ func (b *block) Duplicate(s *state.State) (newID string, visitedIds []string, bl
return block.Model().Id, visitedIds, blocks, nil
}

func (b *block) Normalize(s *state.State) error {
tb, err := NewTable(s, b.Id)
if err != nil {
log.Errorf("normalize table %s: broken table state: %s", b.Id, err)
if !s.Unlink(b.Id) {
log.Errorf("failed to unlink table block: %s", b.Id)
}
return nil
}

tb.normalizeColumns()
tb.normalizeRows()
if err = tb.normalizeHeaderRows(); err != nil {
// actually we cannot get error here, as all rows are checked in normalizeRows
log.Errorf("normalize header rows: %v", err)
}
return nil
}

type rowSort struct {
indices []int
cells []string
Expand All @@ -164,13 +153,13 @@ func (r *rowSort) Swap(i, j int) {
r.cells[i], r.cells[j] = r.cells[j], r.cells[i]
}

func normalizeRows(s *state.State, tb *Table) error {
rows := s.Get(tb.Rows().Id)
func (tb Table) normalizeHeaderRows() error {
rows := tb.s.Get(tb.Rows().Id)

var headers []string
regular := make([]string, 0, len(rows.Model().ChildrenIds))
for _, rowID := range rows.Model().ChildrenIds {
row, err := pickRow(s, rowID)
row, err := pickRow(tb.s, rowID)
if err != nil {
return fmt.Errorf("pick row %s: %w", rowID, err)
}
Expand All @@ -182,14 +171,19 @@ func normalizeRows(s *state.State, tb *Table) error {
}
}

s.SetChildrenIds(rows.Model(), append(headers, regular...))
tb.s.SetChildrenIds(rows.Model(), append(headers, regular...))
return nil
}

func normalizeRow(s *state.State, colIdx map[string]int, row simple.Block) {
func (tb Table) normalizeRow(colIdx map[string]int, row simple.Block) {
if row == nil || row.Model() == nil {
return
}

if colIdx == nil {
colIdx = tb.MakeColumnIndex()
}

rs := &rowSort{
cells: make([]string, 0, len(row.Model().ChildrenIds)),
indices: make([]int, 0, len(row.Model().ChildrenIds)),
Expand All @@ -198,15 +192,15 @@ func normalizeRow(s *state.State, colIdx map[string]int, row simple.Block) {
for _, id := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(id)
if err != nil {
log.Warnf("normalize row %s: discard cell %s: invalid id", row.Model().Id, id)
log.Warnf("normalize row %s: move cell %s under the table: invalid id", row.Model().Id, id)
toRemove = append(toRemove, id)
rs.touched = true
continue
}

v, ok := colIdx[colID]
if !ok {
log.Warnf("normalize row %s: discard cell %s: column %s not found", row.Model().Id, id, colID)
log.Warnf("normalize row %s: move cell %s under the table: column %s not found", row.Model().Id, id, colID)
toRemove = append(toRemove, id)
rs.touched = true
continue
Expand All @@ -217,11 +211,88 @@ func normalizeRow(s *state.State, colIdx map[string]int, row simple.Block) {
sort.Sort(rs)

if rs.touched {
if s == nil {
row.Model().ChildrenIds = rs.cells
} else {
s.RemoveFromCache(toRemove)
s.SetChildrenIds(row.Model(), rs.cells)
tb.MoveBlocksUnderTheTable(toRemove...)
tb.s.SetChildrenIds(row.Model(), rs.cells)
}
}

func (tb Table) normalizeColumns() {
var (
invalidFound bool
colIds = make([]string, 0)
toRemove = make([]string, 0)
)

for _, colId := range tb.ColumnIDs() {
if _, err := pickColumn(tb.s, colId); err != nil {
invalidFound = true
switch {
case errors.Is(err, errColumnNotFound):
// Fix data integrity by adding missing column
log.Warnf("normalize columns '%s': column '%s' is not found: recreating it", tb.Columns().Id, colId)
col := makeColumn(colId)
if !tb.s.Add(col) {
log.Errorf("add missing column block %s", colId)
toRemove = append(toRemove, colId)
continue
}
colIds = append(colIds, colId)
case errors.Is(err, errNotAColumn):
log.Warnf("normalize columns '%s': block '%s' is not a column: move it under the table", tb.Columns().Id, colId)
tb.MoveBlocksUnderTheTable(colId)
default:
log.Errorf("pick column %s: %v", colId, err)
toRemove = append(toRemove, colId)
}
continue
}
colIds = append(colIds, colId)
}

if invalidFound {
tb.s.RemoveFromCache(toRemove)
tb.s.SetChildrenIds(tb.Columns(), colIds)
}
}

func (tb Table) normalizeRows() {
var (
invalidFound bool
rowIds = make([]string, 0)
toRemove = make([]string, 0)
colIdx = tb.MakeColumnIndex()
)

for _, rowId := range tb.RowIDs() {
row, err := getRow(tb.s, rowId)
if err != nil {
invalidFound = true
switch {
case errors.Is(err, errRowNotFound):
// Fix data integrity by adding missing row
log.Warnf("normalize rows '%s': row '%s' is not found: recreating it", tb.Rows().Id, rowId)
row = makeRow(rowId)
if !tb.s.Add(row) {
log.Errorf("add missing row block %s", rowId)
toRemove = append(toRemove, rowId)
continue
}
rowIds = append(rowIds, rowId)
case errors.Is(err, errNotARow):
log.Warnf("normalize rows '%s': block '%s' is not a row: move it under the table", tb.Rows().Id, rowId)
tb.MoveBlocksUnderTheTable(rowId)
default:
log.Errorf("get row %s: %v", rowId, err)
toRemove = append(toRemove, rowId)
}
continue
}
tb.normalizeRow(colIdx, row)
rowIds = append(rowIds, rowId)
}

if invalidFound {
tb.s.RemoveFromCache(toRemove)
tb.s.SetChildrenIds(tb.Rows(), rowIds)
}
}
115 changes: 76 additions & 39 deletions core/block/editor/table/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/block/simple/base"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
Expand All @@ -18,34 +19,41 @@ func TestNormalize(t *testing.T) {
want *state.State
}{
{
name: "empty",
name: "empty table should remain empty",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}),
},
{
name: "invalid ids",
name: "cells with invalid ids are moved under the table",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-c11", "row1-col2"},
{"row2-col3"},
{"row2-col3", "cell"},
}),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col2"},
{},
}),
{"row1-c11", "row1-col2"},
{"row2-col3", "cell"},
}, withChangedChildren(map[string][]string{
"root": {"table", "row2-col3", "cell", "row1-c11"},
"row1": {"row1-col2"},
"row2": {},
})),
},
{
name: "wrong column order",
name: "wrong cells order -> do sorting and move invalid cells under the table",
source: mkTestTable([]string{"col1", "col2", "col3"}, []string{"row1", "row2"}, [][]string{
{"row1-col3", "row1-col1", "row1-col2"},
{"row2-col3", "row2-c1", "row2-col1"},
}),
want: mkTestTable([]string{"col1", "col2", "col3"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2", "row1-col3"},
{"row2-col1", "row2-col3"},
}),
{"row2-col3", "row2-c1", "row2-col1"},
}, withChangedChildren(map[string][]string{
"root": {"table", "row2-c1"},
"row2": {"row2-col1", "row2-col3"},
})),
},
{
name: "wrong place for header rows",
name: "wrong place for header rows -> do sorting",
source: mkTestTable([]string{"col1", "col2", "col3"}, []string{"row1", "row2", "row3"}, nil,
withRowBlockContents(map[string]*model.BlockContentTableRow{
"row3": {IsHeader: true},
Expand All @@ -55,45 +63,74 @@ func TestNormalize(t *testing.T) {
"row3": {IsHeader: true},
})),
},
{
name: "cell is a child of rows, not row -> move under the table",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2"}, {"row2-col1", "row2-col2"},
}, withChangedChildren(map[string][]string{
"rows": {"row1", "row1-col2", "row2"},
"row1": {"row1-col1"},
})),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2"}, {"row2-col1", "row2-col2"},
}, withChangedChildren(map[string][]string{
"root": {"table", "row1-col2"},
"row1": {"row1-col1"},
})),
},
{
name: "columns contain invalid children -> move under the table",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2"}, {"row2-col1", "row2-col2"},
}, withChangedChildren(map[string][]string{
"columns": {"col1", "col2", "row1-col2"},
"row1": {"row1-col1"},
})),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2"}, {"row2-col1", "row2-col2"},
}, withChangedChildren(map[string][]string{
"root": {"table", "row1-col2"},
"row1": {"row1-col1"},
})),
},
{
name: "table block contains invalid children -> table is dropped",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}, withChangedChildren(map[string][]string{
"table": {"columns"},
})),
want: state.NewDoc("root", map[string]simple.Block{"root": simple.New(&model.Block{Id: "root"})}).NewState(),
},
{
name: "missed column is recreated",
source: mkTestTable([]string{"col1"}, []string{"row1", "row2"}, [][]string{}, withChangedChildren(map[string][]string{
"columns": {"col1", "col2"},
})),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}),
},
{
name: "missed row is recreated",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1"}, [][]string{}, withChangedChildren(map[string][]string{
"rows": {"row1", "row2"},
})),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}),
},
} {
t.Run(tc.name, func(t *testing.T) {
tb, err := NewTable(tc.source, "table")
// given
st := tc.source.Copy()
tb := st.Pick("table")
require.NotNil(t, tb)

require.NoError(t, err)
// when
err := tb.(Block).Normalize(st)

st := tc.source.Copy()
err = tb.block.(Block).Normalize(st)
// then
require.NoError(t, err)

assert.Equal(t, tc.want.Blocks(), st.Blocks())
})
}
}

func TestNormalizeAbsentRow(t *testing.T) {
source := mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2", "row3"}, [][]string{
{"row1-c11", "row1-col2"},
{"row2-col3"},
})
source.CleanupBlock("row3")

want := mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2", "row3"}, [][]string{
{"row1-col2"},
{},
{},
})

tb, err := NewTable(source, "table")

require.NoError(t, err)

st := source.Copy()
err = tb.block.(Block).Normalize(st)
require.NoError(t, err)

assert.Equal(t, want.Blocks(), st.Blocks())
}

func TestDuplicate(t *testing.T) {
s := mkTestTable([]string{"col1", "col2", "col3"}, []string{"row1", "row2"},
[][]string{
Expand Down
Loading

0 comments on commit de8e3d1

Please sign in to comment.