Skip to content

confdb: fix ordering of list paths on set/unset - #17687

Open
miguelpires wants to merge 1 commit into
canonical:masterfrom
miguelpires:fix-list-unset
Open

miguelpires wants to merge 1 commit into
canonical:masterfrom
miguelpires:fix-list-unset

Conversation

@miguelpires

@miguelpires miguelpires commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

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

@miguelpires miguelpires added the confdb confdb work (previously called registries and before aspects) label Sep 21, 2026
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>
@codecov

codecov Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.66667% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.42%. Comparing base (55d3556) to head (0d35440).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
confdb/confdb.go 94.66% 3 Missing and 1 partial ⚠️
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     
Flag Coverage Δ
unittests 78.42% <94.66%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Mon Sep 21 17:33:58 UTC 2026
The following results are from: https://github.com/canonical/snapd/actions/runs/35611417850

Test Predictor Analysis

Preparing

Test Success %
openstack:ubuntu-26.04-64:tests/main/nss-modules:nis 🟢 99.3%
openstack:ubuntu-26.04-64:tests/main/nss-modules:winbind 🟢 99.3%

Executing

Test Success %
openstack:ubuntu-core-24-64:tests/main/dbus-activation-session 🟢 99.0%
openstack:ubuntu-core-24-64:tests/main/snap-user-service 🟡 58.6%

Skipped tests from snapd-testing-skip

If 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)

  • openstack:arch-linux-64:tests/main/interfaces-location-control
  • openstack:debian-12-64:tests/main/interfaces-location-control
  • openstack:ubuntu-20.04-64:tests/main/interfaces-location-control
  • openstack:ubuntu-24.04-64:tests/main/auto-refresh-retry
  • openstack:ubuntu-24.04-64:tests/main/i18n
  • openstack:ubuntu-24.04-64:tests/main/interfaces-location-control
  • openstack:ubuntu-24.04-64:tests/main/network-retry
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-flag-restart
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-integration-tests
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-prompt-restoration
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-smoke
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-snapd-startup
  • openstack:ubuntu-26.04-64:tests/main/apparmor-prompting-support
  • openstack:ubuntu-26.04-64:tests/main/interfaces-location-control
  • openstack:ubuntu-26.04-64:tests/main/interfaces-requests-activates-handlers

@andrewphelpsj andrewphelpsj left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Seems that the comparison function needs a change.

Comment thread confdb/confdb.go
// 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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
FAIL

unset 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 branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

confdb confdb work (previously called registries and before aspects)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants