summaryrefslogtreecommitdiff
path: root/odb/odb/relational/changelog.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'odb/odb/relational/changelog.cxx')
-rw-r--r--odb/odb/relational/changelog.cxx1239
1 files changed, 1239 insertions, 0 deletions
diff --git a/odb/odb/relational/changelog.cxx b/odb/odb/relational/changelog.cxx
new file mode 100644
index 0000000..99f72da
--- /dev/null
+++ b/odb/odb/relational/changelog.cxx
@@ -0,0 +1,1239 @@
+// file : odb/relational/changelog.cxx
+// license : GNU GPL v3; see accompanying LICENSE file
+
+#include <map>
+
+#include <odb/diagnostics.hxx>
+
+#include <odb/semantics/relational.hxx>
+#include <odb/traversal/relational.hxx>
+
+#include <odb/relational/context.hxx>
+#include <odb/relational/generate.hxx>
+
+using namespace std;
+
+namespace relational
+{
+ namespace changelog
+ {
+ using namespace sema_rel;
+ using sema_rel::model;
+ using sema_rel::changelog;
+
+ typedef map<qname, semantics::node*> deleted_table_map;
+ typedef map<uname, semantics::data_member*> deleted_column_map;
+
+ namespace
+ {
+ //
+ // diff
+ //
+
+ struct diff_table: trav_rel::column,
+ trav_rel::primary_key,
+ trav_rel::foreign_key,
+ trav_rel::index
+ {
+ enum mode_type {mode_add, mode_drop};
+
+ diff_table (table& o,
+ mode_type m,
+ alter_table& a,
+ graph& gr,
+ options const& op,
+ model_version const* v)
+ : other (o), mode (m), at (a), g (gr), ops (op), version (v) {}
+
+ virtual void
+ traverse (sema_rel::column& c)
+ {
+ using sema_rel::column;
+
+ if (mode == mode_add)
+ {
+ if (column* oc = other.find<column> (c.name ()))
+ {
+ if (c.type () != oc->type ())
+ diagnose_column (c, "type", oc->type (), c.type ());
+
+ if (c.null () != oc->null ())
+ {
+ alter_column& ac (g.new_node<alter_column> (c.id ()));
+
+ // Set the alters edge.
+ //
+ column* b (at.lookup<column, drop_column> (c.name ()));
+ assert (b != 0);
+ g.new_edge<alters> (ac, *b);
+
+ ac.null (c.null ());
+ g.new_edge<unames> (at, ac, c.name ());
+ }
+
+ if (c.default_ () != oc->default_ ())
+ diagnose_column (
+ c, "default value", oc->default_ (), c.default_ ());
+
+ if (c.options () != oc->options ())
+ diagnose_column (c, "options", oc->options (), c.options ());
+ }
+ else
+ {
+ if (version != 0)
+ {
+ data_member_path const& mp (
+ c.get<data_member_path> ("member-path"));
+
+ semantics::data_member* m (context::added_member (mp));
+ if (m != 0)
+ {
+ // Make sure the addition version is the current version.
+ //
+ if (context::added (*m) != version->current)
+ {
+ location l (m->get<location_t> ("added-location"));
+ error (l) << "member addition version is not the same " <<
+ "as the current model version" << endl;
+ throw operation_failed ();
+ }
+ }
+ // Warn about hard additions. If the version is closed, then
+ // we have already seen these warnings so no need to repeat
+ // them.
+ //
+ else if (ops.warn_hard_add () && version->open)
+ {
+ // Issue nicer diagnostics for the direct data member case.
+ //
+ if (mp.size () == 1)
+ {
+ location l (mp.back ()->location ());
+ warn (l) << "data member is hard-added" << endl;
+ }
+ else
+ {
+ semantics::class_& s (
+ dynamic_cast<semantics::class_&> (
+ mp.front ()->scope ()));
+
+ warn (s.location ()) << "column '" << c.name () << "' " <<
+ "in class '" << s.name () << "' is hard-added" << endl;
+
+ for (data_member_path::const_iterator i (mp.begin ());
+ i != mp.end (); ++i)
+ {
+ info ((*i)->location ()) << "corresponding hard-" <<
+ "added data member could be '" << (*i)->name () <<
+ "'" << endl;
+ }
+ }
+ }
+ }
+
+ add_column& ac (g.new_node<add_column> (c, at, g));
+ g.new_edge<unames> (at, ac, c.name ());
+ }
+ }
+ else
+ {
+ if (other.find<column> (c.name ()) == 0)
+ {
+ if (version != 0)
+ {
+ // See if we have an entry for this column in the soft-
+ // deleted map.
+ //
+ deleted_column_map const& dm (
+ other.get<deleted_column_map> ("deleted-map"));
+ deleted_column_map::const_iterator i (dm.find (c.name ()));
+
+ if (i != dm.end ())
+ {
+ if (context::deleted (*i->second) != version->current)
+ {
+ location l (i->second->get<location_t> ("deleted-location"));
+ error (l) << "member deletion version is not the same " <<
+ "as the current model version" << endl;
+ throw operation_failed ();
+ }
+ }
+ // Warn about hard deletions. If the version is closed, then
+ // we have already seen these warnings so no need to repeat
+ // them.
+ //
+ else if (ops.warn_hard_delete () && version->open)
+ {
+ // Here all we have is a column name and a class (object)
+ // or data member (container) corresponding to the table.
+ //
+ if (semantics::class_* s =
+ other.get<semantics::class_*> ("class", 0))
+ {
+ warn (s->location ()) << "column '" << c.name () << "' " <<
+ "in class '" << s->name () << "' is hard-deleted" << endl;
+ }
+ else
+ {
+ data_member_path const& mp (
+ other.get<data_member_path> ("member-path"));
+
+ warn (mp.back ()->location ()) << "column '" <<
+ c.name () << "' in container '" <<
+ mp.back ()->name () << "' is hard-deleted" << endl;
+ }
+ }
+ }
+
+ drop_column& dc (g.new_node<drop_column> (c.id ()));
+ g.new_edge<unames> (at, dc, c.name ());
+ }
+ }
+ }
+
+ virtual void
+ traverse (sema_rel::primary_key& pk)
+ {
+ using sema_rel::primary_key;
+
+ if (mode == mode_add)
+ {
+ if (primary_key* opk = other.find<primary_key> (pk.name ()))
+ {
+ if (pk.auto_ () != opk->auto_ ())
+ diagnose_primary_key (pk, "auto kind");
+
+ // Database-specific information.
+ //
+ for (primary_key::extra_map::const_iterator i (
+ pk.extra ().begin ()); i != pk.extra ().end (); ++i)
+ {
+ if (opk->extra ().count (i->first) == 0 ||
+ opk->extra ()[i->first] != i->second)
+ diagnose_primary_key (pk, i->first.c_str ());
+ }
+
+ for (primary_key::extra_map::const_iterator i (
+ opk->extra ().begin ()); i != opk->extra ().end (); ++i)
+ {
+ if (pk.extra ().count (i->first) == 0 ||
+ pk.extra ()[i->first] != i->second)
+ diagnose_primary_key (pk, i->first.c_str ());
+ }
+
+ if (pk.contains_size () != opk->contains_size ())
+ diagnose_primary_key (pk, "member set");
+
+ for (primary_key::contains_size_type i (0);
+ i != pk.contains_size (); ++i)
+ {
+ sema_rel::contains& c (pk.contains_at (i));
+ sema_rel::contains& oc (opk->contains_at (i));
+
+ if (c.column ().name () != oc.column ().name ())
+ diagnose_primary_key (pk, "member set");
+ }
+ }
+ else
+ {
+ location const& l (pk.get<location> ("cxx-location"));
+ error (l) << "adding object id to an existing class is " <<
+ "not supported" << endl;
+ info (l) << "consider re-implementing this change by adding " <<
+ "a new class with the object id, migrating the data, and " <<
+ "deleteing the old class" << endl;
+ throw operation_failed ();
+ }
+ }
+ else
+ {
+ if (other.find<primary_key> (pk.name ()) == 0)
+ {
+ location const& l (other.get<location> ("cxx-location"));
+ error (l) << "deleting object id from an existing class is " <<
+ "not supported" << endl;
+ info (l) << "consider re-implementing this change by adding " <<
+ "a new class without the object id, migrating the data, " <<
+ "and deleteing the old class" << endl;
+ throw operation_failed ();
+ }
+ }
+ }
+
+ virtual void
+ traverse (sema_rel::foreign_key& fk)
+ {
+ using sema_rel::foreign_key;
+
+ if (mode == mode_add)
+ {
+ if (foreign_key* ofk = other.find<foreign_key> (fk.name ()))
+ {
+ if (fk.deferrable () != ofk->deferrable ())
+ diagnose_foreign_key (fk, "deferrable mode");
+
+ if (fk.on_delete () != ofk->on_delete ())
+ diagnose_foreign_key (fk, "on delete action");
+
+ if (fk.referenced_table () != ofk->referenced_table ())
+ // See diagnose_foreign_key() if changing this name.
+ //
+ diagnose_foreign_key (fk, "pointed-to class");
+
+ if (fk.referenced_columns () != ofk->referenced_columns ())
+ diagnose_foreign_key (fk, "id member set");
+
+ if (fk.contains_size () != ofk->contains_size ())
+ diagnose_foreign_key (fk, "id member set");
+
+ for (foreign_key::contains_size_type i (0);
+ i != fk.contains_size (); ++i)
+ {
+ sema_rel::contains& c (fk.contains_at (i));
+ sema_rel::contains& oc (ofk->contains_at (i));
+
+ if (c.column ().name () != oc.column ().name ())
+ diagnose_foreign_key (fk, "id member set");
+ }
+ }
+ else
+ {
+ add_foreign_key& afk (g.new_node<add_foreign_key> (fk, at, g));
+ g.new_edge<unames> (at, afk, fk.name ());
+ }
+ }
+ else
+ {
+ if (other.find<foreign_key> (fk.name ()) == 0)
+ {
+ drop_foreign_key& dfk (g.new_node<drop_foreign_key> (fk.id ()));
+ g.new_edge<unames> (at, dfk, fk.name ());
+ }
+ }
+ }
+
+ virtual void
+ traverse (sema_rel::index& i)
+ {
+ using sema_rel::index;
+
+ if (mode == mode_add)
+ {
+ if (index* oi = other.find<index> (i.name ()))
+ {
+ if (i.type () != oi->type ())
+ diagnose_index (i, "type", oi->type (), i.type ());
+
+ if (i.method () != oi->method ())
+ diagnose_index (i, "method", oi->method (), i.method ());
+
+ if (i.options () != oi->options ())
+ diagnose_index (i, "options", oi->options (), i.options ());
+
+ if (i.contains_size () != oi->contains_size ())
+ diagnose_index (i, "member set", "", "");
+
+ for (index::contains_size_type j (0);
+ j != i.contains_size (); ++j)
+ {
+ sema_rel::contains& c (i.contains_at (j));
+ sema_rel::contains& oc (oi->contains_at (j));
+
+ if (c.column ().name () != oc.column ().name ())
+ diagnose_index (i, "member set", "", "");
+
+ if (c.options () != oc.options ())
+ diagnose_index (
+ i, "member options", oc.options (), c.options ());
+ }
+ }
+ else
+ {
+ add_index& ai (g.new_node<add_index> (i, at, g));
+ g.new_edge<unames> (at, ai, i.name ());
+ }
+ }
+ else
+ {
+ if (other.find<index> (i.name ()) == 0)
+ {
+ drop_index& di (g.new_node<drop_index> (i.id ()));
+ g.new_edge<unames> (at, di, i.name ());
+ }
+ }
+ }
+
+ void
+ diagnose_column (sema_rel::column& c,
+ char const* name,
+ string const& ov,
+ string const& nv)
+ {
+ table& t (c.table ());
+ location const& tl (t.get<location> ("cxx-location"));
+ location const& cl (c.get<location> ("cxx-location"));
+
+ error (cl) << "change to data member results in the change of " <<
+ "the corresponding column " << name;
+
+ if (!ov.empty () || !nv.empty ())
+ cerr << " (old: '" << ov << "', new: '" << nv << "')";
+
+ cerr << endl;
+
+ error (cl) << "this change is not yet handled automatically" << endl;
+ info (cl) << "corresponding column '" << c.name () << "' " <<
+ "originates here" << endl;
+ info (tl) << "corresponding table '" << t.name () << "' " <<
+ "originates here" << endl;
+ info (cl) << "consider re-implementing this change by adding " <<
+ "a new data member with the desired " << name << ", migrating " <<
+ "the data, and deleting the old data member" << endl;
+
+ throw operation_failed ();
+ }
+
+ void
+ diagnose_primary_key (sema_rel::primary_key& pk, char const* name)
+ {
+ location const& l (pk.get<location> ("cxx-location"));
+
+ error (l) << "changing object id " << name << " in an existing " <<
+ "class is not supported" << endl;
+ info (l) << "consider re-implementing this change by adding " <<
+ "a new class with the desired object id " << name << ", " <<
+ "migrating the data, and deleteing the old class" << endl;
+
+ throw operation_failed ();
+ }
+
+ void
+ diagnose_foreign_key (sema_rel::foreign_key& fk, char const* name)
+ {
+ // This can be an object pointer or a polymorphic base link.
+ // The latter will trigger this call if we change one base
+ // to another.
+ //
+ using sema_rel::table;
+ using sema_rel::foreign_key;
+
+ // Polymorphic base link is the first foreign key.
+ //
+ table& t (fk.table ());
+ table::names_iterator p (t.find (fk.name ()));
+
+ if (t.extra ()["kind"] == "polymorphic derived object" &&
+ (p == t.names_begin () || !(--p)->is_a<foreign_key> ()))
+ {
+ location const& l (t.get<location> ("cxx-location"));
+
+ if (name == string ("pointed-to class"))
+ {
+ error (l) << "changing polymorphic base is not " <<
+ "supported" << endl;
+ info (l) << "consider re-implementing this change by adding " <<
+ "a new derived class with the desired base, migrating the " <<
+ "data, and deleteing the old class" << endl;
+ }
+ else
+ {
+ error (l) << "changing polymorphic base " << name <<
+ " is not supported" << endl;
+ info (l) << "consider re-implementing this change by adding " <<
+ "a new derived class with the desired " << name << ", " <<
+ "migrating the data, and deleteing the old class" << endl;
+ }
+ }
+ else
+ {
+ location const& l (fk.get<location> ("cxx-location"));
+
+ error (l) << "changing object pointer " << name << " is not " <<
+ "supported" << endl;
+ info (l) << "consider re-implementing this change by adding " <<
+ "a new object pointer with the desired " << name << ", " <<
+ "migrating the data, and deleteing the old pointer" << endl;
+ }
+
+ throw operation_failed ();
+ }
+
+ void
+ diagnose_index (sema_rel::index& i,
+ char const* name,
+ string const& ov,
+ string const& nv)
+ {
+ table& t (i.table ());
+ location const& tl (t.get<location> ("cxx-location"));
+ location const& il (i.get<location> ("cxx-location"));
+
+ error (il) << "change to index " << name;
+
+ if (!ov.empty () || !nv.empty ())
+ cerr << " (old: '" << ov << "', new: '" << nv << "')";
+
+ cerr << " is not yet handled automatically" << endl;
+
+ info (il) << "corresponding index '" << i.name () << "' " <<
+ "originates here" << endl;
+ info (tl) << "corresponding table '" << t.name () << "' " <<
+ "originates here" << endl;
+ info (il) << "consider re-implementing this change by adding " <<
+ "a new index with the desired " << name << " and deleting the " <<
+ "old one" << endl;
+
+ throw operation_failed ();
+ }
+
+ protected:
+ table& other;
+ mode_type mode;
+ alter_table& at;
+ graph& g;
+ options const& ops;
+ model_version const* version;
+ };
+
+ struct diff_model: trav_rel::table
+ {
+ enum mode_type {mode_add, mode_drop};
+
+ diff_model (model& o,
+ mode_type m,
+ changeset& s,
+ graph& gr,
+ string const& in,
+ options const& op,
+ model_version const* v)
+ : other (o),
+ mode (m),
+ cs (s),
+ g (gr),
+ in_name (in),
+ ops (op),
+ version (v) {}
+
+ virtual void
+ traverse (sema_rel::table& t)
+ {
+ using sema_rel::table;
+
+ if (mode == mode_add)
+ {
+ if (table* ot = other.find<table> (t.name ()))
+ {
+ // See if there are any changes to the table.
+ //
+ alter_table& at (g.new_node<alter_table> (t.id ()));
+
+ // Set the alters edge for lookup.
+ //
+ table* bt (cs.lookup<table, drop_table> (t.name ()));
+ assert (bt != 0);
+ alters& ae (g.new_edge<alters> (at, *bt));
+
+ if (t.options () != ot->options ())
+ diagnose_table (t, "options", ot->options (), t.options ());
+
+ if (ot->extra ()["kind"] != t.extra ()["kind"])
+ diagnose_table (t, "kind",
+ ot->extra ()["kind"], t.extra ()["kind"]);
+
+ {
+ trav_rel::table table;
+ trav_rel::unames names;
+ diff_table dtable (
+ *ot, diff_table::mode_add, at, g, ops, version);
+ table >> names >> dtable;
+ table.traverse (t);
+ }
+
+ {
+ trav_rel::table table;
+ trav_rel::unames names;
+ diff_table dtable (
+ t, diff_table::mode_drop, at, g, ops, version);
+ table >> names >> dtable;
+ table.traverse (*ot);
+ }
+
+ if (!at.names_empty ())
+ g.new_edge<qnames> (cs, at, t.name ());
+ else
+ {
+ g.delete_edge (at, *bt, ae);
+ g.delete_node (at);
+ }
+ }
+ else
+ {
+ // Soft-add is only applicable to containers.
+ //
+ if (version != 0 && t.count ("member-path"))
+ {
+ data_member_path const& mp (
+ t.get<data_member_path> ("member-path"));
+
+ // Make sure the addition version is the current version.
+ //
+ semantics::data_member* m (context::added_member (mp));
+ if (m != 0)
+ {
+ if (context::added (*m) != version->current)
+ {
+ location l (m->get<location_t> ("added-location"));
+ error (l) << "member addition version is not the same " <<
+ "as the current model version" << endl;
+ throw operation_failed ();
+ }
+ }
+ // Warn about hard additions. If the version is closed, then
+ // we have already seen these warnings so no need to repeat
+ // them.
+ //
+ else if (ops.warn_hard_add () && version->open)
+ {
+ // Issue nicer diagnostics for the direct data member case.
+ //
+ if (mp.size () == 1)
+ {
+ location l (mp.back ()->location ());
+ warn (l) << "data member is hard-added" << endl;
+ }
+ else
+ {
+ semantics::class_& s (
+ dynamic_cast<semantics::class_&> (
+ mp.front ()->scope ()));
+
+ warn (s.location ()) << "container table '" <<
+ t.name () << "' in class '" << s.name () << "' is " <<
+ "hard-added" << endl;
+
+ for (data_member_path::const_iterator i (mp.begin ());
+ i != mp.end (); ++i)
+ {
+ info ((*i)->location ()) << "corresponding hard-" <<
+ "added data member could be '" << (*i)->name () <<
+ "'" << endl;
+ }
+ }
+ }
+ }
+
+ add_table& at (g.new_node<add_table> (t, cs, g));
+ g.new_edge<qnames> (cs, at, t.name ());
+ }
+ }
+ else
+ {
+ if (other.find<table> (t.name ()) == 0)
+ {
+ if (version != 0)
+ {
+ // See if we have an entry for this table in the soft-
+ // deleted map.
+ //
+ deleted_table_map const& dm (
+ other.get<deleted_table_map> ("deleted-map"));
+ deleted_table_map::const_iterator i (dm.find (t.name ()));
+
+ if (i != dm.end ())
+ {
+ // This table could be derived either from a class (object)
+ // or data member (container).
+ //
+ semantics::class_* c (
+ dynamic_cast<semantics::class_*> (i->second));
+ if (c != 0 && context::deleted (*c) != version->current)
+ {
+ location l (c->get<location_t> ("deleted-location"));
+ error (l) << "class deletion version is not the same " <<
+ "as the current model version" << endl;
+ throw operation_failed ();
+ }
+
+ semantics::data_member* m (
+ dynamic_cast<semantics::data_member*> (i->second));
+ if (m != 0 && context::deleted (*m) != version->current)
+ {
+ location l (m->get<location_t> ("deleted-location"));
+ error (l) << "member deletion version is not the same " <<
+ "as the current model version" << endl;
+ throw operation_failed ();
+ }
+ }
+ // Warn about hard deletions. If the version is closed, then
+ // we have already seen these warnings so no need to repeat
+ // them.
+ //
+ // At first, it may seem like a good idea not to warn about
+ // class deletions since it is not possible to do anything
+ // useful with a class without referencing it from the code
+ // (in C++ sense). However, things get tricky once we consider
+ // polymorphism since the migration code could be "working"
+ // with the hard-deleted derived class via its base. So we
+ // are going to warn about polymorphic derived tables.
+ //
+ else if (ops.warn_hard_delete () && version->open)
+ {
+ string k (t.extra ()["kind"]);
+
+ // We don't have any sensible location.
+ //
+ if (k == "container")
+ {
+ // We will issue a useless warning if the object table
+ // that this container references is also deleted.
+ // While we could detect this, it is going to require
+ // some effort since we would have to delay the warning
+ // and check later once we know all the deleted tables.
+ //
+ cerr << in_name << ": warning: container table '" <<
+ t.name () << "' is hard-deleted" << endl;
+ }
+ else if (k == "polymorphic derived object")
+ {
+ // The same as above: the warning will be useless if
+ // we are dropping the whole hierarchy.
+ //
+ cerr << in_name << ": warning: polymorphic derived " <<
+ "object table '" << t.name () << "' is hard-deleted" <<
+ endl;
+ }
+ }
+ }
+
+ drop_table& dt (g.new_node<drop_table> (t.id ()));
+ g.new_edge<qnames> (cs, dt, t.name ());
+ }
+ }
+ }
+
+ void
+ diagnose_table (sema_rel::table& t,
+ char const* name,
+ string const& ov,
+ string const& nv)
+ {
+ location const& tl (t.get<location> ("cxx-location"));
+
+ error (tl) << "change to object or container member results in "
+ "the change of the corresponding table " << name;
+
+ if (!ov.empty () || !nv.empty ())
+ cerr << " (old: '" << ov << "', new: '" << nv << "')";
+
+ cerr << endl;
+
+ error (tl) << "this change is not yet handled automatically" << endl;
+ info (tl) << "consider re-implementing this change by adding a " <<
+ "new object or container member with the desired " << name <<
+ ", migrating the data, and deleting the old object or member" <<
+ endl;
+
+ throw operation_failed ();
+ }
+
+ protected:
+ model& other;
+ mode_type mode;
+ changeset& cs;
+ graph& g;
+ string in_name;
+ options const& ops;
+ model_version const* version;
+ };
+
+ // Assumes the new model has cxx-location set. If version is not 0,
+ // then assume it is the current model version and the new model is
+ // the current model which has member paths and deleted maps set.
+ //
+ changeset&
+ diff (model& o,
+ model& n,
+ changelog& l,
+ string const& in_name,
+ options const& ops,
+ model_version const* version)
+ {
+ changeset& r (l.new_node<changeset> (n.version ()));
+
+ // Set the alters edge for lookup. If we are diff'ing two models of
+ // the same version, then use the old model as a base. Otherwise use
+ // the tip of changelog (it should correspond to the old model).
+ //
+ if (o.version () == n.version ())
+ l.new_edge<alters> (r, o);
+ else
+ {
+ if (l.contains_changeset_empty ())
+ {
+ model& m (l.model ());
+
+ // The changelog model version may also be equal to the new model
+ // version if the new base model version is greater than the
+ // latest changeset.
+ //
+ assert (m.version () == o.version () ||
+ m.version () == n.version ());
+
+ l.new_edge<alters> (r, m);
+ }
+ else
+ {
+ changeset& c (l.contains_changeset_back ().changeset ());
+ assert (o.version () == c.version ());
+ l.new_edge<alters> (r, c);
+ }
+ }
+
+ {
+ trav_rel::model model;
+ trav_rel::qnames names;
+ diff_model dmodel (
+ o, diff_model::mode_add, r, l, in_name, ops, version);
+ model >> names >> dmodel;
+ model.traverse (n);
+ }
+
+ {
+ trav_rel::model model;
+ trav_rel::qnames names;
+ diff_model dmodel (
+ n, diff_model::mode_drop, r, l, in_name, ops, version);
+ model >> names >> dmodel;
+ model.traverse (o);
+ }
+
+ return r;
+ }
+
+ //
+ // patch
+ //
+
+ struct patch_table: trav_rel::add_column,
+ trav_rel::drop_column,
+ trav_rel::alter_column,
+ trav_rel::add_index,
+ trav_rel::drop_index,
+ trav_rel::add_foreign_key,
+ trav_rel::drop_foreign_key
+ {
+ patch_table (table& tl, graph& gr): t (tl), g (gr) {}
+
+ virtual void
+ traverse (sema_rel::add_column& ac)
+ {
+ try
+ {
+ column& c (g.new_node<column> (ac, t, g));
+ g.new_edge<unames> (t, c, ac.name ());
+ }
+ catch (duplicate_name const&)
+ {
+ cerr << "error: invalid changelog: column '" << ac.name () <<
+ "' already exists in table '" << t.name () << "'" << endl;
+ throw operation_failed ();
+ }
+ }
+
+ virtual void
+ traverse (sema_rel::drop_column& dc)
+ {
+ table::names_iterator i (t.find (dc.name ()));
+
+ if (i == t.names_end () || !i->nameable ().is_a<column> ())
+ {
+ cerr << "error: invalid changelog: column '" << dc.name () <<
+ "' does not exist in table '" << t.name () << "'" << endl;
+ throw operation_failed ();
+ }
+
+ g.delete_edge (t, i->nameable (), *i);
+ }
+
+ virtual void
+ traverse (sema_rel::alter_column& ac)
+ {
+ if (column* c = t.find<column> (ac.name ()))
+ {
+ if (ac.null_altered ())
+ c->null (ac.null ());
+ }
+ else
+ {
+ cerr << "error: invalid changelog: column '" << ac.name () <<
+ "' does not exist in table '" << t.name () << "'" << endl;
+ throw operation_failed ();
+ }
+ }
+
+ virtual void
+ traverse (sema_rel::add_index& ai)
+ {
+ using sema_rel::index;
+
+ try
+ {
+ index& i (g.new_node<index> (ai, t, g));
+ g.new_edge<unames> (t, i, ai.name ());
+ }
+ catch (duplicate_name const&)
+ {
+ cerr << "error: invalid changelog: index '" << ai.name () <<
+ "' already exists in table '" << t.name () << "'" << endl;
+ throw operation_failed ();
+ }
+ }
+
+ virtual void
+ traverse (sema_rel::drop_index& di)
+ {
+ using sema_rel::index;
+ table::names_iterator i (t.find (di.name ()));
+
+ if (i == t.names_end () || !i->nameable ().is_a<index> ())
+ {
+ cerr << "error: invalid changelog: index '" << di.name () <<
+ "' does not exist in table '" << t.name () << "'" << endl;
+ throw operation_failed ();
+ }
+
+ g.delete_edge (t, i->nameable (), *i);
+ }
+
+ virtual void
+ traverse (sema_rel::add_foreign_key& afk)
+ {
+ using sema_rel::foreign_key;
+
+ try
+ {
+ foreign_key& fk (g.new_node<foreign_key> (afk, t, g));
+ g.new_edge<unames> (t, fk, afk.name ());
+ }
+ catch (duplicate_name const&)
+ {
+ cerr << "error: invalid changelog: foreign key '" << afk.name () <<
+ "' already exists in table '" << t.name () << "'" << endl;
+ throw operation_failed ();
+ }
+ }
+
+ virtual void
+ traverse (sema_rel::drop_foreign_key& dfk)
+ {
+ using sema_rel::foreign_key;
+ table::names_iterator i (t.find (dfk.name ()));
+
+ if (i == t.names_end () || !i->nameable ().is_a<foreign_key> ())
+ {
+ cerr << "error: invalid changelog: foreign key '" << dfk.name () <<
+ "' does not exist in table '" << t.name () << "'" << endl;
+ throw operation_failed ();
+ }
+
+ g.delete_edge (t, i->nameable (), *i);
+ }
+
+ protected:
+ table& t;
+ graph& g;
+ };
+
+ struct patch_model: trav_rel::add_table,
+ trav_rel::drop_table,
+ trav_rel::alter_table
+ {
+ patch_model (model& ml, graph& gr): m (ml), g (gr) {}
+
+ virtual void
+ traverse (sema_rel::add_table& at)
+ {
+ try
+ {
+ table& t (g.new_node<table> (at, m, g));
+ g.new_edge<qnames> (m, t, at.name ());
+ }
+ catch (duplicate_name const&)
+ {
+ cerr << "error: invalid changelog: table '" << at.name () <<
+ "' already exists in model version " << m.version () << endl;
+ throw operation_failed ();
+ }
+ }
+
+ virtual void
+ traverse (sema_rel::drop_table& dt)
+ {
+ model::names_iterator i (m.find (dt.name ()));
+
+ if (i == m.names_end () || !i->nameable ().is_a<table> ())
+ {
+ cerr << "error: invalid changelog: table '" << dt.name () <<
+ "' does not exist in model version " << m.version () << endl;
+ throw operation_failed ();
+ }
+
+ g.delete_edge (m, i->nameable (), *i);
+ }
+
+ virtual void
+ traverse (sema_rel::alter_table& at)
+ {
+ if (table* t = m.find<table> (at.name ()))
+ {
+ trav_rel::alter_table atable;
+ trav_rel::unames names;
+ patch_table ptable (*t, g);
+ atable >> names >> ptable;
+ atable.traverse (at);
+ }
+ else
+ {
+ cerr << "error: invalid changelog: table '" << at.name () <<
+ "' does not exist in model version " << m.version () << endl;
+ throw operation_failed ();
+ }
+ }
+
+ protected:
+ model& m;
+ graph& g;
+ };
+
+ model&
+ patch (model& m, changeset& c, graph& g)
+ {
+ model& r (g.new_node<model> (m, g));
+
+ trav_rel::changeset changeset;
+ trav_rel::qnames names;
+ patch_model pmodel (r, g);
+ changeset >> names >> pmodel;
+ changeset.traverse (c);
+
+ r.version (c.version ());
+ return r;
+ }
+ }
+
+ cutl::shared_ptr<changelog>
+ generate (model& m,
+ model_version const& mv,
+ changelog* old,
+ string const& in_name,
+ string const& out_name,
+ options const& ops)
+ {
+ database db (ops.database ()[0]);
+ cutl::shared_ptr<changelog> cl (
+ new (shared) changelog (db.string (), ops.schema_name ()[db]));
+ graph& g (*cl);
+
+ if (old == 0)
+ {
+ // Don't allow changelog initialization if the version is closed.
+ // This will prevent adding new files to an existing object model
+ // with a closed version.
+ //
+ if (!mv.open)
+ {
+ cerr << out_name << ": error: unable to initialize changelog " <<
+ "because current version is closed" << endl;
+ throw operation_failed ();
+ }
+
+ if (!ops.init_changelog ())
+ cerr << out_name << ": info: initializing changelog with base " <<
+ "version " << mv.base << endl;
+
+ if (mv.base == mv.current)
+ g.new_edge<contains_model> (*cl, g.new_node<model> (m, g));
+ else
+ {
+ // In this case we have to create an empty model at the base
+ // version and a changeset. We do it this way instead of putting
+ // everything into the base model in order to support adding new
+ // header files to the project.
+ //
+ cerr << out_name << ": warning: base and current versions " <<
+ "differ; assuming base model is empty" << endl;
+
+ model& nm (g.new_node<model> (mv.base));
+ g.new_edge<contains_model> (*cl, nm);
+ changeset& c (diff (nm, m, *cl, in_name, ops, &mv));
+
+ if (!c.names_empty ())
+ {
+ g.new_edge<alters_model> (c, nm);
+ g.new_edge<contains_changeset> (*cl, c);
+ }
+ }
+
+ return cl;
+ }
+
+ // Get the changelog base and current versions and do some sanity
+ // checks.
+ //
+ version bver (old->model ().version ());
+ version cver (
+ old->contains_changeset_empty ()
+ ? bver
+ : old->contains_changeset_back ().changeset ().version ());
+
+ if (mv.base < bver)
+ {
+ cerr << in_name << ": error: latest changelog base version is " <<
+ "greater than model base version" << endl;
+ throw operation_failed ();
+ }
+
+ if (mv.current < cver)
+ {
+ cerr << in_name << ": error: latest changelog current version is " <<
+ "greater than model current version" << endl;
+ throw operation_failed ();
+ }
+
+ // Build the new changelog.
+ //
+ model& oldm (old->model ());
+
+ // Now we have a case with a "real" old model (i.e., non-empty
+ // and with version older than current) as well as zero or more
+ // changeset.
+ //
+ //
+ model* last (&g.new_node<model> (oldm, g));
+ model* base (bver == mv.base && mv.base != mv.current ? last : 0);
+ if (base != 0)
+ g.new_edge<contains_model> (*cl, *base);
+
+ for (changelog::contains_changeset_iterator i (
+ old->contains_changeset_begin ());
+ i != old->contains_changeset_end (); ++i)
+ {
+ changeset& cs (i->changeset ());
+
+ // Don't copy the changeset for the current version. Instead, we
+ // will re-create it from scratch.
+ //
+ if (cs.version () == mv.current)
+ break;
+
+ model& prev (*last);
+ last = &patch (prev, cs, g);
+
+ if (base == 0)
+ {
+ if (last->version () == mv.base)
+ {
+ base = last;
+ g.new_edge<contains_model> (*cl, *base);
+ }
+ else if (last->version () > mv.base)
+ {
+ // We have a gap. Plug it with an empty base model. We will
+ // also need to create a new changeset for this step.
+ //
+ base = &g.new_node<model> (mv.base);
+ g.new_edge<contains_model> (*cl, *base);
+
+ changeset& c (diff (*base, *last, *cl, in_name, ops, 0));
+ if (!c.names_empty ())
+ {
+ g.new_edge<alters_model> (c, *base);
+ g.new_edge<contains_changeset> (*cl, c);
+ }
+
+ continue;
+ }
+ }
+
+ // Copy the changeset unless it is below or at our base version.
+ //
+ if (last->version () <= mv.base)
+ continue;
+
+ changeset& c (
+ g.new_node<changeset> (
+ cs,
+ cl->contains_changeset_empty ()
+ ? static_cast<qscope&> (*base) // Cannot be NULL.
+ : cl->contains_changeset_back ().changeset (),
+ g));
+
+ g.new_edge<alters_model> (c, prev);
+ g.new_edge<contains_changeset> (*cl, c);
+ }
+
+ // If we still haven't found the new base model, then it means it
+ // has version greater than any changeset we have seen.
+ //
+ if (base == 0)
+ {
+ if (mv.base == mv.current)
+ base = &g.new_node<model> (m, g);
+ else
+ {
+ // Fast-forward the latest model to the new base.
+ //
+ base = last;
+ base->version (mv.base);
+ }
+
+ g.new_edge<contains_model> (*cl, *base);
+ }
+
+ // If the current version is closed, make sure the model hasn't
+ // changed.
+ //
+ if (!mv.open)
+ {
+ // If the last changeset has the current version, then apply it.
+ //
+ model* om (last);
+ if (!old->contains_changeset_empty ())
+ {
+ changeset& c (old->contains_changeset_back ().changeset ());
+ if (c.version () == mv.current)
+ om = &patch (*last, c, g);
+ }
+
+ changeset& c (diff (*om, m, *cl, in_name, ops, &mv));
+
+ if (!c.names_empty ())
+ {
+ qnames& n (*c.names_begin ());
+
+ cerr << out_name << ": error: current version is closed" << endl;
+ cerr << out_name << ": info: first new change is " <<
+ n.nameable ().kind () << " '" << n.name () << "'" << endl;
+
+ throw operation_failed ();
+ }
+ }
+
+ // Add a changeset for the current version unless it is the same
+ // as the base version.
+ //
+ if (mv.base != mv.current)
+ {
+ // Add it even if it is empty. This can be useful, for example,
+ // for data-only migrations were the user relies on the database
+ // version being updated in the version table.
+ //
+ changeset& c (diff (*last, m, *cl, in_name, ops, &mv));
+ g.new_edge<alters_model> (c, *last);
+ g.new_edge<contains_changeset> (*cl, c);
+ }
+
+ return cl;
+ }
+ }
+}