![]() |
Database »
Database »
General
Intermediate
A scripted SQL query generation framework with IDE: SQLpp (v1.4)By Jonathan de HalleuxA helper framework for generation of SQL queries in C++ and Lua |
SQL, VC6, VC7, VC7.1Win2K, WinXP, Win2003, SQL Server, STL, DBA, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||
SQLpp objective is to simplify the creation of SQL queries, and more specifically in C++. A .NET wrapper is under construction.
SQLpp uses the Boost Graph Library (BGL), see [2], to make an internal graph presentation of the table and constraints, in order to simplify query generation.
SQLpp follows the article proposed by Andreas Saurwein, SQL WHERE - the
easy way, see [1]. The author presents a small
class helper for easily formatting where clauses in C++.
Starting from his idea, SQLpp has been built in order to simplify the tedious
and error-prone writing of SQL queries.
The latest informations and downloads are available at http://sqlpp.sourceforge.net/.
A full documentation of the classes is given (using Doxygen), therefore, I will not go into details about the classes' methods but rather give a general overview of SQLpp and various examples to illustrate it.
In this article, it is assumed that the reader has some basic knowledge about SQL and SQL queries.
SQLpp is still a work in progress, I present it here in order to have construtive suggestions. In the future SQLpp should be embedded into some GUI...
Here is a list of SQLpp features:
date, time, timestamp
join method to link two query tables
inner join, outer join (left, right, full),
join resolution if possible*,
and, or, xor, not,
is (not) null,
exists,
union, excepts, intersects
count, avg, max,
min, sum, mean
in predicate,
* Given two tables, SQLpp checks for the corresponding reference constraints. If this constraint is unique, SQLpp knows it is the constraint to use to perform the join.
Before getting into the details of SQLpp, let me illustrate it with a simple example. Suppose that we have the following database:
There are a few remarks to make about it:

