confdb: fix ordering of list paths on set/unset - #17687
miguelpires wants to merge 1 commit into
Conversation
This correct the ordering of path operations to be aware of whether we're writing or removing data, as appropriate. It also changes the ordering path removals regarding lists to be the opposite of the writes. For instance, we want to remove list[2] before list[1].field, for the same reason we do the write in the opposite order - to avoid changing the meaning of subsequent operations. The one case where operations should be sorted in a way that ignores whether it's a write or removal is when the paths are ancestor/descendant, in that case the more specific order should "win". This also filters unsets of paths fully included in more general unsets since removing either list[1] or list[1].field can both result in list[1] being fully removed which then changes the meaning of whichever path is removed after. Signed-off-by: Miguel Pires <miguel.pires@canonical.com>
48486ee to
0d35440
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #17687 +/- ##
==========================================
+ Coverage 78.36% 78.42% +0.05%
==========================================
Files 1417 1407 -10
Lines 200507 200433 -74
Branches 2503 2503
==========================================
+ Hits 157129 157183 +54
+ Misses 33953 33820 -133
- Partials 9425 9430 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Mon Sep 21 17:33:58 UTC 2026 Test Predictor AnalysisPreparing
Executing
Skipped tests from snapd-testing-skipIf you wish to have any of the below tests run in your PR, in your PR description, add 'unskip:' followed by a copy-and-pasted list of the below tests you wish to run (unskip plus test list must be valid yaml)
|
andrewphelpsj
left a comment
There was a problem hiding this comment.
Thanks! Seems that the comparison function needs a change.
| // after [1] because the opposite would fail on a clean slate | ||
| // - removals use the opposite order so that removing a list element cannot | ||
| // shift an element that still needs to be removed | ||
| func byAccessor(getAccs accGetter) func(x, y int) bool { |
There was a problem hiding this comment.
This comparator is no longer transitive:
diff --git a/confdb/confdb.go b/confdb/confdb.go
index 0ca230d2e5..173ee1188a 100644
--- a/confdb/confdb.go
+++ b/confdb/confdb.go
@@ -1321,6 +1321,11 @@ func (v *View) Set(databag Databag, request string, value any) error {
sort.Slice(expandedMatches, byAccessor(getAccs))
+ for i, match := range expandedMatches {
+ fmt.Printf("sorted change %d: %s = %#v\n",
+ i, JoinAccessors(match.storagePath), match.value)
+ }
+
for _, match := range expandedMatches {
if err := databag.Set(match.storagePath, match.value); err != nil {
return err
diff --git a/confdb/confdb_test.go b/confdb/confdb_test.go
index 2048eefa71..39483e250c 100644
--- a/confdb/confdb_test.go
+++ b/confdb/confdb_test.go
@@ -5010,6 +5010,29 @@ func (*viewSuite) TestSetValueSetsAncestorUnsetsLeaf(c *C) {
c.Assert(stored, Equals, "value")
}
+func (*viewSuite) TestSetMixedChangesPreservesDescendant(c *C) {
+ schema, err := confdb.NewSchema("acc", "confdb", map[string]any{
+ "foo": map[string]any{
+ "rules": []any{
+ map[string]any{"request": "foo.values.{key}", "storage": "{key}.c"},
+ map[string]any{"request": "foo.other", "storage": "a"},
+ map[string]any{"request": "foo.old", "storage": "b"},
+ },
+ },
+ }, confdb.NewJSONSchema())
+ c.Assert(err, IsNil)
+
+ bag := confdb.NewJSONDatabag()
+ err = schema.View("foo").Set(bag, "foo", map[string]any{
+ "values": map[string]any{"b": "value"},
+ })
+ c.Assert(err, IsNil)
+
+ stored, err := bag.Get(parsePath(c, "b.c"), nil)
+ c.Assert(err, IsNil)
+ c.Assert(stored, Equals, "value")
+}
+
func (*viewSuite) TestListSetsLeafUnsetsAncestor(c *C) {
schema, err := confdb.NewSchema("acc", "confdb", map[string]any{
"foo": map[string]any{phelps@arch ~/s/pr> go test ./confdb/ -check.f TestSetMixedChangesPreservesDescendant
sorted change 0: b.c = "value"
sorted change 1: a = <nil>
sorted change 2: b = <nil>
----------------------------------------------------------------------
FAIL: confdb_test.go:5013: viewSuite.TestSetMixedChangesPreservesDescendant
confdb_test.go:5032:
c.Assert(err, IsNil)
... value *confdb.NoDataError = &confdb.NoDataError{requests:[]string(nil), view:""} ("cannot get : no data")
OOPS: 0 passed, 1 FAILED
--- FAIL: Test (0.01s)
FAIL
FAIL github.com/snapcore/snapd/confdb 0.019s
FAILunset a < unset b
return xAcc.Access() < yAcc.Access()unset b < set b.c
if len(xPath) != len(yPath) {
return len(xPath) < len(yPath)
}set b.c < unset a
if xOrder != yOrder {
return xOrder < yOrder
}sort.Slice assumes that unset a < unset b < set b.c, but that conflicts with set b.c < unset a.
This makes the ordering of path operations aware of whether we're writing or removing data. We want to remove list[2] before list[1].field, for the same reason we do the write in the opposite order - to avoid changing the meaning of subsequent operations. We also sort removals to go before writes since a write can provide a partial value that makes some paths be written to and others removed. If we don't do that the removal part can change the meaning of the subsequent write.
The one case where operations should be sorted in a way that ignores whether it's a write or removal is when the paths are ancestor/descendant, in that case the more specific order should "win".
This also filters out unsets of paths fully included in more general unset paths since removing either list[1] or list[1].field can result in list[1] being fully removed, which then changes the meaning of removing the second path.
https://warthogs.atlassian.net/browse/SNAPDENG-37511