I'm writing a SQLite wrapper for C++. I have this class representing a database:

  class Database {
    sqlite3 *db;
    Database(const std::string& file_path, bool readonly = false) throw(SQLiteException);

    std::vector<std::map<std::string, Value> > Query(const std::string& query) throw(SQLiteException);
    std::vector<std::map<std::string, Value> > Query(const std::string& query, const std::vector<Value>& bindings) throw(SQLiteException);

Where Value is a container for an int, double, std::string or std::vector<char> (because the returned type isn't yet known at compile-time).

I'm concerned about the return value types of Database::Query. How could I simplify this?


Types have to be known at compile time. Unfortunately to be flexable you need to look up arbitory types at runtime for a DB. So either you need to use some form of template-meta programming or you can use a variant type. I would go for something like boost Any object or boost variant this will allow you to return arbitrary types.

I don't think you want to return the result from the DB after creating the Query. You want to return a query object that will allow you to bind things and execute the query at a latter stage. This is because in most DB systems creating the query is relatively expensive.

And when you do execute the query you want to use the concept of the cursor (iterator) to move over the result set rather than generating the full result in one go (as this could potentially be huge).

int main();
    Database   db("Bla", "Passowrd", true);
    Query      getName = db.query("SELECT Name,Address,Phone "
                                  "FROM Users Where Age=%d and Sex=%s");

    // Allow user to bind values to the query and execute.
    // As building the query can be expensive. So you want to re-use it.
    Result     age25   = getName.execute(bind() % 25 % "M"));

    // Re-use the Query object for the next query   (Note bind should allow multiple param)
    Result      age31   = getName.execute(bind() % 31 % "F"));

    // Rather than returning all the results from the query in one go
    // Have the result object provide an `input iterator` allowing you to retrieve the
    // the rows as required. That way you do not need to copy out huge objects all in
    // one go. You can scan over the input until you get the bit you want (potentially)
    // only copying selected parts out of the result.

    if (!age25) // convert object to boolean to test for OK
    {           // Like a std::stream object. This way if the query has
                // an error you can quickly an easily test for it.

        std::cout << age25.error() << "\n";
        for(RowIter row = RowIter(age25); row != RowIter(); ++row)
        {                                 //     ^^^^^^^^^  like std::istream_iteator
                                          //                an empty object is end()

            std::cout << db_cast<std::string>(row->get("Name")) << " "
                      << db_cast<std::string>(row->get("Address")) << " "
                      << db_cast<int>(row->get("Phone")) << "\n";
                     //  ^^^^^^^^^^^^
                     //  Convert the object into the correct type
                     //  using a cast like syntax.


Also do not limit it to sqlite.
Make it so it is easily expandable to other DB types.

I would go with a prefix Notation on the DB name:

DataBase    db1("sqlite://FileName1", "OptionalPassword", flags);
DataBase    db2("mysql://db@host.bob.com", "Password", flags);
Database    db3("oracle://plop.foo.bar");

Internally you can then use the PIMPL design pattern to abstract the different types of DB. For now you just implement the sqlite version make the others generate an exception.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy