ReportDatabase#merge - a method to merge two report databases

This commit is contained in:
Matthias Koefferlein 2026-03-01 17:39:29 +01:00
parent 8a594c1626
commit 992947f748
5 changed files with 450 additions and 16 deletions

View File

@ -1676,6 +1676,15 @@ Class<rdb::Database> 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"
) +

View File

@ -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<id_type, id_type> &cat2cat)
static void map_category (const rdb::Category &cat, rdb::Database &db, std::map<id_type, id_type> &cat2cat, std::map<id_type, id_type> &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<id_type, id_type> &cell2cell,
std::map<id_type, id_type> &rev_cell2cell,
std::map<id_type, id_type> &cat2cat,
std::map<id_type, id_type> &rev_cat2cat,
std::map<id_type, id_type> &tag2tag,
std::map<id_type, id_type> &rev_tag2tag,
bool create_missing)
{
std::map<id_type, id_type> cell2cell;
std::map<id_type, id_type> cat2cat;
std::map<id_type, id_type> tag2tag;
std::map<id_type, id_type> 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<std::string, id_type> 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<id_type, id_type> cell2cell;
std::map<id_type, id_type> rev_cell2cell;
std::map<id_type, id_type> cat2cat;
std::map<id_type, id_type> rev_cat2cat;
std::map<id_type, id_type> tag2tag;
std::map<id_type, id_type> rev_tag2tag;
map_databases (*this, other, cell2cell, rev_cell2cell, cat2cat, rev_cat2cat, tag2tag, rev_tag2tag, false);
std::map<std::pair<id_type, id_type>, 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<id_type, id_type> cell2cell;
std::map<id_type, id_type> rev_cell2cell;
std::map<id_type, id_type> cat2cat;
std::map<id_type, id_type> rev_cat2cat;
std::map<id_type, id_type> tag2tag;
std::map<id_type, id_type> 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<std::pair<unsigned int, std::string> > &layers_and_descriptions, bool flat)
{

View File

@ -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
*

View File

@ -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<std::string> 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<std::string> 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<std::string> 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");
}

View File

@ -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")