[#1419] pilorama: Create new nodes in path if needed

Consider a node `{FileName: "dir", Attribute: "xxx"}`. In case we add
a new node by path `["dir", "file.txt"]`, create a new intermediate node
with a single attribute.

`GetByPath` now also considers only nodes with a single attribute while building a path.

Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
Evgenii Stratonikov 2022-05-23 14:32:24 +03:00
parent 730f14e4eb
commit 3cc67db083
5 changed files with 58 additions and 16 deletions

View file

@ -470,14 +470,10 @@ loop:
return 0, 0, err return 0, 0, err
} }
for j := range m.Items { // Internal nodes have exactly one attribute.
if m.Items[j].Key == attr { if len(m.Items) == 1 && m.Items[0].Key == attr && string(m.Items[0].Value) == path[i] {
if string(m.Items[j].Value) == path[i] { curNode = child
curNode = child continue loop
continue loop
}
break
}
} }
childKey, _ = c.Next() childKey, _ = c.Next()
} }

View file

@ -59,9 +59,12 @@ func (f *memoryForest) TreeAddByPath(cid cidSDK.ID, treeID string, attr string,
node = lm[j-i].Child node = lm[j-i].Child
s.operations = append(s.operations, lm[j-i]) s.operations = append(s.operations, lm[j-i])
} }
mCopy := make([]KeyValue, len(m))
copy(mCopy, m)
lm[len(lm)-1] = s.do(&Move{ lm[len(lm)-1] = s.do(&Move{
Parent: node, Parent: node,
Meta: Meta{Time: s.timestamp(), Items: m}, Meta: Meta{Time: s.timestamp(), Items: mCopy},
Child: s.findSpareID(), Child: s.findSpareID(),
}) })
return lm, nil return lm, nil

View file

@ -242,6 +242,44 @@ func testForestTreeAddByPath(t *testing.T, s Forest) {
require.Equal(t, 2, len(lm)) require.Equal(t, 2, len(lm))
testMeta(t, s, cid, treeID, lm[0].Child, lm[0].Parent, Meta{Time: lm[0].Time, Items: []KeyValue{{AttributeFilename, []byte("dir")}}}) testMeta(t, s, cid, treeID, lm[0].Child, lm[0].Parent, Meta{Time: lm[0].Time, Items: []KeyValue{{AttributeFilename, []byte("dir")}}})
testMeta(t, s, cid, treeID, lm[1].Child, lm[1].Parent, Meta{Time: lm[1].Time, Items: meta}) testMeta(t, s, cid, treeID, lm[1].Child, lm[1].Parent, Meta{Time: lm[1].Time, Items: meta})
t.Run("create internal nodes", func(t *testing.T) {
meta[0].Value = []byte("SomeValue")
meta[1].Value = []byte("another")
lm, err = s.TreeAddByPath(cid, treeID, AttributeFilename, []string{"path"}, meta)
require.NoError(t, err)
require.Equal(t, 1, len(lm))
oldMove := lm[0]
meta[0].Value = []byte("Leaf")
meta[1].Value = []byte("file.txt")
lm, err = s.TreeAddByPath(cid, treeID, AttributeFilename, []string{"path", "another"}, meta)
require.NoError(t, err)
require.Equal(t, 2, len(lm))
testMeta(t, s, cid, treeID, lm[0].Child, lm[0].Parent,
Meta{Time: lm[0].Time, Items: []KeyValue{{AttributeFilename, []byte("another")}}})
testMeta(t, s, cid, treeID, lm[1].Child, lm[1].Parent, Meta{Time: lm[1].Time, Items: meta})
require.NotEqual(t, lm[0].Child, oldMove.Child)
testMeta(t, s, cid, treeID, oldMove.Child, oldMove.Parent,
Meta{Time: oldMove.Time, Items: []KeyValue{
{AttributeVersion, []byte("SomeValue")},
{AttributeFilename, []byte("another")}}})
t.Run("get by path", func(t *testing.T) {
nodes, err := s.TreeGetByPath(cid, treeID, AttributeFilename, []string{"path", "another"}, false)
require.NoError(t, err)
require.Equal(t, 2, len(nodes))
require.ElementsMatch(t, []Node{lm[0].Child, oldMove.Child}, nodes)
nodes, err = s.TreeGetByPath(cid, treeID, AttributeFilename, []string{"path", "another", "file.txt"}, false)
require.NoError(t, err)
require.Equal(t, 1, len(nodes))
require.Equal(t, lm[1].Child, nodes[0])
})
})
} }
func TestForest_Apply(t *testing.T) { func TestForest_Apply(t *testing.T) {
@ -475,15 +513,18 @@ func testTreeGetByPath(t *testing.T, s Forest) {
} }
func testMove(t *testing.T, s Forest, ts int, node, parent Node, cid cidSDK.ID, treeID, filename, version string) { func testMove(t *testing.T, s Forest, ts int, node, parent Node, cid cidSDK.ID, treeID, filename, version string) {
items := make([]KeyValue, 1, 2)
items[0] = KeyValue{AttributeFilename, []byte(filename)}
if version != "" {
items = append(items, KeyValue{AttributeVersion, []byte(version)})
}
require.NoError(t, s.TreeApply(cid, treeID, &Move{ require.NoError(t, s.TreeApply(cid, treeID, &Move{
Parent: parent, Parent: parent,
Child: node, Child: node,
Meta: Meta{ Meta: Meta{
Time: uint64(ts), Time: uint64(ts),
Items: []KeyValue{ Items: items,
{AttributeFilename, []byte(filename)},
{AttributeVersion, []byte(version)},
},
}, },
})) }))
} }

View file

@ -184,8 +184,9 @@ loop:
for i := range path { for i := range path {
children := t.childMap[curNode] children := t.childMap[curNode]
for j := range children { for j := range children {
f := t.infoMap[children[j]].Meta.GetAttr(attr) meta := t.infoMap[children[j]].Meta
if string(f) == path[i] { f := meta.GetAttr(attr)
if len(meta.Items) == 1 && string(f) == path[i] {
curNode = children[j] curNode = children[j]
continue loop continue loop
} }

View file

@ -10,6 +10,7 @@ type Forest interface {
TreeMove(cid cidSDK.ID, treeID string, m *Move) (*LogMove, error) TreeMove(cid cidSDK.ID, treeID string, m *Move) (*LogMove, error)
// TreeAddByPath adds new node in the tree using provided path. // TreeAddByPath adds new node in the tree using provided path.
// The path is constructed by descending from the root using the values of the attr in meta. // The path is constructed by descending from the root using the values of the attr in meta.
// Internal nodes in path should have exactly one attribute, otherwise a new node is created.
TreeAddByPath(cid cidSDK.ID, treeID string, attr string, path []string, meta []KeyValue) ([]LogMove, error) TreeAddByPath(cid cidSDK.ID, treeID string, attr string, path []string, meta []KeyValue) ([]LogMove, error)
// TreeApply applies replicated operation from another node. // TreeApply applies replicated operation from another node.
TreeApply(cid cidSDK.ID, treeID string, m *Move) error TreeApply(cid cidSDK.ID, treeID string, m *Move) error