changeset 169:450d884456c4

Server: initial re-support of PostgreSQL
author David Demelier <markand@malikania.fr>
date Fri, 17 Aug 2018 15:44:12 +0200
parents bc450538a6c4
children a3af3b793da4
files CMakeLists.txt libcommon/CMakeLists.txt libserver/CMakeLists.txt libserver/malikania/server/db/account.cpp libserver/malikania/server/db/account.hpp libserver/malikania/server/db/character.cpp libserver/malikania/server/db/character.hpp libserver/malikania/server/db/database.cpp libserver/malikania/server/db/database.hpp libserver/malikania/server/db/dynlib_database.cpp libserver/malikania/server/db/dynlib_database.hpp libserver/malikania/server/db/model.hpp libserver/malikania/server/db/spell.cpp libserver/malikania/server/db/spell.hpp libserver/malikania/server/net/auth_handler.cpp server/main.cpp
diffstat 16 files changed, 912 insertions(+), 656 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Mon Aug 06 13:01:00 2018 +0200
+++ b/CMakeLists.txt	Fri Aug 17 15:44:12 2018 +0200
@@ -19,7 +19,7 @@
 cmake_minimum_required(VERSION 3.0)
 project(malikania)
 
-set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_CXX_STANDARD 17)
 set(CMAKE_CXX_STANDARD_REQUIRED TRUE)
 set(CMAKE_POSITION_INDEPENDENT_CODE TRUE)
 
--- a/libcommon/CMakeLists.txt	Mon Aug 06 13:01:00 2018 +0200
+++ b/libcommon/CMakeLists.txt	Fri Aug 17 15:44:12 2018 +0200
@@ -42,15 +42,6 @@
     ${libmlk-common_SOURCE_DIR}/malikania/util.cpp
 )
 
