16
\$\begingroup\$

Working on my SQL project at last.

The concept is easy to use and integrate SQL into C++.

    ThorsSQL::Connection    mysql("mysql://host", "username", "password", "databaseName");
    ThorsSQL::Statement     bigEarnerStat(mysql, 
                                          "SELECT ID, Name, Salary FROM Employee WHERE Salary > % and Age < %"
                                          ThorsAnvil::SQL::Prepare);

    // Bind variables to '%' in statement
    // Then execute the SQL statement.
    // Call function for every row returned.
    bigEarnerStat.execute(Bind(1000000, 32), // parameter bound to % in statement.
        // Function executed for each row returned.
        // Parameters are matched against the SELECT in the statement.
        // A bad type conversion will throw an exception.
        [](u64 id, std::string const& name, int salary){
            std::cout << name << " is a fat cat earning $" << salary/100 << "." << salary%100 << "\n";
        }
    );

Connection:
Represents a single connection to the DB.

The thing to note above is mysql://host. The concept being that these classes provide the framework that allows specific DB code to be plugged into (A MySQL variant will be coming to code review soon). So the "Schema" part of the URL string specifies the type of DB (and thus what specific plugin the code below uses (see ConnectionCreatorRegister)

So the Connection object will defer all the DB specific work to the proxy member. This class handles all the generic code.

ConnectionProxy:
This DB specific code for a connection.

ConnectionCreatorRegister:
This allows a DB specific implementation to register itself as a viable alternative.

Note: If you want to try compiling the code I suggest you check it out of the git repo and compile using the instructions there. But Saying that you can potentially compile it using only the source here just add a main().

Connection.h

#ifndef THORS_ANVIL_SQL_CONNECTION_H
#define THORS_ANVIL_SQL_CONNECTION_H

#include "SQLUtil.h"
#include <string>
#include <map>
#include <memory>

namespace ThorsAnvil
{
    namespace SQL
    {
        class Statement;
        class StatementProxy;
        class ConnectionProxy
        {
            public:
                virtual ~ConnectionProxy()  = 0;
                virtual std::unique_ptr<StatementProxy> createStatementProxy(std::string const& statement, StatementType type) = 0;
        };

        inline ConnectionProxy::~ConnectionProxy() {}
        using ConnectionCreator= std::function<std::unique_ptr<ConnectionProxy>(std::string const& host, int port,
                                                                        std::string const& username,
                                                                        std::string const& password,
                                                                        std::string const& database,
                                                                        Options const& options)>;
        class Connection
        {
            private:
                static std::map<std::string, ConnectionCreator>&    getCreators();

                friend class Statement;
                std::unique_ptr<StatementProxy> createStatementProxy(std::string const& statement, StatementType type);

                std::unique_ptr<ConnectionProxy>  proxy;
            public:
                Connection(std::string const& connection,
                            std::string const& username,
                            std::string const& password,
                            std::string const& database,
                            Options const& options = Options{});

                static void registerConnectionType(std::string const& schema, ConnectionCreator creator);
        };

        template<typename T>
        class ConnectionCreatorRegister
        {
            public:
                ConnectionCreatorRegister(std::string const& schema)
                {
                    Connection::registerConnectionType(schema, [](std::string const& host, int port,
                                                                std::string const& username,
                                                                std::string const& password,
                                                                std::string const& database,
                                                                Options const& options)
                    {
                        return std::unique_ptr<ConnectionProxy>(new T(host, port , username, password, database, options));
                    });
                }
        };
    }
} 
#endif

Connection.cpp

#include "Connection.h"
#include "Statement.h"
#include <cerrno>
#include <cstdlib>

using namespace ThorsAnvil::SQL;

Connection::Connection(std::string const& connection,
                       std::string const& username,
                       std::string const& password,
                       std::string const& database,
                       Options const& options)
{
    std::size_t     schemaEnd   = connection.find(':');
    if (schemaEnd == std::string::npos || connection[schemaEnd + 1] != '/' || connection[schemaEnd + 2] != '/') {
        throw std::runtime_error("Connection::Connection: Failed to find schema: " + connection);
    }

    bool        hasPort     = true;
    std::size_t hostEnd     = connection.find(':', schemaEnd + 3);

    if (hostEnd == std::string::npos) {
        hasPort = false;
        hostEnd = connection.size();
    }

    std::string schema      = connection.substr(0, schemaEnd);
    std::string host        = connection.substr(schemaEnd + 3, hostEnd - schemaEnd - 3);
    std::string port        = hasPort ? connection.substr(hostEnd + 1) : "0";

    errno                   = 0;
    char*       endPtr;
    int         portNumber  = std::strtol(port.c_str(), &endPtr, 10);
    auto        creator     = getCreators().find(schema);

    if (host == "" || errno != 0 || *endPtr != '\0') {
        throw std::runtime_error("Connection::Connection: Failed to parse connection: " + connection);
    }

    if (creator == getCreators().end()) {
        throw std::runtime_error("Connection::Conection: Schema for unregister DB type: " + schema + " From: " + connection);
    }
    proxy   = creator->second(host, portNumber, username, password, database, options);
}