EmployeeStores has a multiple field primary key,
Clients,
or remark above),
Employees contains a self reference (ChiefID)In order to help the user, SQLpp must learn the database structure. This can be done by hard coding the tables, fields and constraints or more easily by using the built-in SQL parser (written using Spirit 7.0).
New in 1.4: You need to provide a database adapter to the constructor. A database adapter takes care of handling the difference between the different databases available, differences like date format, forward declaration support, subqueries support, etc...
//creating a database adaptor adapters::database_adaptor_shared_ptr mysql (new adapters::mysql_adaptor()); database_shared_ptr db = database::create(mysql); if(!parse_sql( sql )) ...// could not parse the stringLua
Remarks:
mssql_adaptor is also availableThis is about all you need to do. Internally, SQLpp has built a graph, using the BGL, where the vertices are the table and the edges are the reference constraints as depicted in the picture below.
You can also verify the DB structure by generating its SQL creating statement:
cout<<db->get_create_statement()
Adaptors are used to match the particularity of each database engine. They have two main properties:
Adaptors are in the adaptors namespace.
Once the database is constructed, it can be used to create queries. A query object can be constructed from the database:
select_query_shared_ptr q = db.create_query();
and the SQL code is generated by
cout<<q->get_sql();
Once the query is allocated, you need to take the following steps:
where, group by, having and order by conditions (optional).
The steps above will be illustrated by a number of SQL queries ranging from very simple to more elaborate.
We want to list all the clients:
SELECT * FROM Clients
q->add_table( db->get_table("Clients") );
Remarks:
database::get_table retreives the table Clients (as a shared pointer). If table Clients is not found, an exception is thrown.
select_query::add_table adds the Clients
table to the query and returns are query table pointer. A query table is an
instance of a table which has to be used later to specify field belongings. Note
that we need to differentiate query table and table in order to handle
self-referenced tables.select all clients last name :
SELECT C.ClientLastName AS 'Last Name' FROM Clients AS C
query_table_shared_ptr tc=q->add_table( db->get_table("Clients"), "C" ); q->add_field( tc->get_field("ClientLastName"), "Last Name" );
Remarks:
C is the Clients table alias
query_table::get_field retrieve the field ClientLastName (as a shared pointer),
select_query::add_field adds the field ClientLastName with alias Last Name. The
alias is optional.
select count( * ) from clients :
SELECT COUNT(ClientID) FROM Clients
query_table_shared_ptr tc=q->add_table( db->get_table("Clients")); q->add_field( count( tc ) );
Remarks:
count takes any query_field pointer. If
NULL, * is used.
sum, avg, max, min,
sum are also available
DISTINCT, put true as second
parameter: q->add_field( count( tc->get_field("ClientID"), true ) );
select product price * order quantity from orders and products:
SELECT O.OrderQuantity * P.ProductPrice
FROM ...
query_table_shared_ptr to=q->add_table( db->get_table("Orders"), "C" ); query_table_shared_ptr tp=q->join( to, db->get_table("Products"), "C" ); q->add_field( to->get_field("OrderQuantity") * tp->get_field("ProductPrice") );
Remarks:
join will be explained later. It performs the inner join.
+, *, -,
/
select product price * VAT from products :
SELECT P.ProductPrice * 1.21 FROM Products
double vat = 1.21; q->add_field( tp->get_field("ProductPrice") * vat );
Remarks
float, double,
int, uint, long, ulong,
select sum( product price * order quantity * VAT ) from products and orders :
SELECT SUM( P.ProductPrice * O.OrderQuantity * 1.21 ) FROM ...
double vat = 1.21; q->add_field( sum( tp->get_field("ProductPrice") * tp->get_field("ProductPrice") * vat ) );
select orders date and client name:
SELECT C.ClientLastName AS 'Name', O.OrderDate
FROM Orders AS O
INNER JOIN Clients AS C USING(ClientID)
query_table_shared_ptr to=q->add_table( db->get_table("Orders"), "O" ); query_table_shared_ptr tc=q->join( to, db->get_table("Clients"), "C" ); q->add_field( tc->get_field("ClientLastName"), "Name" ); q->add_field( to->get_field("OrderDate"));
Remarks:
select_query::join is used to link Orders
table with Clients table. Internally, SQLpp first checks
that a reference constraint exists between the table, using the database graph.
If not, exception is thrown.
join you must always the source table first
(that contains foreign keys) and then target table (that is referenced),
select_query builds a graph of the query tables and
the joins: query tables are the vertices, joins are the edges,
INNER JOIN has
been defaulted,
USING clause (foreign key and
primary key have same name) can be used, if not it switches to
ON.select the employees name and their respective chief name (referenced by ChiefID):
SELECT E.EmployeeLastName, Ch.EmployeeLastName
FROM Employees AS E
INNER JOIN Employees AS Ch ON E.ChiefID = Ch.EmployeeID
//let disable using select_query::set_support_using(false); query_table_shared_ptr te= q->add_table( db->get_table("Employees"), "E" ); query_table_shared_ptr tch= q->add_table( db->get_table("Employees"), "Ch" ); q->join( te, // joined table (contains the foreing key) tch // referenced table ); q->add_field( tc->get_field("LastName")); q->add_field( tch->get_field("LastName"));
Remarks
Employees has a self reference (ChiefID).
EmployeeLastName but link them to
different query tables.
join can be used to link to tables already added to the query
select_query::get_table_joins(). Employees AS E -> Employees AS Ch
select order date, product name, client name, vendor name and store address:
The two previous examples where presenting simple joins between two tables. What happens when you have more complex joins involving a number of tables? Several problems arise:
These two questions are easily answered using graph theory and the BGL. For
example, the second question can be translated into: How do I find a path that
explores incrementally all the vertices (see picture)? The answer is use
undirected_dfs from the BGL.
SELECT O.OrderDate, P.ProductName, C.ClientLastName,
E.EmployeeLastName, S.StoreAddress
FROM (((Orders AS O
INNER JOIN Clients AS C USING(ClientID))
INNER JOIN Products AS P USING(ProductID))
INNER JOIN Employees AS E USING(EmployeeID))
LEFT OUTER JOIN Stores AS S Using(StoreID)
query_table_shared_ptr to=q->add_table( db->get_table("Orders"), "O" ); query_table_shared_ptr tc=q->join( to, db->get_table("Clients"), "C" ); query_table_shared_ptr tp=q->join( to, db->get_table("Products"), "P" ); query_table_shared_ptr te=q->join( to, db->get_table("Employee"), "E" ); query_table_shared_ptr ts= q->join( tp, db->get_table("Stores"), "S" , LeftOuterJoin ); q->add_field( to->get_field("OrderDate")); q->add_field( tp->get_field("ProductName")); q->add_field( tc->get_field("ClientLastName")); q->add_field( te->get_field("EmployeeLastName")); q->add_field( ts->get_field("StoreAddress"));
Remarks
LeftOuterJoin has been added to illustrate how you specify
different join types. Other supported types are: LeftJoin, RightOuterJoin,
FullJoin. In the previous examples, we didn't specify a where
condition. To do, just do:
q->set_where( ... );
In the following, for each example, we suppose that tables have been added and joined.
select from clients where client first name = "John"
SELECT * FROM Clients
q->set_where( t_clients->get_field("ClientFirstName") == "John" );
Remarks
== operator is a template function that create class
holding the field and the value
Not type checking is made.
ClientFirstName
query_field pointer: query_field_shared_ptr qf_cfn = t_clients->get_field("ClientFirstName");
select from clients, orders where cliendid = order.clientid
SELECT * FROM Clients AS C, Orders AS O WHERE C.ClientID = O.ClientID
q->set_where(equal( qf_cfn, t_orders->get_field("ClientID") ) );
Remarks
== operator because it clashes
with the smart pointer == operator.
!=. select from clients where client first name is null
SELECT * FROM Clients WHERE ClientFirstName IS NOT NULL
q->set_where(is_not_null(qf_cfn) );Remarks
is_null can be used to generate IS
NULLselect from clients where client first name in ('John' or 'Bill')
SELECT * FROM Clients WHERE ClientFirstName in ('John','Bill')
vector<string> v; v.push_back("John"); v.push_back("Bill"); q->set_where(in( qf_cfn, v.begin(), v.end() ) );
Remarks
in takes an iterator range (v.begin(),
v.end()). Since it is a template function, it does not depend on the
container type (must support forward iterators).
select from clients where client first name in (select client first name from clients where ClientID > 10)
SELECT * FROM Clients WHERE
ClientFirstName in (SELECT ClientFirstName FROM
Clients WHERE ClientID > 10)
// creating subquery select_query_shared_ptr sub_q = db::create_query();... t_sub_client= sub_q->add_table( db->get_table("Clients")); sub_q->add_field( t_sub_client, t_sub_client->get_field("ClientFirstName") ); // creating main query q->set_where(in( qf_cfn, sub_q ) );
select from clients where clientid between 10 and 50
SELECT * FROM Clients WHERE ClientID BETWEEN 10 AND 50
q->set_where(between( qf_cid,10,50 ) );
SELECT * FROM Clients WHERE ClientFirstName LIKE 'John%'
q->set_where(like(qf_cfn, "John%"));
select from clients where client first name = "John" and not client first like 'M%' "Martin"
SELECT * FROM Clients WHERE ClientFirstName = 'John'
AND NOT ClientFirstName LIKE 'M%'
q->set_where(and( qf_cfnq == "John", not( like(qf_cfln, "M%") ) ) );
Remarks
or and xor also available,
not is an unary operator. select case clientID when 1 then 'found' else 'not found' end from clients
SELECT
CASE C.ClientID
WHEN 1 THEN 'found'
ELSE 'not found'
END
FROM Clients AS C
q->add_field( case_( qc->get_field("ClientID") ) ->when( to_expression(1) , to_expression("found") ) ->else( to_expression("not found") );
Remarks
case_ without
argument.Unions, difference and intersections on queries are done as:
query_shared_ptr q1, q2, q; q=union_(q1,q2); q=except(q1,q2); q=intersect(q1,q2, true); // add ALL
Remarks
union_ has a underscore because union is a C++ reserved word,
ALL by setting the third parameter to
trueAll SQLpp objects are manipulated as smart pointers using Boost
shared_ptr implementation. Internally, some are stored as
weak_ptr to break cycles.
A lot of methods throw exception when the desired behavior is flawed: a wrong
field name, bad join, etc... All classes throw a sqlpp_exception
(derived from std::exception):
try { // proted here the SQLpp code } catch (std::exception& e) { cout<<e.what()<<endl; }
The SQL parser is a "home made" parser built with Spirit 1.7. As always, use it at your own risk. It supports:
alter table ... add constraint
... declarationInclude sqlpp/parsers/sql_parser.hpp to use it.
The namespaces mimic the include file directory structure as in C#. All SQLpp
lives in sqlpp namespace, select_query and
query_predicate live in queries, parsers live in parsers.
date, time, timestamp
join method to link two query tables
joins (now using
undirected graph)
select_query::get_table_joins query_field semantics: tc +
tc->get_field becomes tc->get_field,
in_predicate,
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 12 Sep 2003 Editor: Chris Maunder |
Copyright 2003 by Jonathan de Halleux Everything else Copyright © CodeProject, 1999-2009 Web20 | Advertise on the Code Project |