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 <[email protected]>
---
 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 --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
-- 
2.55.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to