std::map<std::string, ConnectionCreator>& Connection::getCreators()
{
    static std::map<std::string, ConnectionCreator> creators;
    return creators;
}
void Connection::registerConnectionType(std::string const& schema, ConnectionCreator creator)
{
    getCreators().emplace(schema, creator);
}


std::unique_ptr<StatementProxy> Connection::createStatementProxy(std::string const& statement, StatementType type)
{
    return proxy->createStatementProxy(statement, type);
}

test/ConnectionTest.cpp

#include "Connection.h"
#include "Statement.h"

#include "gtest/gtest.h"
#include "test/MockMysql.h"


ThorsAnvil::SQL::ConnectionCreatorRegister<MockMySQLConnection>   registerFakeMysql("mysql");

TEST(ConnectionTest, Create)
{
    using ThorsAnvil::SQL::Connection;
    Connection     connection("mysql://127.0.0.1:69", "root", "testPassword", "test");
}
TEST(ConnectionTest, CreateDefaultPort)
{
    using ThorsAnvil::SQL::Connection;
    Connection     connection("mysql://127.0.0.1", "root", "testPassword", "test");
}

TEST(ConnectionTest, BadSchema)
{
    using ThorsAnvil::SQL::Connection;
    ASSERT_THROW(
    Connection     connection("badschema://127.0.0.1:69", "root", "testPassword", "test"),
    std::runtime_error
    );
}

TEST(ConnectionTest, NoSchema)
{
    using ThorsAnvil::SQL::Connection;
    ASSERT_THROW(
    Connection     connection("127.0.0.1:69", "root", "testPassword", "test"),
    std::runtime_error
    );
}
TEST(ConnectionTest, BadHost)
{
    using ThorsAnvil::SQL::Connection;
    ASSERT_THROW(
    Connection     connection("mysql://:69", "root", "testPassword", "test"),
    std::runtime_error
    );
}
TEST(ConnectionTest, BadPort)
{
    using ThorsAnvil::SQL::Connection;
    ASSERT_THROW(
    Connection     connection("mysql://127.0.0.1:XY", "root", "testPassword", "test"),
    std::runtime_error
    );
}
\$\endgroup\$
  • 1
    \$\begingroup\$ No enough to make an answer, but you might want to use std::make_unique<T>(...) rather than std::unique_ptr<ConnectionProxy>(new T(...)). A bit cleaner and safer. \$\endgroup\$ – Apples Jun 29 '15 at 21:28
1
\$\begingroup\$

GCC now has SSO, with the ABI breaking backwards compatibility and all that. Which probably means you can pass them by value.

\$\endgroup\$
1
\$\begingroup\$

There isn't too much to say here, since this code isn't the interest part of the project. Way to set me up with the bigEarnerStat.execute() call and then not have a code review for that part. I feel empty inside.

Anyway, mostly minor comments.

Is there a reason for ConnectionProxy::~ConnectionProxy() to be pure virtual instead of simply defaulted?

I'm wary of the ConnectionCreatorRegister class. It's just there to be able to stick a global variable somewhere to call the registerConnectionType() function, but the initialization rules are based on odr-use of the variable. You never odr-use registerFakeMysql, so I'm not sure that you have the guarantee that it will have actually been registered. Startup issues like that are weird.


Code-wise, this:

return std::unique_ptr<ConnectionProxy>(new T(host, port , username, password, database, options));

should be spelled:

return std::make_unique<T>(host, port, username, password, database, options);

There's no advantage here compared to make_shared, but it's shorter and more explicit. We're making a T, and this puts that front and center. You could, if you want, throw in a static_assert that std::is_convertible<T*, ConnectionProxy*> so that the error message will be clearer to your users.

The Connection() constructor could be cleaned up. Basically, you start by parsing connection into a schema, a host, and a port. We could factor that out into a separate type and a function:

struct ConnectionParts { // or better name
    std::string schema;
    std::string host;
    int port;

    static ConnectionParts parse(std::string const& );
};

Connection(std::string const& connection,
           std::string const& username,
           std::string const& password,
           std::string const& database,
           Options const& options)
: Connection(ConnectionParts::parse(connection), username, password, database, options)
{ }

Connection(ConnectionParts const& parts, ...)
{ ... }

That'll also let the user pass in the schema, host, and port independently themselves if that's what they want to do. Just makes it more usable, and you don't lose anything.

Lastly, I'm not a fan of lining up the =s. It takes a lot of work on the coding side to actually do that, and I find it makes the code much harder to read. You have to scan a lot further to find the important bits. Especially when you're indenting the variable names too, like:

auto        creator     = getCreators().find(schema);
    <------>       <--->

Too much screen real estate. Just do:

auto creator = getCreators().find(schema);
\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.