// file      : odb/relational/changelog.cxx
// copyright : Copyright (c) 2009-2015 Code Synthesis Tools CC
// 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 ());
            assert (o.version () == m.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;
    }
  }
}
