From 992947f748ac1acec33303aa0ccc92f3f9915660 Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 1 Mar 2026 17:39:29 +0100 Subject: [PATCH] ReportDatabase#merge - a method to merge two report databases --- src/rdb/rdb/gsiDeclRdb.cc | 9 ++ src/rdb/rdb/rdb.cc | 118 +++++++++++++--- src/rdb/rdb/rdb.h | 21 +++ src/rdb/unit_tests/rdbTests.cc | 247 +++++++++++++++++++++++++++++++++ testdata/ruby/rdbTest.rb | 71 ++++++++++ 5 files changed, 450 insertions(+), 16 deletions(-) diff --git a/src/rdb/rdb/gsiDeclRdb.cc b/src/rdb/rdb/gsiDeclRdb.cc index ccadf6eff..95e370e73 100644 --- a/src/rdb/rdb/gsiDeclRdb.cc +++ b/src/rdb/rdb/gsiDeclRdb.cc @@ -1676,6 +1676,15 @@ Class decl_ReportDatabase ("rdb", "ReportDatabase", "\n" "This method has been added in version 0.29.1." ) + + gsi::method ("merge", &rdb::Database::merge, gsi::arg ("other"), + "@brief Merges the other database with this one\n" + "This method will merge the other database with this one. The other database needs to have the same top cell than\n" + "this database. In the merge step, identical cells and categories will be identified and missing cells or categories\n" + "will be created in this database. After that, all items will be transferred from the other database into this one\n" + "and will be associated with cells and categories from this database.\n" + "\n" + "This method has been added in version 0.30.7." + ) + gsi::method ("is_modified?", &rdb::Database::is_modified, "@brief Returns a value indicating whether the database has been modified\n" ) + diff --git a/src/rdb/rdb/rdb.cc b/src/rdb/rdb/rdb.cc index 4207721df..475f310e4 100644 --- a/src/rdb/rdb/rdb.cc +++ b/src/rdb/rdb/rdb.cc @@ -958,11 +958,18 @@ Tags::tag (id_type id) void Tags::import_tag (const Tag &t) { - Tag &tt = tag (t.name (), t.is_user_tag ()); - tt.set_description (t.description ()); + import_tag_with_ref (t); } -bool +Tag & +Tags::import_tag_with_ref (const Tag &t) +{ + Tag &tt = tag (t.name (), t.is_user_tag ()); + tt.set_description (t.description ()); + return tt; +} + +bool Tags::has_tag (const std::string &name, bool user_tag) const { return m_ids_for_names.find (std::make_pair (name, user_tag)) != m_ids_for_names.end (); @@ -1314,6 +1321,13 @@ Database::import_tags (const Tags &tags) } } +Tag & +Database::import_tag (const Tag &tag) +{ + set_modified (); + return m_tags.import_tag_with_ref (tag); +} + void Database::import_categories (Categories *categories) { @@ -1880,41 +1894,52 @@ namespace }; } -static void map_category (const rdb::Category &cat, const rdb::Database &db, std::map &cat2cat) +static void map_category (const rdb::Category &cat, rdb::Database &db, std::map &cat2cat, std::map &rev_cat2cat, bool create_missing, rdb::Category *parent) { - const rdb::Category *this_cat = db.category_by_name (cat.path ()); + rdb::Category *this_cat = db.category_by_name_non_const (cat.path ()); + if (! this_cat && create_missing) { + this_cat = db.create_category (parent, cat.name ()); + this_cat->set_description (cat.description ()); + } if (this_cat) { cat2cat.insert (std::make_pair (this_cat->id (), cat.id ())); + rev_cat2cat.insert (std::make_pair (cat.id (), this_cat->id ())); } for (auto c = cat.sub_categories ().begin (); c != cat.sub_categories ().end (); ++c) { - map_category (*c, db, cat2cat); + map_category (*c, db, cat2cat, rev_cat2cat, create_missing, this_cat); } } -void -Database::apply (const rdb::Database &other) +static void map_databases (rdb::Database &self, const rdb::Database &other, + std::map &cell2cell, + std::map &rev_cell2cell, + std::map &cat2cat, + std::map &rev_cat2cat, + std::map &tag2tag, + std::map &rev_tag2tag, + bool create_missing) { - std::map cell2cell; - std::map cat2cat; - std::map tag2tag; - std::map rev_tag2tag; - for (auto c = other.cells ().begin (); c != other.cells ().end (); ++c) { // TODO: do we have a consistent scheme of naming variants? What requirements // exist towards detecting variant specific waivers - const rdb::Cell *this_cell = cell_by_qname (c->qname ()); + rdb::Cell *this_cell = self.cell_by_qname_non_const (c->qname ()); + if (! this_cell && create_missing) { + this_cell = self.create_cell (c->name (), c->variant (), c->layout_name ()); + this_cell->import_references (c->references ()); + } if (this_cell) { cell2cell.insert (std::make_pair (this_cell->id (), c->id ())); + rev_cell2cell.insert (std::make_pair (c->id (), this_cell->id ())); } } for (auto c = other.categories ().begin (); c != other.categories ().end (); ++c) { - map_category (*c, *this, cat2cat); + map_category (*c, self, cat2cat, rev_cat2cat, create_missing, 0); } std::map tags_by_name; - for (auto c = tags ().begin_tags (); c != tags ().end_tags (); ++c) { + for (auto c = self.tags ().begin_tags (); c != self.tags ().end_tags (); ++c) { tags_by_name.insert (std::make_pair (c->name (), c->id ())); } @@ -1923,8 +1948,25 @@ Database::apply (const rdb::Database &other) if (t != tags_by_name.end ()) { tag2tag.insert (std::make_pair (t->second, c->id ())); rev_tag2tag.insert (std::make_pair (c->id (), t->second)); + } else if (create_missing) { + auto tt = self.import_tag (*c); + tag2tag.insert (std::make_pair (tt.id (), c->id ())); + rev_tag2tag.insert (std::make_pair (c->id (), tt.id ())); } } +} + +void +Database::apply (const rdb::Database &other) +{ + std::map cell2cell; + std::map rev_cell2cell; + std::map cat2cat; + std::map rev_cat2cat; + std::map tag2tag; + std::map rev_tag2tag; + + map_databases (*this, other, cell2cell, rev_cell2cell, cat2cat, rev_cat2cat, tag2tag, rev_tag2tag, false); std::map, ValueMapEntry> value_map; @@ -1963,6 +2005,50 @@ Database::apply (const rdb::Database &other) } } +void +Database::merge (const Database &other) +{ + if (top_cell_name () != other.top_cell_name ()) { + throw tl::Exception (tl::to_string (tr ("Merging of RDB databases requires identical top cell names"))); + } + + std::map cell2cell; + std::map rev_cell2cell; + std::map cat2cat; + std::map rev_cat2cat; + std::map tag2tag; + std::map rev_tag2tag; + + map_databases (*this, other, cell2cell, rev_cell2cell, cat2cat, rev_cat2cat, tag2tag, rev_tag2tag, true); + + for (Items::const_iterator i = other.items ().begin (); i != other.items ().end (); ++i) { + + auto icell = rev_cell2cell.find (i->cell_id ()); + if (icell == rev_cell2cell.end ()) { + continue; + } + + auto icat = rev_cat2cat.find (i->category_id ()); + if (icat == rev_cat2cat.end ()) { + continue; + } + + rdb::Item *this_item = create_item (icell->second, icat->second); + + this_item->set_values (i->values ()); + this_item->set_multiplicity (i->multiplicity ()); + this_item->set_comment (i->comment ()); + this_item->set_image_str (i->image_str ()); + + for (auto tt = rev_tag2tag.begin (); tt != rev_tag2tag.end (); ++tt) { + if (i->has_tag (tt->first)) { + this_item->add_tag (tt->second); + } + } + + } +} + void Database::scan_layout (const db::Layout &layout, db::cell_index_type cell_index, const std::vector > &layers_and_descriptions, bool flat) { diff --git a/src/rdb/rdb/rdb.h b/src/rdb/rdb/rdb.h index 4286e883a..62b340295 100644 --- a/src/rdb/rdb/rdb.h +++ b/src/rdb/rdb/rdb.h @@ -1987,6 +1987,11 @@ public: */ void import_tag (const Tag &tag); + /** + * @brief Import a tag and returns the reference for the new tag + */ + Tag &import_tag_with_ref (const Tag &tag); + /** * @brief Clear the collection of tags */ @@ -2143,6 +2148,11 @@ public: */ void import_tags (const Tags &tags); + /** + * @brief Import a tag + */ + Tag &import_tag (const Tag &tag); + /** * @brief Get the reference to the categories collection (const version) */ @@ -2478,6 +2488,17 @@ public: */ void apply (const rdb::Database &other); + /** + * @brief Merges another database to this one + * + * Merging requires the other database to have the same top cell. + * Merging involves: + * * categories present in other, but not in *this will be created (based on category name and parent category) + * * cells present in other, but not in *this will be created (based on cell name and variant) + * * items will be transferred from other to *this based on cell and category + */ + void merge (const rdb::Database &other); + /** * @brief Scans a layout into this RDB * diff --git a/src/rdb/unit_tests/rdbTests.cc b/src/rdb/unit_tests/rdbTests.cc index 3fd2d79a1..4ccc25a08 100644 --- a/src/rdb/unit_tests/rdbTests.cc +++ b/src/rdb/unit_tests/rdbTests.cc @@ -822,3 +822,250 @@ TEST(13_ApplyIgnoreUnknownTag) EXPECT_EQ (i1->tag_str (), "tag2"); } +TEST(20_MergeBasic) +{ + rdb::Database db1; + db1.set_top_cell_name ("A"); + + rdb::Database db2; + db1.set_top_cell_name ("B"); + + try { + // can't merge DB's with different top cell names + db1.merge (db2); + EXPECT_EQ (0, 1); + } catch (...) { } + + db1.set_top_cell_name ("TOP"); + db2.set_top_cell_name ("TOP"); + db1.merge (db2); + + { + rdb::Cell *cell = db2.create_cell ("A", "VAR", "ALAY"); + + rdb::Category *pcat = db2.create_category ("PCAT"); + rdb::Category *cat = db2.create_category (pcat, "CAT"); + cat->set_description ("A child category"); + + // create two tags + /*auto t1_id =*/ db2.tags ().tag ("T1").id (); + auto t2_id = db2.tags ().tag ("T2", true).id (); + + rdb::Item *item = db2.create_item (cell->id (), cat->id ()); + item->set_comment ("Comment"); + item->add_tag (t2_id); + item->set_image_str ("%nonsense%"); + item->set_multiplicity (42); + item->add_value (db::DBox (0, 0, 1.0, 1.5)); + item->add_value (42.0); + } + + db1.merge (db2); + + auto c = db1.cells ().begin (); + tl_assert (c != db1.cells ().end ()); + const rdb::Cell *cell = c.operator-> (); + ++c; + EXPECT_EQ (c == db1.cells ().end (), true); + EXPECT_EQ (cell->name (), "A"); + EXPECT_EQ (cell->variant (), "VAR"); + EXPECT_EQ (cell->layout_name (), "ALAY"); + + const rdb::Category *cat = db1.category_by_name ("PCAT.CAT"); + tl_assert (cat != 0); + EXPECT_EQ (cat->name (), "CAT"); + EXPECT_EQ (cat->path (), "PCAT.CAT"); + EXPECT_EQ (cat->description (), "A child category"); + EXPECT_EQ (cat->num_items (), size_t (1)); + + auto i = db1.items ().begin (); + tl_assert (i != db1.items ().end ()); + const rdb::Item *item = i.operator-> (); + ++i; + EXPECT_EQ (i == db1.items ().end (), true); + EXPECT_EQ (item->category_id (), cat->id ()); + EXPECT_EQ (item->cell_id (), cell->id ()); + EXPECT_EQ (item->comment (), "Comment"); + EXPECT_EQ (item->multiplicity (), size_t (42)); + EXPECT_EQ (item->has_image (), true); + EXPECT_EQ (item->image_str (), "%nonsense%") + EXPECT_EQ (item->values ().to_string (&db1), "box: (0,0;1,1.5);float: 42"); + EXPECT_EQ (item->tag_str (), "#T2"); +} + +TEST(21_MergeCategories) +{ + rdb::Database db1; + db1.set_top_cell_name ("TOP"); + + rdb::Database db2; + db2.set_top_cell_name ("TOP"); + + { + rdb::Category *pcat = db1.create_category ("PCAT"); + pcat->set_description ("db1"); + rdb::Category *cat = db1.create_category (pcat, "CAT"); + cat->set_description ("db1"); + } + + { + rdb::Category *pcat = db2.create_category ("PCAT"); + pcat->set_description ("db2a"); + rdb::Category *cat2 = db2.create_category (pcat, "CAT2"); + cat2->set_description ("db2a"); + + rdb::Category *pcat2 = db2.create_category ("PCAT2"); + pcat2->set_description ("db2b"); + rdb::Category *cat3 = db2.create_category (pcat2, "CAT3"); + cat3->set_description ("db2b"); + } + + db1.merge (db2); + + const rdb::Category *cat; + + cat = db1.category_by_name ("PCAT"); + tl_assert (cat != 0); + EXPECT_EQ (cat->name (), "PCAT"); + EXPECT_EQ (cat->description (), "db1"); + + cat = db1.category_by_name ("PCAT2"); + tl_assert (cat != 0); + EXPECT_EQ (cat->name (), "PCAT2"); + EXPECT_EQ (cat->description (), "db2b"); + + cat = db1.category_by_name ("PCAT.CAT"); + tl_assert (cat != 0); + EXPECT_EQ (cat->name (), "CAT"); + EXPECT_EQ (cat->description (), "db1"); + + cat = db1.category_by_name ("PCAT.CAT2"); + tl_assert (cat != 0); + EXPECT_EQ (cat->name (), "CAT2"); + EXPECT_EQ (cat->description (), "db2a"); + + cat = db1.category_by_name ("PCAT2.CAT3"); + tl_assert (cat != 0); + EXPECT_EQ (cat->name (), "CAT3"); + EXPECT_EQ (cat->description (), "db2b"); + + int ncat = 0; + for (auto c = db1.categories ().begin (); c != db1.categories ().end (); ++c) { + ++ncat; + for (auto s = c->sub_categories ().begin (); s != c->sub_categories ().end (); ++s) { + ++ncat; + } + } + EXPECT_EQ (ncat, 5); +} + +TEST(22_MergeCells) +{ + rdb::Database db1; + db1.set_top_cell_name ("TOP"); + + rdb::Database db2; + db2.set_top_cell_name ("TOP"); + + { + rdb::Cell *parent; + parent = db1.create_cell ("TOP"); + + rdb::Cell *cell; + cell = db1.create_cell ("A"); + cell->references ().insert (rdb::Reference (db::DCplxTrans (db::DVector (1.0, 2.0)), parent->id ())); + cell = db1.create_cell ("A", "VAR1", "ALAY"); + cell->references ().insert (rdb::Reference (db::DCplxTrans (db::DVector (1.0, -2.0)), parent->id ())); + } + + { + rdb::Cell *parent; + parent = db2.create_cell ("TOP"); + + rdb::Cell *cell; + cell = db2.create_cell ("B"); + cell->references ().insert (rdb::Reference (db::DCplxTrans (db::DVector (1.0, 0.0)), parent->id ())); + cell = db2.create_cell ("A"); + cell->references ().insert (rdb::Reference (db::DCplxTrans (db::DVector (1.0, 2.5)), parent->id ())); // reference not taken! + cell = db2.create_cell ("A", "VAR2", "ALAY"); + cell->references ().insert (rdb::Reference (db::DCplxTrans (db::DVector (1.0, -1.0)), parent->id ())); + } + + db1.merge (db2); + + std::set cells; + for (auto c = db1.cells ().begin (); c != db1.cells ().end (); ++c) { + if (c->references ().begin () != c->references ().end ()) { + cells.insert (c->qname () + "[" + c->references ().begin ()->trans_str () + "]"); + } else { + cells.insert (c->qname ()); + } + } + + EXPECT_EQ (tl::join (cells.begin (), cells.end (), ";"), "A:1[r0 *1 1,2];A:VAR1[r0 *1 1,-2];A:VAR2[r0 *1 1,-1];B[r0 *1 1,0];TOP"); +} + +TEST(23_MergeTags) +{ + rdb::Database db1; + db1.set_top_cell_name ("TOP"); + + rdb::Database db2; + db2.set_top_cell_name ("TOP"); + + db1.tags ().tag ("T1"); + db1.tags ().tag ("T2"); + + db2.tags ().tag ("T1"); + db2.tags ().tag ("T3", true); + + db1.merge (db2); + + std::set tags; + for (auto t = db1.tags ().begin_tags (); t != db1.tags ().end_tags (); ++t) { + tags.insert (t->is_user_tag () ? "#" + t->name () : t->name ()); + } + EXPECT_EQ (tl::join (tags.begin (), tags.end (), ";"), "#T3;T1;T2"); +} + +TEST(24_MergeItems) +{ + rdb::Database db1; + db1.set_top_cell_name ("TOP"); + + rdb::Database db2; + db2.set_top_cell_name ("TOP"); + + { + rdb::Cell *cell = db1.create_cell ("TOP"); + rdb::Category *cat1 = db1.create_category ("CAT1"); + rdb::Category *cat2 = db1.create_category ("CAT2"); + + rdb::Item *item; + item = db1.create_item (cell->id (), cat1->id ()); + item->set_comment ("db1a"); + item = db1.create_item (cell->id (), cat2->id ()); + item->set_comment ("db1b"); + } + + { + rdb::Cell *cell = db2.create_cell ("TOP"); + rdb::Category *cat1 = db2.create_category ("CAT1"); + rdb::Category *cat3 = db2.create_category ("CAT3"); + + rdb::Item *item; + item = db2.create_item (cell->id (), cat1->id ()); + item->set_comment ("db2a"); + item = db2.create_item (cell->id (), cat3->id ()); + item->set_comment ("db2b"); + } + + db1.merge (db2); + + std::set items; + for (auto i = db1.items ().begin (); i != db1.items ().end (); ++i) { + const rdb::Item *item = i.operator-> (); + items.insert (item->cell_qname () + ":" + item->category_name () + "=" + item->comment ()); + } + EXPECT_EQ (tl::join (items.begin (), items.end (), ";"), "TOP:CAT1=db1a;TOP:CAT1=db2a;TOP:CAT2=db1b;TOP:CAT3=db2b"); +} diff --git a/testdata/ruby/rdbTest.rb b/testdata/ruby/rdbTest.rb index 6d6f7e2b6..056a2c9f7 100644 --- a/testdata/ruby/rdbTest.rb +++ b/testdata/ruby/rdbTest.rb @@ -1205,6 +1205,77 @@ class RDB_TestClass < TestBase end + # apply + def test_16 + + rdb1 = RBA::ReportDatabase::new + cat = rdb1.create_category("CAT") + cell = rdb1.create_cell("TOP") + item = rdb1.create_item(cell, cat) + item.add_value("item1") + item.add_tag(rdb1.tag_id("t1")) + + assert_equal(item.tags_str, "t1") + + rdb2 = RBA::ReportDatabase::new + cat = rdb2.create_category("CAT") + cell = rdb2.create_cell("TOP") + item = rdb2.create_item(cell, cat) + item.add_value("item1") + item.add_tag(rdb2.tag_id("t2")) + + assert_equal(item.tags_str, "t2") + + items = rdb1.each_item.to_a + assert_equal(items.size, 1) + + assert_equal(items[0].tags_str, "t1") + + rdb1.apply(rdb2) + + items = rdb1.each_item.to_a + assert_equal(items.size, 1) + + assert_equal(items[0].tags_str, "t2") + + end + + # merge + def test_17 + + rdb1 = RBA::ReportDatabase::new + cat = rdb1.create_category("CAT") + cell = rdb1.create_cell("TOP") + item = rdb1.create_item(cell, cat) + item.add_value("item1") + item.add_tag(rdb1.tag_id("t1")) + + assert_equal(item.tags_str, "t1") + + rdb2 = RBA::ReportDatabase::new + cat = rdb2.create_category("CAT") + cell = rdb2.create_cell("TOP") + item = rdb2.create_item(cell, cat) + item.add_value("item1") + item.add_tag(rdb2.tag_id("t2")) + + assert_equal(item.tags_str, "t2") + + items = rdb1.each_item.to_a + assert_equal(items.size, 1) + + assert_equal(items[0].tags_str, "t1") + + rdb1.merge(rdb2) + + items = rdb1.each_item.to_a + assert_equal(items.size, 2) + + assert_equal(items[0].tags_str, "t1") + assert_equal(items[1].tags_str, "t2") + + end + end load("test_epilogue.rb")