-if (WITH_BACKEND MATCHES "SDL")
-    find_package(SDL2 REQUIRED)
-
-    list(APPEND HEADERS ${libmlk-common_SOURCE_DIR}/malikania/${WITH_BACKEND_DIR}/sdl_util.hpp)
-    list(APPEND SOURCES ${libmlk-common_SOURCE_DIR}/malikania/${WITH_BACKEND_DIR}/sdl_util.cpp)
-    list(APPEND INCLUDES ${SDL2_INCLUDE_DIRS})
-    list(APPEND LIBRARIES ${SDL2_LIBRARIES})
-endif ()
-
 malikania_define_library(
     PROJECT libmlk-common
     TARGET libmlk-common
--- a/libserver/CMakeLists.txt	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/CMakeLists.txt	Fri Aug 17 15:44:12 2018 +0200
@@ -18,24 +18,28 @@
 
 project(libmlk-server)
 
+find_package(PostgreSQL REQUIRED)
+
 set(
     HEADERS
+    ${libmlk-server_SOURCE_DIR}/malikania/server/client.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/account.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/character.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/database.hpp
+    ${libmlk-server_SOURCE_DIR}/malikania/server/db/model.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/spell.hpp
-    ${libmlk-server_SOURCE_DIR}/malikania/server/db/model.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/net/auth_handler.hpp
-    ${libmlk-server_SOURCE_DIR}/malikania/server/client.hpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/server.hpp
 )
 
 set(
     SOURCES
+    ${libmlk-server_SOURCE_DIR}/malikania/server/client.cpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/account.cpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/db/character.cpp
+    ${libmlk-server_SOURCE_DIR}/malikania/server/db/database.cpp
+    ${libmlk-server_SOURCE_DIR}/malikania/server/db/spell.cpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/net/auth_handler.cpp
-    ${libmlk-server_SOURCE_DIR}/malikania/server/client.cpp
     ${libmlk-server_SOURCE_DIR}/malikania/server/server.cpp
 )
 
@@ -44,12 +48,14 @@
     SOURCES ${HEADERS} ${SOURCES}
     LIBRARIES
         ${Boost_LIBRARIES}
+        ${PostgreSQL_LIBRARIES}
         OpenSSL::Crypto
         OpenSSL::SSL
         libmlk-common
         $<$<BOOL:${WIN32}>:mswsock>
     PUBLIC_INCLUDES
         ${Boost_INCLUDE_DIRS}
+        ${PostgreSQL_INCLUDE_DIRS}
         ${libmlk-server_SOURCE_DIR}
     PRIVATE_INCLUDES
         ${libmlk-server_SOURCE_DIR}/malikania/server
--- a/libserver/malikania/server/db/account.cpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/db/account.cpp	Fri Aug 17 15:44:12 2018 +0200
@@ -16,97 +16,227 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <cassert>
+
 #include "account.hpp"
-#include "database.hpp"
+
+namespace mlk::server::db {
+
+auto account::load(const result& res) -> account
+{
+    account a(PQgetvalue(res.get(), 0, 1), PQgetvalue(res.get(), 0, 2));
+
+    a.id_ = std::stoi(PQgetvalue(res.get(), 0, 0));
+    a.firstname_ = PQgetvalue(res.get(), 0, 3);
+    a.lastname_ = PQgetvalue(res.get(), 0, 4);
+    a.email_ = PQgetvalue(res.get(), 0, 5);
+    a.characters_ = character::load(a);
 
-namespace mlk {
+    return a;
+}
+
+void account::clear()
+{
+    id_ = 0U;
+}
 
-namespace server {
+account::account(std::string login, std::string password) noexcept
+    : login_(std::move(login))
+    , password_(std::move(password))
+{
+    assert(!login_.empty());
+    assert(!password_.empty());
+}
+
+auto account::get_login() const noexcept -> const std::string&
+{
+    return login_;
+}
 
 void account::set_password(std::string password)
 {
+    static const std::string sql(
+        "UPDATE account"
+        "   SET password = $1"
+        " WHERE id = $2"
+    );
+
     assert(!password.empty());
 
     if (is_published() && password_ != password)
-        db_.accounts().set_password(*this, password);
+        db::exec(sql, { password, id_ });
 
     password_ = std::move(password);
 }
 
+auto account::get_email() const noexcept -> const std::string&
+{
+    return email_;
+}
+
 void account::set_email(std::string email)
 {
+    static const std::string sql(
+        "UPDATE account"
+        "   SET email = $1"
+        " WHERE id = $2"
+    );
+
     assert(!email.empty());
 
     if (is_published() && email_ != email)
-        db_.accounts().set_email(*this, email);
+        db::exec(sql, { email, id_ });
 
     email_ = std::move(email);
 }
 
+auto account::get_firstname() const noexcept -> const std::string&
+{
+    return firstname_;
+}
+
 void account::set_firstname(std::string name)
 {
+    static const std::string sql(
+        "UPDATE account"
+        "   SET firstname = $1"
+        " WHERE id = $2"
+    );
+
     if (is_published() && firstname_ != name)
-        db_.accounts().set_firstname(*this, name);
+        db::exec(sql, { name, id_ });
 
     firstname_ = std::move(name);
 }
 
+auto account::get_lastname() const noexcept -> const std::string&
+{
+    return lastname_;
+}
+
 void account::set_lastname(std::string name)
 {
+    static const std::string sql(
+        "UPDATE account"
+        "   SET lastname = $1"
+        " WHERE id = $2"
+    );
+
     if (is_published() && lastname_ != name)
-        db_.accounts().set_lastname(*this, name);
+        db::exec(sql, { name, id_ });
 
     lastname_ = std::move(name);
 }
 
+auto account::get_characters() const noexcept -> const std::vector<character>&
+{
+    return characters_;
+}
+
 void account::add(character ch)
 {
-    assert(is_published() && ch.is_draft());
+    assert(ch.is_draft());
+
+    characters_.reserve(characters_.size() + 1);
 
-    db_.characters().publish(ch, *this);
+    if (is_published())
+        ch.publish(*this);
+
     characters_.push_back(std::move(ch));
 }
 
 void account::remove(std::vector<character>::iterator it)
 {
-    // TODO: assert it is in vector.
-    assert(is_published());
-
-    db_.characters().unpublish(*it);
+    // TODO: assert 'it' is in vector.
+    it->unpublish();
     characters_.erase(it);
 }
 
-void account::save()
+void account::publish()
 {
-    if (is_draft())
-        db_.accounts().publish(*this);
+    static const std::string sql(
+        "INSERT INTO account("
+        "  login,"
+        "  password,"
+        "  firstname,"
+        "  lastname,"
+        "  email"
+        ") "
+        "VALUES ($1, $2, $3, $4, $5) "
+        "RETURNING id"
+    );
+
+    assert(is_draft());
+
+    /*
+     * Recursively save the account, its characters and spells of those
+     * characters.
+     */
+    transaction txn([this] {
+        clear();
+
+        for (auto& c : characters_)
+            c.clear();
+    });
+
+    const auto r = select(sql, { login_, password_, firstname_, lastname_, email_ });
+
+    id_ = std::stoi(PQgetvalue(r.get(), 0, 0));
+
+    for (auto& c : characters_)
+        c.publish(*this);
+
+    txn.commit();
 
     assert(is_published());
 }
 
-void account::remove()
+void account::unpublish()
 {
-    if (is_published())
-        db_.accounts().unpublish(*this);
+    static const std::string sql(
+        "DELETE"
+        "  FROM account"
+        " WHERE id = $1"
+    );
+
+    assert(is_published());
 
-    id_ = 0U;
-    characters_.clear();
+    id_ = 0;
 
+    /*
+     * Recursively make all characters and their spells draft.
+     */
+    for (auto& c : characters_)
+        c.clear();
+    
     assert(is_draft());
 }
 
-boost::optional<account> account_dao::authenticate(const std::string& login,
-                                                   const std::string& password)
+auto account::find_by_login(const std::string& login) -> std::optional<account>
+{
+    static const std::string sql(
+        "SELECT *"
+        "  FROM account"
+        " WHERE login = $1"
+    );
+
+    const auto r = select(sql, { login });
+
+    if (PQntuples(r.get()) == 0)
+        return std::nullopt;
+
+    return load(r);
+}
+
+auto account::authenticate(const std::string& login,
+                           const std::string& password) -> std::optional<account>
 {
     auto ac = find_by_login(login);
 
-    if (!ac || ac->get_password() != password)
-        return boost::none;
+    if (!ac || ac->password_ != password)
+        return std::nullopt;
 
     return ac;
 }
 
-
-} // !server
-
-} // !mlk
+} // !mlk::server::db
--- a/libserver/malikania/server/db/account.hpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/db/account.hpp	Fri Aug 17 15:44:12 2018 +0200
@@ -24,14 +24,13 @@
  * \brief Database account object.
  */
 
-#include <boost/optional.hpp>
+#include <optional>
 
-#include "model.hpp"
 #include "character.hpp"
+#include "database.hpp"
+#include "model.hpp"
 
-namespace mlk {
-
-namespace server {
+namespace mlk::server::db {
 
 /**
  * \brief Database account object.
@@ -45,6 +44,10 @@
     std::string lastname_;
     std::vector<character> characters_;
 
+    static auto load(const result&) -> account;
+
+    void clear();
+
 public:
     /**
      * Create a draft account.
@@ -55,35 +58,14 @@
      * \param password the password
      * \warning the password is saved as-is and **must** be hashed by the caller.
      */
-    inline account(database& db, std::string login, std::string password) noexcept
-        : model(db)
-        , login_(std::move(login))
-        , password_(std::move(password))
-    {
-        assert(!login_.empty());
-        assert(!password_.empty());
-    }
+    account(std::string login, std::string password) noexcept;
 
     /**
      * Get the account login.
      *
      * \return the login
      */
-    inline const std::string& get_login() const noexcept
-    {
-        return login_;
-    }
-
-    /**
-     * Get the password.
-     *
-     * \note the password is returned as-is
-     * \return the password
-     */
-    inline const std::string& get_password() const noexcept
-    {
-        return password_;
-    }
+    auto get_login() const noexcept -> const std::string&;
 
     /**
      * Set the password.
@@ -98,10 +80,7 @@
      *
      * \return the email
      */
-    inline const std::string& get_email() const noexcept
-    {
-        return email_;
-    }
+    auto get_email() const noexcept -> const std::string&;
 
     /**
      * Set the account email.
@@ -116,10 +95,7 @@
      *
      * \return the name
      */
-    inline const std::string& get_firstname() const noexcept
-    {
-        return firstname_;
-    }
+    auto get_firstname() const noexcept -> const std::string&;
 
     /**
      * Set the account firstname.
@@ -133,10 +109,7 @@
      *
      * \return the name
      */
-    inline const std::string& get_lastname() const noexcept
-    {
-        return lastname_;
-    }
+    auto get_lastname() const noexcept -> const std::string&;
 
     /**
      * Set the account last name.
@@ -150,27 +123,7 @@
      *
      * \return the associated characters.
      */
-    inline const std::vector<character>& get_characters() const noexcept
-    {
-        return characters_;
-    }
-
-#if defined(MALIKANIA_PRIVATE)
-    /**
-     * Overloaded function.
-     *
-     * This function is only available if MALIKANIA_PRIVATE is defined as it is
-     * not a end user function but is required to implement new database
-     * backends.
-     *
-     * \warning only use this function in database backends
-     * \param characters the list of characters
-     */
-    inline std::vector<character>& get_characters() noexcept
-    {
-        return characters_;
-    }
-#endif // !MALIKANIA_PRIVATE
+    auto get_characters() const noexcept -> const std::vector<character>&;
 
     /**
      * Add the character to the account.
@@ -198,7 +151,7 @@
      * \throw std::exception on errors
      * \post is_published()
      */
-    void save();
+    void publish();
 
     /**
      * Destroy the account.
@@ -208,106 +161,29 @@
      * \throw std::exception on errors
      * \post is_draft()
      */
-    void remove();
-};
-
-/**
- * \brief Account DAO.
- */
-class account_dao {
-public:
-    /**
-     * Authenticate the user.
-     *
-     * \param login the login
-     * \param password the password
-     * \return the account or null if does not exist
-     */
-    boost::optional<account> authenticate(const std::string& login,
-                                          const std::string& password);
+    void unpublish();
 
     /**
      * Find an account by login.
      *
-     * \param login the login name
-     * \return the account or null if not found
-     * \throw exception on other errors
+     * \param login the login
+     * \return the account or none if not found
+     * \throw std::runtime_error on database errors
      */
-    virtual boost::optional<account> find_by_login(const std::string& login) = 0;
-
-    /**
-     * Save the account.
-     *
-     * The implementation will be called only if the account is draft and must
-     * do the following:
-     *
-     * 1. Register the account to its database,
-     * 2. Create a unique and global id and set it as id_ member variable.
-     *
-     * \note called from account::save
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void publish(account& ac) = 0;
-
-    /**
-     * Remove the account.
-     *
-     * The implementation will be called only if the account is public and must
-     * do remove from the database all the associated characters along with
-     * their spells.
-     *
-     * \note called from account::remove
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void unpublish(const account& ac) = 0;
+    static auto find_by_login(const std::string& login) -> std::optional<account>;
 
     /**
-     * Update the account password in database.
-     *
-     * Only called when the password needs to be changed, the implementation
-     * does not need to update password_ field.
+     * Find and authenticate a user.
      *
-     * \note called from set_password helper
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void set_password(const account& ac, const std::string& password) = 0;
-
-    /**
-     * Update the account email in database.
-     *
-     * Only called when the email needs to be changed, the implementation does
-     * not need to update email_ field.
-     *
-     * \note called from set_email helper
-     * \throw std::exception if the operation could not succeed
+     * \param login the login
+     * \param password the password
+     * \return the account or none if not found
+     * \throw std::runtime_error on database errors
      */
-    virtual void set_email(const account& ac, const std::string& email) = 0;
-
-    /**
-     * Update the account firstname in database.
-     *
-     * Only called when the first name needs to be changed, the implementation
-     * does not need to update firstname_ field.
-     *
-     * \note called from set_firstname helper
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void set_firstname(const account& ac, const std::string& name) = 0;
-
-    /**
-     * Update the account lastname in database.
-     *
-     * Only called when the last name needs to be changed, the implementation
-     * does not need to update lastname_ field.
-     *
-     * \note called from set_lastname helper
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void set_lastname(const account& ac, const std::string& name) = 0;
+    static auto authenticate(const std::string& login,
+                             const std::string& password) -> std::optional<account>;
 };
 
-} // !server
-
-} // !mlk
+} // !mlk::server::db
 
 #endif // !MALIKANIA_SERVER_DB_ACCOUNT_HPP
--- a/libserver/malikania/server/db/character.cpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/db/character.cpp	Fri Aug 17 15:44:12 2018 +0200
@@ -16,57 +16,251 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <cassert>
 #include <numeric>
 
+#include "account.hpp"
 #include "character.hpp"
 #include "database.hpp"
 
-namespace mlk {
+namespace mlk::server::db {
+
+auto character::load(const result& result, int row) -> character
+{
+    character ch(PQgetvalue(result.get(), row, 2), PQgetvalue(result.get(), row, 3));
+
+    ch.levels_[stat::hp] = std::stoi(PQgetvalue(result.get(), row, 4));
+    ch.factors_[stat::hp] = std::stoi(PQgetvalue(result.get(), row, 5));
+    ch.exp_[stat::hp] = std::stoi(PQgetvalue(result.get(), row, 6));
+
+    ch.levels_[stat::force] = std::stoi(PQgetvalue(result.get(), row, 7));
+    ch.factors_[stat::force] = std::stoi(PQgetvalue(result.get(), row, 8));
+    ch.exp_[stat::force] = std::stoi(PQgetvalue(result.get(), row, 9));
+
+    ch.levels_[stat::defense] = std::stoi(PQgetvalue(result.get(), row, 10));
+    ch.factors_[stat::defense] = std::stoi(PQgetvalue(result.get(), row, 11));
+    ch.exp_[stat::defense] = std::stoi(PQgetvalue(result.get(), row, 12));
+
+    ch.levels_[stat::agility] = std::stoi(PQgetvalue(result.get(), row, 13));
+    ch.factors_[stat::agility] = std::stoi(PQgetvalue(result.get(), row, 14));
+    ch.exp_[stat::agility] = std::stoi(PQgetvalue(result.get(), row, 15));
+
+    ch.levels_[stat::luck] = std::stoi(PQgetvalue(result.get(), row, 16));
+    ch.factors_[stat::luck] = std::stoi(PQgetvalue(result.get(), row, 17));
+    ch.exp_[stat::luck] = std::stoi(PQgetvalue(result.get(), row, 18));
+
+    ch.id_ = std::stoi(PQgetvalue(result.get(), row, 0));
+    ch.spells_ = spell::load(ch);
+
+    return ch;
+}
+
+auto character::load(const account& parent) -> std::vector<character>
+{
+    static const std::string sql(
+        "SELECT *"
+        "  FROM character"
+        " WHERE account_id = $1"
+    );
+
+    const auto r = select(sql, { parent.get_id() });
+
+    if (PQntuples(r.get()) == 0)
+        throw std::runtime_error("failed to load characters");
+
+    std::vector<character> characters;
+
+    for (int i = 0; i < PQntuples(r.get()); ++i)
+        characters.push_back(load(r, i));
+
+    return characters;
+}
+
+void character::clear()
+{
+    id_ = 0U;
+
+    for (auto& s : spells_)
+        s.clear();
+}
 
-namespace server {
+void character::publish(account& parent)
+{
+    static const std::string sql(
+        "INSERT INTO character ("
+        "  account_id,"
+        "  nickname,"
+        "  type,"
+        "  hp_level,"
+        "  hp_factor,"
+        "  hp_exp,"
+        "  force_level,"
+        "  force_factor,"
+        "  force_exp,"
+        "  defense_level,"
+        "  defense_factor,"
+        "  defense_exp,"
+        "  agility_level,"
+        "  agility_factor,"
+        "  agility_exp,"
+        "  luck_level,"
+        "  luck_factor,"
+        "  luck_exp"
+        ") "
+        "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) "
+        "RETURNING id"
+    );
+
+    const auto r = select(sql, {
+        parent.get_id(),
+        nickname_,
+        type_,
+        levels_[stat::hp],
+        factors_[stat::hp],
+        exp_[stat::hp],
+        levels_[stat::force],
+        factors_[stat::force],
+        exp_[stat::force],
+        levels_[stat::defense],
+        factors_[stat::defense],
+        exp_[stat::defense],
+        levels_[stat::agility],
+        factors_[stat::agility],
+        exp_[stat::agility],
+        levels_[stat::luck],
+        factors_[stat::luck],
+        exp_[stat::luck]
+    });
+
+    if (PQntuples(r.get()) == 0)
+        throw std::runtime_error("failed to save character");
+
+    id_ = std::stoi(PQgetvalue(r.get(), 0, 0));
+
+    for (auto& s : spells_)
+        s.publish(*this);
+}
+
+void character::unpublish()
+{
+    static const std::string sql(
+        "DELETE"
+        "  FROM character"
+        " WHERE id = $1"
+    );
+
+    exec(sql, { id_ });
+    clear();
+}
+
+character::character(std::string nickname, std::string type) noexcept
+    : nickname_(std::move(nickname))
+    , type_(std::move(type))
+{
+    assert(!nickname_.empty());
+    assert(!type_.empty());
+}
+
+auto character::get_nickname() const noexcept -> const std::string&
+{
+    return nickname_;
+}
+
+auto character::get_type() const noexcept -> const std::string&
+{
+    return type_;
+}
+
+auto character::get_spells() const noexcept -> const std::vector<spell>&
+{
+    return spells_;
+}
+
+auto character::get_levels() const noexcept -> const std::array<std::uint8_t, 5>&
+{
+    return levels_;
+}
 
 void character::set_levels(std::array<std::uint8_t, 5> levels)
 {
+    static const std::string sql(
+        "UPDATE character"
+        "   SET hp_level = $1"
+        "     , force_level = $2"
+        "     , defense_level = $3"
+        "     , agility_level = $4"
+        "     , luck_level = $5"
+    );
+
     if (is_published())
-        db_.characters().set_levels(*this, levels);
+        exec(sql, { id_, levels[0], levels[1], levels[2], levels[3], levels[4] });
 
     levels_ = std::move(levels);
 }
 
+auto character::get_factors() const noexcept -> const std::array<std::uint8_t, 5>&
+{
+    return factors_;
+}
+
 void character::set_factors(std::array<std::uint8_t, 5> factors)
 {
+    static const std::string sql(
+        "UPDATE character"
+        "   SET hp_factor = $1"
+        "     , force_factor = $2"
+        "     , defense_factor = $3"
+        "     , agility_factor = $4"
+        "     , luck_factor = $5"
+    );
+
     assert(std::accumulate(factors.begin(), factors.end(), 0U) == 100U);
 
     if (is_published())
-        db_.characters().set_factors(*this, factors);
+        exec(sql, { id_, factors[0], factors[1], factors[2], factors[3], factors[4] });
 
     factors_ = std::move(factors);
 }
 
+auto character::get_experience() const noexcept -> const std::array<std::uint32_t, 5>&
+{
+    return exp_;
+}
+
 void character::set_experience(std::array<std::uint32_t, 5> experience)
 {
+    static const std::string sql(
+        "UPDATE character"
+        "   SET hp_experience = $1"
+        "     , force_experience = $2"
+        "     , defense_experience = $3"
+        "     , agility_experience = $4"
+        "     , luck_experience = $5"
+    );
+
     if (is_published())
-        db_.characters().set_experience(*this, experience);
+        exec(sql, { id_, experience[0], experience[1], experience[2], experience[3], experience[4] });
 
     exp_ = std::move(experience);
 }
 
 void character::add(spell sp)
 {
-    assert(is_published() && sp.is_draft());
+    assert(sp.is_draft());
+
+    spells_.reserve(spells_.size() + 1);
 
-    db_.spells().publish(*this, sp);
+    if (is_published())
+        sp.publish(*this);
+
     spells_.push_back(std::move(sp));
 }
 
 void character::remove(std::vector<spell>::iterator it)
 {
-    assert(is_published());
-
-    db_.spells().unpublish(*this, *it);
+    // TODO: assert 'it' is in vector.
+    it->unpublish();
     spells_.erase(it);
 }
 
-} // !server
-
-} // !mlk
+} // !mlk::server::db
--- a/libserver/malikania/server/db/character.hpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/db/character.hpp	Fri Aug 17 15:44:12 2018 +0200
@@ -28,12 +28,11 @@
 #include <array>
 #include <vector>
 
+#include "database.hpp"
 #include "model.hpp"
 #include "spell.hpp"
 
-namespace mlk {
-
-namespace server {
+namespace mlk::server::db {
 
 class account;
 
@@ -42,6 +41,8 @@
  */
 class character : public model {
 public:
+    friend class account;
+
     /**
      * \brief Stats index in arrays
      * \see set_levels
@@ -58,94 +59,57 @@
 
 private:
     std::string nickname_;
-    std::string classname_;
+    std::string type_;
     std::vector<spell> spells_;
     std::array<std::uint8_t, 5> levels_{1U, 1U, 1U, 1U, 1U};
     std::array<std::uint8_t, 5> factors_{20U, 20U, 20U, 20U, 20U};
     std::array<std::uint32_t, 5> exp_{0U, 0U, 0U, 0U, 0U};
 
+    static auto load(const result&, int) -> character;
+    static auto load(const account&) -> std::vector<character>;
+
+    void clear();
+    void publish(account&);
+    void unpublish();
+
 public:
     /**
      * Construct a character, no database is modified yet.
      *
      * \pre !nickname.empty()
-     * \pre !classname.empty()
+     * \pre !type.empty()
      * \param nickname the nickname
-     * \param classname the classname
+     * \param type the type
      */
-    inline character(database& db, std::string nickname, std::string classname) noexcept
-        : model(db)
-        , nickname_(std::move(nickname))
-        , classname_(std::move(classname))
-    {
-        assert(!nickname_.empty());
-        assert(!classname_.empty());
-    }
+    character(std::string nickname, std::string type) noexcept;
 
     /**
      * Get the character nickname.
      *
      * \return the name
      */
-    inline const std::string& get_nickname() const noexcept
-    {
-        return nickname_;
-    }
+    auto get_nickname() const noexcept -> const std::string&;
 
     /**
-     * Get the character class name.
+     * Get the character type name.
      *
-     * \return the class name
+     * \return the type name
      */
-    inline const std::string& get_classname() const noexcept
-    {
-        return classname_;
-    }
+    auto get_type() const noexcept -> const std::string&;
 
     /**
      * Get the list of spells.
      *
      * \return the spells
      */
-    inline const std::vector<spell>& get_spells() const noexcept
-    {
-        return spells_;
-    }
-
-#if defined(MALIKANIA_PRIVATE)
-    /**
-     * Get the list of spells.
-     *
-     * \return the spells
-     */
-    inline std::vector<spell>& get_spells() noexcept
-    {
-        return spells_;
-    }
-#endif // !MALIKANIA_PRIVATE
+    auto get_spells() const noexcept -> const std::vector<spell>&;
 
     /**
      * Get the list of levels.
      *
      * \return the levels
      */
-    inline const std::array<std::uint8_t, 5>& get_levels() const noexcept
-    {
-        return levels_;
-    }
-
-#if defined(MALIKANIA_PRIVATE)
-    /**
-     * Get the list of levels.
-     *
-     * \return the levels
-     * \note only available if MALIKANIA_PRIVATE macro is defined
-     */
-    inline std::array<std::uint8_t, 5>& get_levels() noexcept
-    {
-        return levels_;
-    }
-#endif // !MALIKANIA_PRIVATE
+    auto get_levels() const noexcept -> const std::array<std::uint8_t, 5>&;
 
     /**
      * Set the new levels.
@@ -159,23 +123,7 @@
      *
      * \return the factors
      */
-    inline const std::array<std::uint8_t, 5>& get_factors() const noexcept
-    {
-        return factors_;
-    }
-
-#if defined(MALIKANIA_PRIVATE)
-    /**
-     * Get the list of factors.
-     *
-     * \return the factors
-     * \note only available if MALIKANIA_PRIVATE macro is defined
-     */
-    inline std::array<std::uint8_t, 5>& get_factors() noexcept
-    {
-        return factors_;
-    }
-#endif // !MALIKANIA_PRIVATE
+    auto get_factors() const noexcept -> const std::array<std::uint8_t, 5>&;
 
     /**
      * Set progression factors.
@@ -190,23 +138,7 @@
      *
      * \return the experience
      */
-    inline const std::array<std::uint32_t, 5>& get_experience() const noexcept
-    {
-        return exp_;
-    }
-
-#if defined(MALIKANIA_PRIVATE)
-    /**
-     * Get the stats experience.
-     *
-     * \return the experience
-     * \note only available if MALIKANIA_PRIVATE macro is defined
-     */
-    inline std::array<std::uint32_t, 5>& get_experience() noexcept
-    {
-        return exp_;
-    }
-#endif // !MALIKANIA_PRIVATE
+    auto get_experience() const noexcept -> const std::array<std::uint32_t, 5>&;
 
     /**
      * Set stats experience.
@@ -234,78 +166,6 @@
     void remove(std::vector<spell>::iterator it);
 };
 
-/**
- * \brief Character DAO.
- */
-class character_dao {
-public:
-    /**
-     * Default constructor.
-     */
-    character_dao() = default;
-
-    /**
-     * Virtual destructor defaulted.
-     */
-    virtual ~character_dao() = default;
-
-    /**
-     * Save this character.
-     *
-     * The implementation must save the character and update the id, account_id
-     * member variables.
-     *
-     * Then it will be added into account.characters_ variable.
-     *
-     * \param account the parent account
-     * \param ch this character
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void publish(character& ch, const account& account) = 0;
-
-    /**
-     * Remove this character.
-     *
-     * The implementation must remove the character from the database and update
-     * id, account_id member variables.
-     *
-     * \param ch this character
-     */
-    virtual void unpublish(const character& ch) = 0;
-
-    /**
-     * Update the character levels in database.
-     *
-     * \param ch the character
-     * \param levels the new levels
-     * \note called from character::set_levels helper
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void set_levels(const character& ch, const std::array<std::uint8_t, 5>& levels) = 0;
-
-    /**
-     * Update the character progression factors in database.
-     *
-     * \param ch the character
-     * \param factors the new factors
-     * \note called from character::set_levels helper
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void set_factors(const character& ch, const std::array<std::uint8_t, 5>& levels) = 0;
-
-    /**
-     * Update the current level experiences in database.
-     *
-     * \param ch the character
-     * \param experience the new experience
-     * \note called from character::set_levels helper
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void set_experience(const character& ch, const std::array<std::uint32_t, 5>& levels) = 0;
-};
-
-} // !server
-
-} // !mlk
+} // !mlk::server::db
 
 #endif // !MALIKANIA_SERVER_DB_CHARACTER_HPP
--- a/libserver/malikania/server/db/database.cpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/db/database.cpp	Fri Aug 17 15:44:12 2018 +0200
@@ -1,10 +1,210 @@
-#include <cstdlib>
-#include <stdexcept>
+/*
+ * database.cpp -- connection to postgresql database
+ *
+ * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <cassert>
+#include <sstream>
 
 #include "database.hpp"
 
-namespace db {
+namespace mlk::server::db {
+
+namespace {
+
+struct strify {
+    template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
+    auto operator()(T i) const -> std::string
+    {
+        return std::to_string(i);
+    }
+
+    auto operator()(bool v) const -> std::string
+    {
+        return v ? "t" : "f";
+    }
+
+    auto operator()(std::string s) const -> std::string
+    {
+        return s;
+    }
+};
+
+std::unique_ptr<PGconn, void (*)(PGconn*)> connection{nullptr, nullptr};
+
+auto run(const std::string& sql, std::vector<arg> args = {}) -> result
+{
+    std::vector<const char*> list;
+
+    /*
+     * PQexecParams requires an array of C strings, convert all arguments in the
+     * args vector to string and then keep the C string value into a temporary
+     * array.
+     */
+    for (auto& arg : args) {
+        arg = std::visit(strify(), arg);
+        list.push_back(std::get<std::string>(arg).c_str());
+    }
 
+    return {
+        PQexecParams(connection.get(), sql.c_str(), list.size(), nullptr,
+            list.data(), nullptr, nullptr, 0
+        ),
+        PQclear
+    };
+}
+
+} // !namespace
+
+void open(const std::string& host,
+          const std::string& port,
+          const std::string& user,
+          const std::string& database,
+          const std::string& password)
+{
+    assert(!connection);
+
+    std::ostringstream oss;
+
+    if (!host.empty())
+        oss << "host=" << host << " ";
+    if (!port.empty())
+        oss << "port=" << port << " ";
+    if (!user.empty())
+        oss << "user=" << user << " ";
+    if (!database.empty())
+        oss << "dbname=" << database << " ";
+    if (!password.empty())
+        oss << "password=" << password;
+
+    connection = { PQconnectdb(oss.str().c_str()), PQfinish };
+
+    if (PQstatus(connection.get()) != CONNECTION_OK)
+        throw std::runtime_error(PQerrorMessage(connection.get()));
+}
+
+const std::string init_account(
+    "CREATE TABLE IF NOT EXISTS account("
+    "  id SERIAL,"
+    "  login TEXT NOT NULL,"
+    "  password TEXT NOT NULL,"
+    "  firstname TEXT,"
+    "  lastname TEXT,"
+    "  email TEXT,"
+    "  PRIMARY KEY(id)"
+    ")"
+);
 
+const std::string init_character(
+    "CREATE TABLE IF NOT EXISTS character("
+    "  id SERIAL,"
+    "  account_id INTEGER NOT NULL,"
+    "  nickname TEXT NOT NULL,"
+    "  type TEXT NOT NULL,"
+    "  hp_level SMALLINT NOT NULL DEFAULT 1,"
+    "  hp_factor SMALLINT NOT NULL,"
+    "  hp_exp INT NOT NULL,"
+    "  force_level SMALLINT NOT NULL DEFAULT 1,"
+    "  force_factor SMALLINT NOT NULL,"
+    "  force_exp INT NOT NULL,"
+    "  defense_level SMALLINT NOT NULL DEFAULT 1,"
+    "  defense_factor SMALLINT NOT NULL,"
+    "  defense_exp INT NOT NULL,"
+    "  agility_level SMALLINT NOT NULL DEFAULT 1,"
+    "  agility_factor SMALLINT NOT NULL,"
+    "  agility_exp INT NOT NULL,"
+    "  luck_level SMALLINT NOT NULL DEFAULT 1,"
+    "  luck_factor SMALLINT NOT NULL,"
+    "  luck_exp INT NOT NULL,"
+    "  PRIMARY KEY(id),"
+    "  FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE"
+    ")"
+);
 
-} // !db
+const std::string init_spell(
+    "CREATE TABLE IF NOT EXISTS spell("
+    "  id SERIAL,"
+    "  character_id INTEGER NOT NULL,"
+    "  type TEXT NOT NULL,"
+    "  level SMALLINT NOT NULL,"
+    "  PRIMARY KEY(id),"
+    "  FOREIGN KEY (character_id) REFERENCES character(id) ON DELETE CASCADE"
+    ")"
+);
+
+void init()
+{
+    assert(connection);
+
+    exec(init_account);
+    exec(init_character);
+    exec(init_spell);
+}
+
+auto select(const std::string& sql, const std::vector<arg>& args) -> result
+{
+    assert(connection);
+
+    auto result = run(sql, args);
+
+    switch (PQresultStatus(result.get())) {
+    case PGRES_COMMAND_OK:
+    case PGRES_TUPLES_OK:
+        break;
+    default:
+        throw std::runtime_error(PQerrorMessage(connection.get()));
+    }
+
+    return result;
+}
+
+void exec(const std::string& sql, const std::vector<arg>& args)
+{
+    assert(connection);
+
+    switch (const auto result = run(sql, args); PQresultStatus(result.get())) {
+    case PGRES_COMMAND_OK:
+        break;
+    default:
+        throw std::runtime_error(PQerrorMessage(connection.get()));
+    }
+}
+
+transaction::transaction(rollback fn)
+    : rollback_(std::move(fn))
+{
+    assert(rollback_);
+
+    exec("BEGIN");
+}
+
+transaction::~transaction()
+{
+    if (!commit_) {
+        rollback_();
+        exec("ROLLBACK");
+    }
+}
+
+void transaction::commit()
+{
+    if (!commit_) {
+        exec("COMMIT");
+        commit_ = true;
+    }
+}
+
+} // !db::server::mlk
--- a/libserver/malikania/server/db/database.hpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/db/database.hpp	Fri Aug 17 15:44:12 2018 +0200
@@ -1,5 +1,5 @@
 /*
- * database.hpp -- abstract database interface
+ * database.hpp -- connection to postgresql database
  *
  * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
  *
@@ -24,50 +24,128 @@
  * \brief Abstract database interface.
  */
 
-namespace mlk {
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <variant>
+#include <vector>
 
-namespace server {
+#include <libpq-fe.h>
+
+namespace mlk::server::db {
 
-class account_dao;
-class character_dao;
-class spell_dao;
+/**
+ * \brief Statement argument.
+ *
+ * When using prepared statement, you may pass any data you like using this
+ * variant.
+ */
+using arg = std::variant<
+    bool,
+    double,
+    std::int8_t,
+    std::uint8_t,
+    std::int16_t,
+    std::uint16_t,
+    std::int32_t,
+    std::uint32_t,
+    std::int64_t,
+    std::uint64_t,
+    std::string
+>;
+
+/**
+ * \brief Convenient result type.
+ *
+ * This unique pointer holds the PostgreSQL result and free it automatically on
+ * out of scope.
+ */
+using result = std::unique_ptr<PGresult, void (*)(PGresult*)>;
 
 /**
- * \brief Abstract database interface.
+ * Open the connection to the database.
+ *
+ * All arguments are optional.
+ *
+ * \pre connection must be closed
+ * \param host the hostname
+ * \param port the port number
+ * \param user the user name
+ * \param database the database name
+ * \param password the user password
+ * \throw std::runtime_error on errors
+ */
+void open(const std::string& host,
+          const std::string& port,
+          const std::string& user,
+          const std::string& database,
+          const std::string& password);
+
+/**
+ * Unconditionally init the database.
+ *
+ * \throw std::runtime_error on errors
  */
-class database {
+void init();
+
+/**
+ * Execute a SELECT statement.
+ *
+ * \param sql the SQL statement
+ * \param args the optional arguments to bind
+ * \return the result value
+ * \throw std::runtime_error on errors
+ */
+auto select(const std::string& sql, const std::vector<arg>& args = {}) -> result;
+
+/**
+ * Execute an UPDATE statement.
+ *
+ * \param sql the SQL statement
+ * \param args the optional arguments to bind
+ * \throw std::runtime_error on errors
+ */
+void exec(const std::string& sql, const std::vector<arg>& args = {});
+
+class transaction {
 public:
     /**
-     * Default constructor.
+     * \brief Rollback function.
      */
-    database() noexcept = default;
+    using rollback = std::function<void ()>;
+
+private:
+    rollback rollback_;
+    bool commit_{false};
 
+public:
     /**
-     * Virtual destructor defaulted.
+     * Start a transaction.
+     *
+     * If there is already a transaction, the returned object is no-op.
+     *
+     * \param rollback the rollback function
+     * \return the transaction object
      */
-    virtual ~database() noexcept = default;
+    transaction(rollback fn);
 
     /**
-     * Initialize database.
+     * Rollback the transaction if commit() has not been called.
      */
-    virtual void init()
-    {
-    }
+    ~transaction();
 
     /**
-     * Create the account dao.
+     * Commit the transaction.
      *
-     * \return the account dao
+     * Does nothing if already committed.
+     *
+     * \throw std::exception on errors
      */
-    virtual account_dao& accounts() = 0;
-
-    virtual character_dao& characters() = 0;
-
-    virtual spell_dao& spells() = 0;
+    void commit();
 };
 
-} // !server
-
-} // !mlk
+} // !mlk::server::db
 
 #endif // !MALIKANIA_SERVER_DB_DATABASE_HPP
--- a/libserver/malikania/server/db/dynlib_database.cpp	Mon Aug 06 13:01:00 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,88 +0,0 @@
-/*
- * dynlib_database.cpp -- dynamically loadable database adapter
- *
- * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include "dynlib_database.hpp"
-
-namespace mlk {
-
-namespace server {
-
-namespace {
-
-using db_open_t = void (const dynlib_database::params_t&);
-using db_init_t = void ();
-using db_close_t = void ();
-
-template <typename DAO>
-using db_dao_t = std::unique_ptr<DAO>(std::shared_ptr<database>);
-
-const std::string account_dao_name("mlk_account_dao");
-const std::string character_dao_name("mlk_character_dao");
-const std::string spell_dao_name("mlk_spell_dao");
-
-} // !namespace
-
-void dynlib_database::open(const params_t& parameters)
-{
-    lib_.get<db_open_t>("mlk_db_open")(parameters);
-}
-
-void dynlib_database::init()
-{
-    lib_.get<db_init_t>("mlk_db_init")();
-}
-
-void dynlib_database::close()
-{
-    lib_.get<db_close_t>("mlk_db_close")();
-}
-
-dynlib_database::dynlib_database(const std::string& path, const params_t& params)
-    : lib_(path)
-{
-    open(params);
-    init();
-}
-
-dynlib_database::~dynlib_database()
-{
-    close();
-}
-
-#if 0
-
-std::unique_ptr<class account_dao> dynlib_database::account_dao()
-{
-    return lib_.get<db_dao_t<class account_dao>>(account_dao_name)(shared_from_this());
-}
-
-std::unique_ptr<class character_dao> dynlib_database::character_dao()
-{
-    return lib_.get<db_dao_t<class character_dao>>(character_dao_name)(shared_from_this());
-}
-
-std::unique_ptr<class spell_dao> dynlib_database::spell_dao()
-{
-    return lib_.get<db_dao_t<class spell_dao>>(spell_dao_name)(shared_from_this());
-}
-
-#endif
-
-} // !server
-
-} // !mlk
--- a/libserver/malikania/server/db/dynlib_database.hpp	Mon Aug 06 13:01:00 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-/*
- * dynlib_database.hpp -- dynamically loadable database adapter
- *
- * Copyright (c) 2013-2018 David Demelier <markand@malikania.fr>
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#ifndef MALIKANIA_SERVER_DB_DYNLIB_DATABASE_HPP
-#define MALIKANIA_SERVER_DB_DYNLIB_DATABASE_HPP
-
-#include <string>
-#include <unordered_map>
-
-#include <boost/dll.hpp>
-
-#include "database.hpp"
-
-namespace mlk {
-
-namespace server {
-
-class dynlib_database : public database {
-public:
-    using params_t = std::unordered_map<std::string, std::string>;
-
-private:
-    boost::dll::shared_library lib_;
-
-    void open(const params_t& params);
-    void init();
-    void close();
-
-public:
-    dynlib_database(const std::string& path, const params_t& params);
-
-    ~dynlib_database() override;
-
-    inline const boost::dll::shared_library& lib() const noexcept
-    {
-        return lib_;
-    }
-
-    inline boost::dll::shared_library& lib() noexcept
-    {
-        return lib_;
-    }
-
-#if 0
-
-    std::unique_ptr<class account_dao> account_dao() override;
-
-    std::unique_ptr<class character_dao> character_dao() override;
-
-    std::unique_ptr<class spell_dao> spell_dao() override;
-#endif
-};
-
-} // !server
-
-} // !mlk
-
-#endif // !MALIKANIA_SERVER_DB_DYNLIB_DATABASE_HPP
--- a/libserver/malikania/server/db/model.hpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/db/model.hpp	Fri Aug 17 15:44:12 2018 +0200
@@ -26,32 +26,30 @@
 
 #include <cstdint>
 
-namespace mlk {
-
-namespace server {
-
-class database;
+namespace mlk::server::db {
 
 /**
  * \brief Abstract database object.
  */
 class model {
+public:
+    /**
+     * Id type.
+     */
+    using id_type = std::uint64_t;
+
 private:
     model(const model&) = delete;
     model& operator=(const model&) = delete;
 
 protected:
-    database& db_;                      //!< database object
-    std::uint64_t id_{0U};              //!< object id
+    id_type id_{0U};              //!< object id
 
 public:
     /**
      * Default constructor.
      */
-    inline model(database& db) noexcept
-        : db_(db)
-    {
-    }
+    model() = default;
 
     /**
      * Move constructor.
@@ -59,8 +57,7 @@
      * \param other the original value (id resets to 0)
      */
     inline model(model&& other) noexcept
-        : db_(other.db_)
-        , id_(other.id_)
+        : id_(other.id_)
     {
         other.id_ = 0U;
     }
@@ -75,11 +72,12 @@
      *
      * \return the id
      */
-    inline std::uint64_t get_id() const noexcept
+    auto get_id() const noexcept -> id_type
     {
         return id_;
     }
 
+#if 0
 #if defined(MALIKANIA_PRIVATE)
     /**
      * Set the internal id.
@@ -91,18 +89,19 @@
      * \warning only use this function in database backends
      * \param id the new id
      */
-    inline void set_id(std::uint64_t id) noexcept
+    inline void set_id(id_type id) noexcept
     {
         id_ = id;
     }
 #endif
+#endif
 
     /**
      * Tells if the object is not persistent.
      *
      * \return true if object was never saved
      */
-    inline bool is_draft() const noexcept
+    auto is_draft() const noexcept -> bool
     {
         return id_ == 0U;
     }
@@ -112,7 +111,7 @@
      *
      * \return true if object is saved
      */
-    inline bool is_published() const noexcept
+    auto is_published() const noexcept -> bool
     {
         return id_ > 0U;
     }
@@ -123,7 +122,7 @@
      * \param other the original value (id resets to 0)
      * \return *this
      */
-    inline model& operator=(model&& other) noexcept
+    auto operator=(model&& other) noexcept -> model&
     {
         id_ = other.id_;
         other.id_ = 0U;
@@ -132,8 +131,6 @@
     }
 };
 
-} // !server
-
-} // !mlk
+} // !mlk::server::db
 
 #endif // !MODEL_HPP
