From c67c06030fb1ac622c96211bffc054a85efe0aa8 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 20 Mar 2013 13:09:45 +0200 Subject: Add support for maintaining log of database model changes --- odb/generator.cxx | 144 ++++++++++++++--- odb/makefile | 1 + odb/relational/changelog.cxx | 288 +++++++++++++++++++++++++++++++++ odb/relational/generate.hxx | 14 ++ odb/semantics/relational/changelog.cxx | 1 + odb/semantics/relational/elements.cxx | 18 +++ odb/semantics/relational/elements.hxx | 52 ++++-- odb/semantics/relational/elements.txx | 12 ++ odb/semantics/relational/model.hxx | 3 + odb/semantics/relational/table.cxx | 35 +++- odb/semantics/relational/table.hxx | 18 +++ odb/traversal/relational/table.hxx | 1 + 12 files changed, 555 insertions(+), 32 deletions(-) create mode 100644 odb/relational/changelog.cxx diff --git a/odb/generator.cxx b/odb/generator.cxx index 83f42b8..a2254e0 100644 --- a/odb/generator.cxx +++ b/odb/generator.cxx @@ -6,6 +6,7 @@ #include #include // std::auto_ptr #include +#include #include #include @@ -14,10 +15,16 @@ #include #include +#include +#include + #include #include #include +#include +#include + #include #include @@ -85,18 +92,15 @@ generate (options const& ops, // First create the database model. // + bool gen_schema (ops.generate_schema () && db != database::common); cutl::shared_ptr model; - if (ops.generate_schema ()) + if (gen_schema) { auto_ptr ctx (create_context (cerr, unit, ops, fts, 0)); switch (db) { - case database::common: - { - break; // No schema for common. - } case database::mssql: case database::mysql: case database::oracle: @@ -106,29 +110,34 @@ generate (options const& ops, model = relational::model::generate (); break; } + case database::common: + break; } } // Output files. // + fs::auto_removes auto_rm; + path file (ops.output_name ().empty () ? p.leaf () : path (ops.output_name ()).leaf ()); string base (file.base ().string ()); - fs::auto_removes auto_rm; - string hxx_name (base + ops.odb_file_suffix ()[db] + ops.hxx_suffix ()); string ixx_name (base + ops.odb_file_suffix ()[db] + ops.ixx_suffix ()); string cxx_name (base + ops.odb_file_suffix ()[db] + ops.cxx_suffix ()); string sch_name (base + ops.schema_file_suffix ()[db] + ops.cxx_suffix ()); string sql_name (base + ops.sql_file_suffix ()[db] + ops.sql_suffix ()); + string log_name (base + ops.changelog_file_suffix ()[db] + + ops.changelog_suffix ()); path hxx_path (hxx_name); path ixx_path (ixx_name); path cxx_path (cxx_name); path sch_path (sch_name); path sql_path (sql_name); + path log_path (p.directory () / path (log_name)); // Input directory. if (!ops.output_dir ().empty ()) { @@ -140,12 +149,72 @@ generate (options const& ops, sql_path = dir / sql_path; } - bool gen_cxx (!ops.generate_schema_only ()); + // Load the old changelog and generate a new one. + // + bool gen_log (gen_schema && unit.count ("model-version") != 0); + cutl::shared_ptr changelog; + cutl::shared_ptr old_changelog; + string old_changelog_xml; + + if (gen_log) + { + ifstream log (log_path.string ().c_str (), + ios_base::in | ios_base::binary); + + if (log.is_open ()) + { + try + { + // Get the XML into a buffer. We use it to avoid modifying the + // file when the changelog hasn't changed. + // + for (bool first (true); !log.eof (); ) + { + string line; + getline (log, line); + + if (log.fail ()) + ios_base::failure ("getline"); + + if (first) + first = false; + else + old_changelog_xml += '\n'; + + old_changelog_xml += line; + } + + istringstream is (old_changelog_xml); + is.exceptions (ios_base::badbit | ios_base::failbit); + + xml::parser p (is, log_path.string ()); + old_changelog.reset ( + new (shared) semantics::relational::changelog (p)); + } + catch (const ios_base::failure& e) + { + cerr << log_path << ": read failure" << endl; + throw failed (); + } + catch (const xml::parsing& e) + { + cerr << e.what () << endl; + throw failed (); + } + } + + changelog = relational::changelog::generate ( + *model, + unit.get ("model-version"), + old_changelog.get (), + log_path.string ()); + } // // - ofstream hxx; + bool gen_cxx (!ops.generate_schema_only ()); + ofstream hxx; if (gen_cxx) { hxx.open (hxx_path.string ().c_str (), ios_base::out); @@ -163,7 +232,6 @@ generate (options const& ops, // // ofstream ixx; - if (gen_cxx) { ixx.open (ixx_path.string ().c_str (), ios_base::out); @@ -181,7 +249,6 @@ generate (options const& ops, // // ofstream cxx; - if (gen_cxx && (db != database::common || md == multi_database::dynamic)) { cxx.open (cxx_path.string ().c_str (), ios_base::out); @@ -198,11 +265,9 @@ generate (options const& ops, // // - bool gen_sql_schema (ops.generate_schema () && - ops.schema_format ()[db].count (schema_format::sql) && - db != database::common); + bool gen_sql_schema (gen_schema && + ops.schema_format ()[db].count (schema_format::sql)); ofstream sql; - if (gen_sql_schema) { sql.open (sql_path.string ().c_str (), ios_base::out); @@ -221,12 +286,10 @@ generate (options const& ops, // bool gen_sep_schema ( gen_cxx && - ops.generate_schema () && - ops.schema_format ()[db].count (schema_format::separate) && - db != database::common); + gen_schema && + ops.schema_format ()[db].count (schema_format::separate)); ofstream sch; - if (gen_sep_schema) { sch.open (sch_path.string ().c_str (), ios_base::out); @@ -770,6 +833,49 @@ generate (options const& ops, } } + // Save the changelog if it changed. + // + if (gen_log) + { + try + { + ostringstream os; + os.exceptions (ifstream::badbit | ifstream::failbit); + xml::serializer s (os, log_path.string ()); + changelog->serialize (s); + string const& changelog_xml (os.str ()); + + if (changelog_xml != old_changelog_xml) + { + ofstream log (log_path.string ().c_str (), + ios_base::out | ios_base::binary); + + if (!log.is_open ()) + { + cerr << "error: unable to open '" << log_path << "' in write mode" + << endl; + throw failed (); + } + + if (old_changelog == 0) + auto_rm.add (log_path); + + log.exceptions (ifstream::badbit | ifstream::failbit); + log << changelog_xml; + } + } + catch (const ios_base::failure& e) + { + cerr << log_path << ": write failure" << endl; + throw failed (); + } + catch (const xml::serialization& e) + { + cerr << e.what () << endl; + throw failed (); + } + } + // Communicate the sloc count to the driver. This is necessary to // correctly handle the total if we are compiling multiple files in // one invocation. diff --git a/odb/makefile b/odb/makefile index 5edd0ea..104bc4a 100644 --- a/odb/makefile +++ b/odb/makefile @@ -32,6 +32,7 @@ pragma.cxx # Relational. # cxx_ptun += \ +relational/changelog.cxx \ relational/common.cxx \ relational/common-query.cxx \ relational/context.cxx \ diff --git a/odb/relational/changelog.cxx b/odb/relational/changelog.cxx new file mode 100644 index 0000000..de5e6f7 --- /dev/null +++ b/odb/relational/changelog.cxx @@ -0,0 +1,288 @@ +// file : odb/relational/changelog.cxx +// copyright : Copyright (c) 2009-2013 Code Synthesis Tools CC +// license : GNU GPL v3; see accompanying LICENSE file + +#include + +#include +#include + +#include +#include + +using namespace std; + +namespace relational +{ + namespace changelog + { + using namespace sema_rel; + using sema_rel::model; + using sema_rel::changelog; + + namespace + { + struct diff_model: trav_rel::table + { + enum mode_type {mode_add, mode_drop}; + + diff_model (model& o, mode_type m, changeset& s, graph& gr) + : other (o), mode (m), cs (s), g (gr) {} + + virtual void + traverse (sema_rel::table& t) + { + if (mode == mode_add) + { + if (other.find (t.name ()) == 0) + { + add_table& at (g.new_node (t, cs, g)); + g.new_edge (cs, at, t.name ()); + } + } + else + { + if (other.find (t.name ()) == 0) + { + drop_table& dt (g.new_node (t.id ())); + g.new_edge (cs, dt, t.name ()); + } + } + } + + protected: + model& other; + mode_type mode; + changeset& cs; + graph& g; + }; + + changeset& + diff (model& o, model& n, graph& g) + { + changeset& r (g.new_node (n.version ())); + + { + trav_rel::model model; + trav_rel::qnames names; + diff_model dmodel (o, diff_model::mode_add, r, g); + model >> names >> dmodel; + model.traverse (n); + } + + { + trav_rel::model model; + trav_rel::qnames names; + diff_model dmodel (n, diff_model::mode_drop, r, g); + model >> names >> dmodel; + model.traverse (o); + } + + return r; + } + + struct patch_model: trav_rel::add_table, + trav_rel::drop_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 (at, m, g)); + g.new_edge (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
()) + { + 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); + } + + protected: + model& m; + graph& g; + }; + + model& + patch (model& m, changeset& c, graph& g) + { + model& r (g.new_node (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 + generate (model& m, + model_version const& mv, + changelog* old, + string const& name) + { + cutl::shared_ptr cl (new (shared) changelog); + graph& g (*cl); + + if (old == 0) + { + if (!mv.open) + { + cerr << name << ": error: unable to initialize changelog because " << + "current version is closed" << endl; + throw operation_failed (); + } + + cerr << name << ": info: initializing changelog with base version " << + m.version () << endl; + + g.new_edge (*cl, g.new_node (m, g)); + 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.current < cver) + { + cerr << name << ": error: latest changelog version is greater " << + "than current version" << endl; + throw operation_failed (); + } + + // Build the new changelog. + // + model& oldm (old->model ()); + + // Handle the cases where we just override the log with the current + // model. + // + if (mv.base == mv.current || bver == mv.current || oldm.names_empty ()) + { + // If the current version is closed, make sure the model hasn't + // changed. + // + if (!mv.open) + { + changeset& cs (diff (oldm, m, g)); + + if (!cs.names_empty ()) + { + qnames& n (*cs.names_begin ()); + + cerr << name << ": error: current version is closed" << endl; + cerr << name << ": info: first new change is " << + n.nameable ().kind () << " '" << n.name () << "'" << endl; + + throw operation_failed (); + } + } + + g.new_edge (*cl, g.new_node (m, g)); + return cl; + } + + // 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* base (bver >= mv.base ? &g.new_node (oldm, g) : 0); + model* last (&oldm); + + 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) + { + // If the current version is closed, make sure the model hasn't + // changed. + // + if (!mv.open) + { + model& old (patch (*last, cs, g)); + changeset& cs (diff (old, m, g)); + + if (!cs.names_empty ()) + { + qnames& n (*cs.names_begin ()); + + cerr << name << ": error: current version is closed" << endl; + cerr << name << ": info: first new change is " << + n.nameable ().kind () << " '" << n.name () << "'" << endl; + + throw operation_failed (); + } + } + break; + } + + last = &patch (*last, cs, g); + + if (base == 0 && last->version () >= mv.base) + base = last; + + // Copy the changeset unless it is below or at our base version. + // + if (last->version () <= mv.base) + continue; + + g.new_edge (*cl, g.new_node (cs, g)); + } + + // If we still haven't found the new base model, then take the + // latest and update its version. + // + if (base == 0) + { + base = last != &oldm ? last : &g.new_node (oldm, g); + base->version (mv.base); + } + g.new_edge (*cl, *base); + + // Add a changeset for the current version. + // + changeset& cs (diff (*last, m, g)); + + if (!cs.names_empty ()) + g.new_edge (*cl, cs); + + return cl; + } + } +} diff --git a/odb/relational/generate.hxx b/odb/relational/generate.hxx index 401c93c..e5dc376 100644 --- a/odb/relational/generate.hxx +++ b/odb/relational/generate.hxx @@ -5,9 +5,12 @@ #ifndef ODB_RELATIONAL_GENERATE_HXX #define ODB_RELATIONAL_GENERATE_HXX +#include #include +#include #include +#include namespace relational { @@ -41,6 +44,17 @@ namespace relational generate (); } + namespace changelog + { + // Returns NULL if the changelog is unchanged. + // + cutl::shared_ptr + generate (semantics::relational::model&, + model_version const&, + semantics::relational::changelog* old, // Can be NULL. + std::string const& name); + } + namespace schema { void diff --git a/odb/semantics/relational/changelog.cxx b/odb/semantics/relational/changelog.cxx index 05dd529..cd94494 100644 --- a/odb/semantics/relational/changelog.cxx +++ b/odb/semantics/relational/changelog.cxx @@ -73,6 +73,7 @@ namespace semantics i != contains_changeset_.rend (); ++i) { (*i)->changeset ().serialize (s); + s.characters ("\n"); } model ().serialize (s); diff --git a/odb/semantics/relational/elements.cxx b/odb/semantics/relational/elements.cxx index 43c03c0..e14bc00 100644 --- a/odb/semantics/relational/elements.cxx +++ b/odb/semantics/relational/elements.cxx @@ -70,6 +70,24 @@ namespace semantics throw duplicate_name (*this, (*i->second)->nameable (), n); } + template <> + void scope:: + remove_edge_left (names_type& e) + { + typename names_iterator_map::iterator i (iterator_map_.find (&e)); + assert (i != iterator_map_.end ()); + + // If we are removing the first key, then move to the next key (or + // the end which means there are no keys). + // + if (first_key_ == i->second) + first_key_++; + + names_.erase (i->second); + names_map_.erase (e.name ()); + iterator_map_.erase (i); + } + // type info // namespace diff --git a/odb/semantics/relational/elements.hxx b/odb/semantics/relational/elements.hxx index 2321e1a..381d7a8 100644 --- a/odb/semantics/relational/elements.hxx +++ b/odb/semantics/relational/elements.hxx @@ -149,6 +149,20 @@ namespace semantics nameable_ = &n; } + void + clear_left_node (scope_type& n) + { + assert (scope_ == &n); + scope_ = 0; + } + + void + clear_right_node (nameable_type& n) + { + assert (nameable_ == &n); + nameable_ = 0; + } + protected: name_type name_; scope_type* scope_; @@ -169,22 +183,16 @@ namespace semantics typedef relational::scope scope_type; name_type const& - name () const - { - return named_->name (); - } + name () const {return named_->name ();} scope_type& - scope () const - { - return named ().scope (); - } + scope () const {return named ().scope ();} names_type& - named () const - { - return *named_; - } + named () const {return *named_;} + + string const& + id () const {return id_;} public: // Id identifies the C++ node (e.g., a class or a data member) that @@ -208,6 +216,13 @@ namespace semantics named_ = &e; } + virtual void + remove_edge_right (names_type& e) + { + assert (named_ == &e); + named_ = 0; + } + using node::add_edge_right; protected: @@ -298,6 +313,12 @@ namespace semantics return names_.end (); } + bool + names_empty () const + { + return names_.empty (); + } + // Find. // template @@ -324,6 +345,9 @@ namespace semantics virtual void add_edge_left (names_type&); + virtual void + remove_edge_left (names_type&); + protected: scope (scope const&, graph&); scope (xml::parser&, graph&); @@ -343,6 +367,10 @@ namespace semantics void scope:: add_edge_left (names_type&); + template <> + void scope:: + remove_edge_left (names_type&); + typedef scope uscope; typedef scope qscope; } diff --git a/odb/semantics/relational/elements.txx b/odb/semantics/relational/elements.txx index 53946a0..e1fd6d5 100644 --- a/odb/semantics/relational/elements.txx +++ b/odb/semantics/relational/elements.txx @@ -169,5 +169,17 @@ namespace semantics else throw duplicate_name (*this, (*i->second)->nameable (), e.nameable ()); } + + template + void scope:: + remove_edge_left (names_type& e) + { + typename names_iterator_map::iterator i (iterator_map_.find (&e)); + assert (i != iterator_map_.end ()); + + names_.erase (i->second); + names_map_.erase (e.name ()); + iterator_map_.erase (i); + } } } diff --git a/odb/semantics/relational/model.hxx b/odb/semantics/relational/model.hxx index 626befd..575100f 100644 --- a/odb/semantics/relational/model.hxx +++ b/odb/semantics/relational/model.hxx @@ -19,6 +19,9 @@ namespace semantics version_type version () const {return version_;} + void + version (version_type v) {version_ = v;} + public: model (version_type v): version_ (v) {} model (model const&, graph&); diff --git a/odb/semantics/relational/table.cxx b/odb/semantics/relational/table.cxx index 1c6e784..6d1c3a2 100644 --- a/odb/semantics/relational/table.cxx +++ b/odb/semantics/relational/table.cxx @@ -13,7 +13,7 @@ namespace semantics // table // table:: - table (table const& t, qscope& s, graph& g) + table (table const& t, qscope&, graph& g) : qnameable (t, g), uscope (t, g) { } @@ -56,6 +56,29 @@ namespace semantics s.end_element (); } + // drop_table + // + drop_table:: + drop_table (xml::parser& p, qscope&, graph& g) + : qnameable (p, g) + { + p.content (xml::parser::empty); + } + + drop_table& drop_table:: + clone (qscope& s, graph& g) const + { + return g.new_node (*this, s, g); + } + + void drop_table:: + serialize (xml::serializer& s) const + { + s.start_element (xmlns, "drop-table"); + qnameable::serialize_attributes (s); + s.end_element (); + } + // type info // namespace @@ -67,6 +90,8 @@ namespace semantics qnameable::parser_map_["table"] = &qnameable::parser_impl
; qnameable::parser_map_["add-table"] = &qnameable::parser_impl; + qnameable::parser_map_["drop-table"] = + &qnameable::parser_impl; using compiler::type_info; @@ -86,6 +111,14 @@ namespace semantics ti.add_base (typeid (table)); insert (ti); } + + // drop_table + // + { + type_info ti (typeid (drop_table)); + ti.add_base (typeid (qnameable)); + insert (ti); + } } } init_; } diff --git a/odb/semantics/relational/table.hxx b/odb/semantics/relational/table.hxx index 768d3d7..3eb1b73 100644 --- a/odb/semantics/relational/table.hxx +++ b/odb/semantics/relational/table.hxx @@ -48,6 +48,24 @@ namespace semantics virtual void serialize (xml::serializer&) const; }; + + class drop_table: public qnameable + { + public: + drop_table (string const& id): qnameable (id) {} + drop_table (drop_table const& t, qscope&, graph& g): qnameable (t, g) {} + drop_table (xml::parser&, qscope&, graph&); + + virtual drop_table& + clone (qscope&, graph&) const; + + virtual string + kind () const {return "drop table";} + + virtual void + serialize (xml::serializer&) const; + }; + } } diff --git a/odb/traversal/relational/table.hxx b/odb/traversal/relational/table.hxx index b438f16..e700e43 100644 --- a/odb/traversal/relational/table.hxx +++ b/odb/traversal/relational/table.hxx @@ -14,6 +14,7 @@ namespace traversal { struct table: scope_template {}; struct add_table: scope_template {}; + struct drop_table: node {}; } } -- cgit v1.1