diff mbox series

[ovs-dev] python: idl: Return schema defaults for unset columns on new rows.

Message ID 20260918134623.1303510-1-twilson@redhat.com
State New
Delegated to: Ilya Maximets
Headers show
Series [ovs-dev] python: idl: Return schema defaults for unset columns on new rows. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_FreeBSD_Build_and_Test success github build: passed

Commit Message

Terry Wilson Sept. 18, 2026, 1:46 p.m. UTC
A newly-inserted, uncommitted Row raised AttributeError for any column
not explicitly set, so it did not present the columns it will have once
committed and could not be maintained in custom indexes before commit.

Return the schema default for such columns instead, matching the C IDL:
ovsdb_idl_read() falls through to ovsdb_datum_default() when a column has
no committed value and was not written this transaction. Like C, this is
lazy (defaults are not materialized at insert) and does not affect commit
serialization, which still omits default-valued columns. The 'old' row in
an update notification keeps a partial _data dict and still raises for
columns it does not carry.

custom_index: guard remove() so a row never added (lacking the indexed
columns) is a no-op rather than an error, matching add() behavior.

Assisted-By: Claude Opus 4.8
Signed-off-by: Terry Wilson <twilson@redhat.com>
---
 python/ovs/db/custom_index.py |  4 ++++
 python/ovs/db/idl.py          | 20 +++++++++++++++++---
 tests/test-ovsdb.py           |  5 ++++-
 3 files changed, 25 insertions(+), 4 deletions(-)
diff mbox series

Patch

diff --git a/python/ovs/db/custom_index.py b/python/ovs/db/custom_index.py
index 3fa03d3c9..15215b3e3 100644
--- a/python/ovs/db/custom_index.py
+++ b/python/ovs/db/custom_index.py
@@ -63,6 +63,10 @@  class MultiColumnIndex(object):
         self.values.add(self.index_entry_from_row(row))
 
     def remove(self, row):
+        if not all(hasattr(row, col.column) for col in self.columns):
+            # The row was never added because it lacks the necessary columns
+            # (see add()), so there is nothing to remove.
+            return
         self.values.remove(self.index_entry_from_row(row))
 
     def clear(self):
diff --git a/python/ovs/db/idl.py b/python/ovs/db/idl.py
index fe504a63a..d9a28fe1f 100644
--- a/python/ovs/db/idl.py
+++ b/python/ovs/db/idl.py
@@ -1376,6 +1376,12 @@  class Row(object):
         else:
             return atom
 
+    def _default_to_row(self, atom, base):
+        # A default Datum built by Datum.default() carries scalar atoms whose
+        # .value is itself an Atom (Atom.default() double-wraps), so unwrap the
+        # extra layer before resolving references the usual way.
+        return self._uuid_to_row(atom.value, base)
+
     def __getattr__(self, column_name):
         assert self._changes is not None
         assert self._mutations is not None
@@ -1395,9 +1401,17 @@  class Row(object):
         if datum is None:
             if self._data is None:
                 if inserts is None:
-                    raise AttributeError("%s instance has no attribute '%s'" %
-                                         (self.__class__.__name__,
-                                          column_name))
+                    # This is a newly-inserted row (no committed form) whose
+                    # column has not been set in this transaction. Expose the
+                    # schema default so the uncommitted row presents the same
+                    # columns it will have once committed (the server fills
+                    # unset columns with their defaults). This keeps attribute
+                    # access and index maintenance consistent before and after
+                    # commit. The 'old' row in an update notification instead
+                    # has a (partial) _data dict, so it still raises below for
+                    # columns it does not carry.
+                    return data.Datum.default(column.type).to_python(
+                        self._default_to_row)
                 else:
                     datum = data.Datum.from_python(column.type,
                                                    inserts,
diff --git a/tests/test-ovsdb.py b/tests/test-ovsdb.py
index 394897648..c328fccd6 100644
--- a/tests/test-ovsdb.py
+++ b/tests/test-ovsdb.py
@@ -612,8 +612,11 @@  def idl_set(idl, commands, step):
             l1_1.ka = [l1_0, l1_1]
         elif name == 'getattrtest':
             l1 = txn.insert(idl.tables["link1"])
+            # A column that has not been set on a newly-inserted row returns
+            # the schema default (here, 0 for an integer), matching how the
+            # row will look once committed.
             i = getattr(l1, 'i', 1)
-            assert i == 1
+            assert i == 0
             l1.i = 2
             i = getattr(l1, 'i', 1)
             assert i == 2