--- a/libserver/malikania/server/db/spell.cpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/db/spell.cpp	Fri Aug 17 15:44:12 2018 +0200
@@ -16,22 +16,109 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include "database.hpp"
+#include <cassert>
+
 #include "character.hpp"
 #include "spell.hpp"
 
-namespace mlk {
+namespace mlk::server::db {
+
+auto spell::load(const result& result, int row) -> spell
+{
+    spell s(PQgetvalue(result.get(), row, 2));
+
+    s.id_ = std::stoi(PQgetvalue(result.get(), row, 0));
+    s.level_ = std::stoi(PQgetvalue(result.get(), row, 3));
+
+    return s;
+}
+
+auto spell::load(const character& parent) -> std::vector<spell>
+{
+    static const std::string sql(
+        "SELECT *"
+        "  FROM spell"
+        " WHERE id = $1"
+    );
+
+    const auto r = select(sql, { parent.get_id() });
+
+    if (PQntuples(r.get()) == 0)
+        throw std::runtime_error("failed to load spells");
+
+    std::vector<spell> spells;
+
+    for (int i = 0; i < PQntuples(r.get()); ++i)
+        spells.push_back(load(r, i));
+
+    return spells;
+}
+
+void spell::clear()
+{
+    id_ = 0U;
+}
 
-namespace server {
+void spell::unpublish()
+{
+    static const std::string sql(
+        "DELETE"
+        "  FROM spell"
+        " WHERE id = $1"
+    );
+
+    exec(sql, { id_ });
+    clear();
+}
+
+void spell::publish(character& parent)
+{
+    static const std::string sql(
+        "INSERT INTO spell("
+        "  character_id,"
+        "  type,"
+        "  level"
+        ") "
+        "VALUES ($1, $2, $3) "
+        "RETURNING id"
+    );
+
+    const auto r = select(sql, { parent.get_id(), type_, level_ });
+
+    if (PQntuples(r.get()) == 0)
+        throw std::runtime_error("failed to save spell");
+
+    id_ = std::stoi(PQgetvalue(r.get(), 0, 0));
+}
+
+spell::spell(std::string type) noexcept
+    : type_(std::move(type))
+{
+    assert(!type_.empty());
+}
+
+auto spell::get_type() const noexcept -> const std::string&
+{
+    return type_;
+}
+
+auto spell::get_level() const noexcept -> std::uint8_t
+{
+    return level_;
+}
 
 void spell::set_level(std::uint8_t level)
 {
+    static const std::string sql(
+        "UPDATE spell"
+        "   SET level = $1"
+        " WHERE id = $2"
+    );
+
     if (is_published() && level_ != level)
-        db_.spells().set_level(level);
+        db::exec(sql, { level_, id_ });
 
     level_ = level;
 }
 
-} // !server
-
-} // !mlk
+} // !mlk::server::db
--- a/libserver/malikania/server/db/spell.hpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/db/spell.hpp	Fri Aug 17 15:44:12 2018 +0200
@@ -24,14 +24,14 @@
  * \brief Database spell object.
  */
 
