OK, so I think the qofinstance (and lower) code relative to guid
functionality is mostly right.  What I think the expected behaviour
is:
1. qof_instance_kvp_add_guid(path, key):
   - If path is not empty, it is removed to make room for the new one.
   - if path is empty it adds the guid under key and time under "date"
     as a frame on path.
2. The {path} is either a FRAME with key and "date" fields, or a glist
   with one entry with "date" and key fields, and the rest with just
   key fields.
3. The merge merges all these into the target either as a glist of
   kvp, and removes from the donor.

I do think there is a bug in the merge where it ignores the merging
being done by KvpVallue::add instead of setting it.  Specifically:

modified libgnucash/engine/qofinstance.cpp @@ -1257,7 +1257,7 @@
qof_instance_kvp_merge_guids (const QofInstance *target, { case
KvpValue::Type::FRAME: if (target_val)
- target_val->add(v);
+ target->kvp_data->set({path}, target_val->add(v)); //Contents moved,
  Don't delete!  else target->kvp_data->set_path({path}, v);
  donor->kvp_data->set({path}, nullptr); //Contents moved, Don't
  delete!

With this fix at the only caller of KvpValue::add that function works
fine.  Without this the add only works if it is a list, which it never
is.  I cannot fix this in KvpValue::add. If it is a frame, cannot make
itself a list because it would be doubly referenced both in the frame
that it is in, and in the list that it put itself in. (FYI: I'm
looking forward to the c++ conversion and using smart pointers as
memory management.  The memory management is really hard to follow/get
right.)

I don't know if this fixes any real bugs or not.  I do know that with
this the qofinstance can store a list of guids, and the following test
covers all the interesting functionality for both lists and frames.

TEST_F(OqfInstanceTest, merge_guids)
{
    auto inst3 = static_cast<QofInstance*>(g_object_new(QOF_TYPE_INSTANCE,
nullptr));
    auto inst4 = static_cast<QofInstance*>(g_object_new(QOF_TYPE_INSTANCE,
nullptr));
    auto gncGuid2 = guid_new();
    auto gncGuid3 = guid_new();
    auto gncGuid4 = guid_new();
    auto gncGuid5 = guid_new();

    // Add merge from 1 to 0 guids.
    qof_instance_kvp_add_guid(m_inst2, "donor", 456u, "donortime",
gncGuid2);
    qof_instance_kvp_merge_guids(m_inst, m_inst2, "donor");
    EXPECT_EQ(qof_instance_get_path_kvp<Time64>(m_inst, {"donor",
"date"}).value().t, 456u);
    EXPECT_TRUE(qof_instance_kvp_has_guid(m_inst, "donor", "donortime",
gncGuid2));
    EXPECT_FALSE(qof_instance_kvp_has_guid(m_inst2, "donor", "donortime",
gncGuid2));

    // Merge from 1 to 1 guids.
    qof_instance_kvp_add_guid(m_inst2, "donor", 456u, "donortime",
gncGuid3);
    qof_instance_kvp_merge_guids(m_inst, m_inst2, "donor");
    EXPECT_TRUE(qof_instance_kvp_has_guid(m_inst, "donor", "donortime",
gncGuid3));
    EXPECT_FALSE(qof_instance_kvp_has_guid(m_inst2, "donor", "donortime",
gncGuid3));

    // Merge from 2 (m_inst2) to to 2 guids.
    qof_instance_kvp_add_guid(m_inst2, "donor", 456u, "donortime",
gncGuid4);
    qof_instance_kvp_add_guid(inst3, "donor", 456u, "donortime", gncGuid5);
    qof_instance_kvp_merge_guids(m_inst2, inst3, "donor");
    qof_instance_kvp_merge_guids(m_inst, m_inst2, "donor");
    EXPECT_TRUE(qof_instance_kvp_has_guid(m_inst, "donor", "donortime",
gncGuid5));
    EXPECT_FALSE(qof_instance_kvp_has_guid(m_inst2, "donor", "donortime",
gncGuid5));

    // Merge from 4 to p guids.
    EXPECT_TRUE(qof_instance_kvp_has_guid(m_inst, "donor", "donortime",
gncGuid5));
    qof_instance_kvp_merge_guids(inst4, m_inst, "donor");
    EXPECT_TRUE(qof_instance_kvp_has_guid(inst4, "donor", "donortime",
gncGuid5));
    EXPECT_FALSE(qof_instance_kvp_has_guid(m_inst, "donor", "donortime",
gncGuid5));
    qof_instance_kvp_remove_guid(inst4, "donor", "donortime", gncGuid5);
    EXPECT_FALSE(qof_instance_kvp_has_guid(inst4, "donor", "donortime",
gncGuid5));
    EXPECT_TRUE(qof_instance_kvp_has_guid(inst4, "donor", "donortime",
gncGuid4));
    EXPECT_TRUE(qof_instance_kvp_has_guid(inst4, "donor", "donortime",
gncGuid3));

    g_object_unref(inst3);
    g_object_unref(inst4);
}

I don't know if this is the expected behavior for the higher level
code.  It seems a little funky that sometimes the date/time is not
stored in the list, or that the keys could be different, but that is
likely OK.

I also don't know how this would affect real usage. There is no
mentions (that a quick google search could find) of peer splits in the
documentation. I did chance this up that the merge is only used in
xaccScrubMergeLotSubSplits.  But that is called in more places I am
not familiar with. If you find a bug for this I can make a pull
request with this.

You mentioned capgains testing.  If you point me in the right
direction, I can take a look, but I don't know where to start at this
point.

On Wed, Jan 28, 2026 at 11:07 PM John Ralls <[email protected]> wrote:

>
>
> > On Jan 28, 2026, at 05:35, Stefan Koch <[email protected]>
> wrote:
> >
> > There is one more issue related to this in qof_instance_kvp_merge_guids
> here:
> >         if (target_val)
> >             target_val->add(v);
> >
> > This does not work if the target_val is not already a glist.  In that
> case, the KvpValueImpl::add creates a new GList which it returns, that the
> above code ignores. (If it was already a list it should work OK.).
> >
> > Since the qof_instance_kvp_merge_guids is only used in the peer splits
> we looked at yesterday, I think it is safe (readonable?) to assume that it
> will not use the glist functionality here.
> >
> > I will add the following change to the removal of glist cases in
> qofinstance guid functionality.
> >
> > modified   libgnucash/engine/qofinstance.cpp
> > @@ -1220,7 +1220,7 @@ qof_instance_kvp_merge_guids (const QofInstance
> *target,
> >      if(v->get_type() == KvpValue::Type::FRAME)
> >      {
> >          if (target_val)
> > -            target_val->add(v);
> > +            PWARN ("Instance KVP on path %s already exists.", path);
> >          else
> >              target->kvp_data->set_path({path}, v);
> >          donor->kvp_data->set({path}, nullptr); //Contents moved, Don't
> delete!
> >
> > This still leaves the tricky to use KvpValueImpl::add.  But it turns out
> that this is only used here, and so when I remove it here, it has no other
> uses (besides the unit test).   I will delete that functionality in the
> same commit.
> >
> > I have created the pull request for this change (with the trivial idata
> property fix as a previous commit).
>
> Stefan,
>
> No, KvpVallue::add does work if it doesn’t contain a GLIst: It creates a
> new KvpValue of type GLIST and populates its Glist with the original value
> and the new one. It doesn’t matter that gnc_instance_kvp_merge_guid
> discards the returned KvpValue* because it doesn’t have any use for it:
> What’s important is that the Split’s peer slot has the new GLIST KvpValue
> and subsequent calls to retrieve that slot or KVP path will get it.
>
> This also changes the equation for testing because it means that there is
> a way to get a GUID-GLIST Kvp that you can use to exercise the GLIST
> branches of gnc_instance_kvp_has_guid and gnc_instance_kvp_remove_guid.
>
> Looks like we need better capgains testing before we muck with this code.
>
> Regards,
> John Ralls
>
>
_______________________________________________
gnucash-devel mailing list
[email protected]
https://lists.gnucash.org/mailman/listinfo/gnucash-devel

Reply via email to