From f5e457f5dee11cbd20fc3557f79d6e1f235fb89f Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Fri, 6 Feb 2015 08:57:30 +0200 Subject: Implement join types support in views --- NEWS | 5 +++ doc/manual.xhtml | 18 +++++++--- odb/context.hxx | 2 ++ odb/pragma.cxx | 68 +++++++++++++++++++++++++++++++++++-- odb/relational/mysql/source.cxx | 13 ++++++++ odb/relational/processor.cxx | 15 +++++++-- odb/relational/source.cxx | 72 ++++++++++++++++++++++++++++++++++------ odb/relational/source.hxx | 17 ++++++++++ odb/relational/sqlite/source.cxx | 19 +++++++++++ 9 files changed, 210 insertions(+), 19 deletions(-) diff --git a/NEWS b/NEWS index f003c4b..896c161 100644 --- a/NEWS +++ b/NEWS @@ -27,6 +27,11 @@ Version 2.4.0 Section 15.2, "Persistent Class Template Instantiations" in the ODB manual. + * Support for object and table join types in views. Supported join type + are left, right, full, inner, and cross with left being the default. + For details, refer to Sections 10.1, "Object Views" and 10.3, "Table + Views" in the ODB manual. + * Support for result modifiers in view query conditions. Currently supported result modifiers are 'distinct' (which is translated to SELECT DISTINCT) and 'for_update' (which is translated to FOR UPDATE or diff --git a/doc/manual.xhtml b/doc/manual.xhtml index 3e941dc..fce5302 100644 --- a/doc/manual.xhtml +++ b/doc/manual.xhtml @@ -9953,6 +9953,7 @@ struct employee_employer

object(name [= alias] + [join-type] [: join-condition])

The name part is a potentially qualified persistent class @@ -9960,12 +9961,16 @@ struct employee_employer part gives this object an alias. If provided, the alias is used in several contexts instead of the object's unqualified name. We will discuss aliases further as we cover each of these contexts - below. The optional join-condition part provides the + below. The optional join-type part specifies the way this + object is associated. It can be left, right, + full, inner, and cross + with left being the default. + Finally, the optional join-condition part provides the criteria which should be used to associate this object with any of the previously associated objects or, as we will see in Section 10.4, "Mixed Views", tables. Note that while the first associated object can have an alias, it cannot - have a join condition.

+ have a join type or condition.

For each subsequent associated object the ODB compiler needs a join condition and there are several ways to specify @@ -10784,6 +10789,7 @@ struct employee_max_vacation

table("name" [= "alias"] + [join-type] [: join-condition])

The name part is a database table name. The optional @@ -10791,12 +10797,16 @@ struct employee_max_vacation alias must be used instead of the table whenever a reference to a table is used. Contexts where such a reference may be needed include the join condition (discussed below), - column names, and query expressions. The optional join-condition + column names, and query expressions. The optional join-type + part specifies the way this table is associated. It can + be left, right, full, + inner, and cross with left + being the default. Finally, the optional join-condition part provides the criteria which should be used to associate this table with any of the previously associated tables or, as we will see in Section 10.4, "Mixed Views", objects. Note that while the first associated table can have an alias, it cannot have - a join condition.

+ a join type or condition.

Similar to object views, for each subsequent associated table the ODB compiler needs a join condition. However, unlike for object views, diff --git a/odb/context.hxx b/odb/context.hxx index d6e4713..c408063 100644 --- a/odb/context.hxx +++ b/odb/context.hxx @@ -159,8 +159,10 @@ struct view_object name () const; enum kind_type { object, table }; + enum join_type { left, right, full, inner, cross }; kind_type kind; + join_type join; tree obj_node; // Tree node if kind is object. std::string obj_name; // Name as specified in the pragma if kind is object. qname tbl_name; // Table name if kind is table. diff --git a/odb/pragma.cxx b/odb/pragma.cxx index 618df67..08dc7ae 100644 --- a/odb/pragma.cxx +++ b/odb/pragma.cxx @@ -1131,7 +1131,7 @@ handle_pragma (cxx_lexer& l, else if (p == "table") { // table () - // table ( [= ""] [: ""] (view only) + // table ( [= ""] [type] [: ""] (view only) // // := "name" | "name.name" | "name"."name" // @@ -1183,9 +1183,41 @@ handle_pragma (cxx_lexer& l, tt = l.next (tl, &tn); } + if (tt == CPP_NAME) + { + // We have a JOIN type. + // + if (tl == "left") + vo.join = view_object::left; + else if (tl == "right") + vo.join = view_object::right; + else if (tl == "full") + vo.join = view_object::full; + else if (tl == "inner") + vo.join = view_object::inner; + else if (tl == "cross") + vo.join = view_object::cross; + else + { + error (l) << "unknown join type '" << tl << "'" << endl; + return; + } + + tt = l.next (tl, &tn); + } + else + vo.join = view_object::left; + if (tt == CPP_COLON) { // We have a condition. + // + if (vo.join == view_object::cross) + { + error (l) + << "no join condition can be specified for a cross join" << endl; + return; + } tt = l.next (tl, &tn); @@ -1653,7 +1685,7 @@ handle_pragma (cxx_lexer& l, } else if (p == "object") { - // object (fq-name [ = name] [: expr]) + // object (fq-name [ = name] [type] [: expr]) // // Make sure we've got the correct declaration type. @@ -1706,9 +1738,41 @@ handle_pragma (cxx_lexer& l, tt = l.next (tl, &tn); } + if (tt == CPP_NAME) + { + // We have a JOIN type. + // + if (tl == "left") + vo.join = view_object::left; + else if (tl == "right") + vo.join = view_object::right; + else if (tl == "full") + vo.join = view_object::full; + else if (tl == "inner") + vo.join = view_object::inner; + else if (tl == "cross") + vo.join = view_object::cross; + else + { + error (l) << "unknown join type '" << tl << "'" << endl; + return; + } + + tt = l.next (tl, &tn); + } + else + vo.join = view_object::left; + if (tt == CPP_COLON) { // We have a condition. + // + if (vo.join == view_object::cross) + { + error (l) + << "no join condition can be specified for a cross join" << endl; + return; + } tt = l.next (tl, &tn); diff --git a/odb/relational/mysql/source.cxx b/odb/relational/mysql/source.cxx index bcc7f23..9725f18 100644 --- a/odb/relational/mysql/source.cxx +++ b/odb/relational/mysql/source.cxx @@ -681,6 +681,19 @@ namespace relational { os << im << "value = 0;"; } + + virtual string + join_syntax (view_object const& vo) + { + if (vo.join == view_object::full) + { + error (vo.loc) + << "FULL OUTER JOIN is not supported by MySQL" << endl; + throw operation_failed (); + } + + return base::join_syntax (vo); + } }; entry class_entry_; diff --git a/odb/relational/processor.cxx b/odb/relational/processor.cxx index 16904b6..9442850 100644 --- a/odb/relational/processor.cxx +++ b/odb/relational/processor.cxx @@ -1344,6 +1344,15 @@ namespace relational for (view_objects::iterator i (objs.begin ()); i != objs.end (); ++i) { + if (i == objs.begin () && i->join != view_object::left) + { + error (i->loc) + << "no join type can be specified for the first associated " + << (i->kind == view_object::object ? "object" : "table") + << endl; + throw operation_failed (); + } + if (i->kind != view_object::object) { // Make sure we have join conditions for tables unless it @@ -1362,11 +1371,13 @@ namespace relational // If we have to generate the query and there was no JOIN // condition specified by the user, try to come up with one - // automatically based on object relationships. + // automatically based on object relationships. CROSS JOIN + // has no condition. // if (vq.kind == view_query::condition && i->cond.empty () && - i != objs.begin ()) + i != objs.begin () && + i->join != view_object::cross) { relationships rs; diff --git a/odb/relational/source.cxx b/odb/relational/source.cxx index dc083bb..e0fbab3 100644 --- a/odb/relational/source.cxx +++ b/odb/relational/source.cxx @@ -4776,12 +4776,19 @@ traverse_view (type& c) continue; } - l = "LEFT JOIN "; + l = join_syntax (*i); + l += ' '; l += quote_id (i->tbl_name); if (!i->alias.empty ()) l += (need_alias_as ? " AS " : " ") + quote_id (i->alias); + if (i->join == view_object::cross) // No ON condition for CROSS JOIN. + { + from.push_back (l); + continue; + } + semantics::scope& scope ( dynamic_cast (*unit.find (i->scope))); @@ -4851,20 +4858,28 @@ traverse_view (type& c) semantics::scope& scope ( dynamic_cast (*unit.find (i->scope))); - expression e ( - translate_expression ( - c, i->cond, scope, i->loc, "object")); + expression e (i->join == view_object::cross + ? expression ("") // Dummy literal expression. + : translate_expression ( + c, i->cond, scope, i->loc, "object")); // Literal expression. // if (e.kind == expression::literal) { - l = "LEFT JOIN "; + l = join_syntax (*i); + l += ' '; l += table_qname (o); if (!alias.empty ()) l += (need_alias_as ? " AS " : " ") + quote_id (alias); + if (i->join == view_object::cross) // No ON condition for CROSS JOIN. + { + from.push_back (l); + continue; + } + l += " ON"; // Add the pragma location for easier error tracking. @@ -4891,6 +4906,9 @@ traverse_view (type& c) // is a to-many relationship, then we first need to JOIN the // container table. This code is similar to object_joins. // + // Note that this cannot be CROSS JOIN; we've handled that case + // above. + // using semantics::data_member; data_member& m (*e.member_path.back ()); @@ -5029,7 +5047,8 @@ traverse_view (type& c) string ct; // Container table. if (cont != 0) { - l = "LEFT JOIN "; + l = join_syntax (*i); + l += ' '; // The same relationship can be used by multiple associated // objects. So if the object that contains this container has @@ -5174,7 +5193,15 @@ traverse_view (type& c) } } - l = "LEFT JOIN "; + // If we have already joined the container with the desired + // join type, then use LEFT JOIN to join the object to the + // container. This is the right thing to do since an entry + // in the container can only point (either via id or value) + // to a single object. + // + l = (cont == 0 ? join_syntax (*i) : "LEFT JOIN"); + l += ' '; + l += table_qname (o); if (!alias.empty ()) @@ -5563,13 +5590,36 @@ traverse_view (type& c) { if (i->compare (0, 5, "FROM ") == 0) os << "r += " << strlit (*i) << ";"; - else if (i->compare (0, 5, "LEFT ") == 0) - os << endl - << "r += " << strlit (sep + *i) << ";"; else if (i->compare (0, 3, "// ") == 0) os << *i << endl; else - os << "r += " << *i << ";"; // Already a string literal. + { + // See if this is a JOIN. The exact spelling is database-dependent, + // but we know there is the JOIN word in there somewhere and before + // it we should only have keywords and spaces. + // + size_t p (i->find ("JOIN ")); + if (p != string::npos) + { + // Make sure before it we only have A-Z and spaces. + // + for (char c; p != 0; --p) + { + c = (*i)[p - 1]; + if ((c < 'A' || c > 'Z') && c != ' ') + break; + } + + if (p == 0) + os << endl + << "r += " << strlit (sep + *i) << ";"; + } + + // Something else, assume already a string literal. + // + if (p != 0) + os << "r += " << *i << ";"; + } } // Generate the query condition. diff --git a/odb/relational/source.hxx b/odb/relational/source.hxx index 48d0e1d..bf675f2 100644 --- a/odb/relational/source.hxx +++ b/odb/relational/source.hxx @@ -6686,6 +6686,23 @@ namespace relational return c.get ("query").for_update ? "FOR UPDATE" : ""; } + virtual string + join_syntax (view_object const& vo) + { + const char* r; + + switch (vo.join) + { + case view_object::left: r = "LEFT JOIN"; break; + case view_object::right: r = "RIGHT JOIN"; break; + case view_object::full: r = "FULL JOIN"; break; + case view_object::inner: r = "INNER JOIN"; break; + case view_object::cross: r = "CROSS JOIN"; break; + } + + return r; + } + virtual void traverse_view (type& c); diff --git a/odb/relational/sqlite/source.cxx b/odb/relational/sqlite/source.cxx index 0824eb2..0e842c1 100644 --- a/odb/relational/sqlite/source.cxx +++ b/odb/relational/sqlite/source.cxx @@ -255,6 +255,25 @@ namespace relational // return ""; } + + virtual string + join_syntax (view_object const& vo) + { + const char* n (0); + + if (vo.join == view_object::full) + n = "FULL OUTER JOIN"; + else if (vo.join == view_object::right) + n = "RIGHT OUTER JOIN"; + + if (n != 0) + { + error (vo.loc) << n << " is not supported by SQLite" << endl; + throw operation_failed (); + } + + return base::join_syntax (vo); + } }; entry class_entry_; } -- cgit v1.1