-#include <cassert>
+#include <cstdint>
 #include <string>
+#include <vector>
 
+#include "database.hpp"
 #include "model.hpp"
 
-namespace mlk {
-
-namespace server {
+namespace mlk::server::db {
 
 class character;
 
@@ -40,32 +40,40 @@
  */
 class spell : public model {
 private:
-    std::string classname_;
-    std::uint8_t level_{1};
+    friend class character;
+
+    std::string type_;
+    std::uint8_t level_{1U};
+
+    static auto load(const result&, int) -> spell;
+    static auto load(const character&) -> std::vector<spell>;
+
+    void clear();
+    void unpublish();
+    void publish(character&);
 
 public:
     /**
      * Constructor.
      *
-     * \pre !classname.empty()
-     * \param classname the class name
+     * \pre !type.empty()
+     * \param type the type name
      */
-    inline spell(database& db, std::string classname)
-        : model(db)
-        , classname_(std::move(classname))
-    {
-        assert(!classname_.empty());
-    }
+    spell(std::string type) noexcept;
+
+    /**
+     * Get the type.
+     *
+     * \return the type
+     */
+    auto get_type() const noexcept -> const std::string&;
 
     /**
      * Get the level.
      *
      * \return the level
      */
-    inline std::uint8_t level() const noexcept
-    {
-        return level_;
-    }
+    auto get_level() const noexcept -> std::uint8_t;
 
     /**
      * Set the spell level.
@@ -75,53 +83,6 @@
     void set_level(std::uint8_t level);
 };
 
-/**
- * \brief Character DAO.
- */
-class spell_dao {
-public:
-    /**
-     * Default constructor.
-     */
-    spell_dao() = default;
-
-    /**
-     * Virtual destructor defaulted.
-     */
-    virtual ~spell_dao() = default;
-
-    /**
-     * Save this spell.
-     *
-     * The implementation must save the spell and update id, character_id
-     * member variables.
-     *
-     * Then it will be added into character.spells_ variable.
-     */
-    virtual void publish(const character& ch, spell& sp) = 0;
-
-    /**
-     * Remove this spell.
-     *
-     * The implementation must remove the spell from the database and update
-     * id, character_id member variables.
-     */
-    virtual void unpublish(const character& ch, const spell& sp) = 0;
-
-    /**
-     * Update the spell level in database.
-     *
-     * Only called when the level needs to be changed, the implementation does
-     * not need to update level_ field.
-     *
-     * \note called from set_level helper
-     * \throw std::exception if the operation could not succeed
-     */
-    virtual void set_level(const character& ch, std::uint8_t level) = 0;
-};
-
-} // !server
-
-} // !mlk
+} // !mlk::server::db
 
 #endif // !MALIKANIA_SERVER_DB_SPELL_HPP
--- a/libserver/malikania/server/net/auth_handler.cpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/libserver/malikania/server/net/auth_handler.cpp	Fri Aug 17 15:44:12 2018 +0200
@@ -31,6 +31,10 @@
 
 void auth_handler::exec(server& server, std::shared_ptr<client> clt, nlohmann::json object) noexcept
 {
+    (void)server;
+    (void)clt;
+    (void)object;
+#if 0
     try {
         // TODO: VERIFY ALREADY LOGGED IN!
         auto ac = server.database().accounts().authenticate(
@@ -47,6 +51,7 @@
     } catch (const std::exception& ex) {
         clt->error("auth", ex.what());
     }
+#endif
 }
 
 } // !server
--- a/server/main.cpp	Mon Aug 06 13:01:00 2018 +0200
+++ b/server/main.cpp	Fri Aug 17 15:44:12 2018 +0200
@@ -16,6 +16,38 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <iostream>
+
+#include <malikania/server/db/database.hpp>
+#include <malikania/server/db/account.hpp>
+
+using namespace mlk::server;
+using namespace mlk::server::db;
+
 int main()
 {
+    try {
+        db::open("", "", "markand", "malikaniadb", "");
+        db::exec("DROP TABLE spell");
+        db::exec("DROP TABLE character");
+        db::exec("DROP TABLE account");
+        db::init();
+
+        account a("markand", "plopation");
+
+        a.set_email("markand@malikania.fr");
+        a.set_firstname("David");
+        a.set_lastname("Demelier");
+
+        character ch("luna", "fairy");
+        spell s("heal");
+
+        ch.add(std::move(s));
+        puts("ABOUT TO ADD");
+        a.add(std::move(ch));
+        puts("ABOUT TO PUBLISH");
+        a.publish();
+    } catch (const std::exception& ex) {
+        std::cerr << "abort: " << ex.what() << std::endl;
+    }
 }