~singpolyma/biboumi

0168b96b79db2627fdba77a8712956408aa081d1 — louiz’ 4 years ago 5b27cee
Add postgresql support
M CMakeLists.txt => CMakeLists.txt +9 -0
@@ 130,6 130,12 @@ elseif(NOT WITHOUT_SQLITE3)
  find_package(SQLITE3)
endif()

if(WITH_POSTGRESQL)
  find_package(PQ REQUIRED)
elseif(NOT WITHOUT_POSTGRESQL)
  find_package(PQ)
endif()

#
## Set all the include directories, depending on what libraries are used
#


@@ 193,6 199,7 @@ if(SQLITE3_FOUND)
  add_library(database OBJECT ${source_database})

  include_directories(database ${SQLITE3_INCLUDE_DIRS})
  include_directories(database ${PQ_INCLUDE_DIRS})
  set(USE_DATABASE TRUE)
else()
  add_library(database OBJECT "")


@@ 261,7 268,9 @@ if(LIBIDN_FOUND)
endif()
if(USE_DATABASE)
  target_link_libraries(${PROJECT_NAME} ${SQLITE3_LIBRARIES})
  target_link_libraries(${PROJECT_NAME} ${PQ_LIBRARIES})
  target_link_libraries(test_suite ${SQLITE3_LIBRARIES})
  target_link_libraries(test_suite ${PQ_LIBRARIES})
endif()

# Define a __FILENAME__ macro with the relative path (from the base project directory)

M INSTALL.rst => INSTALL.rst +7 -4
@@ 32,10 32,12 @@ libiconv_
libuuid_
 Generate unique IDs

sqlite3_ (option, but highly recommended)
 Provides a way to store various options in a (sqlite3) database. Each user
 of the gateway can store their own values (for example their prefered port,
 or their IRC password). Without this dependency, many interesting features
sqlite3_
or
libpq_
 Provides a way to store various options in a database. Each user of the
 gateway can store their own values (for example their prefered port, or
 their IRC password). Without this dependency, many interesting features
 are missing.

libidn_ (optional, but recommended)


@@ 165,3 167,4 @@ to use biboumi.
.. _systemd: https://www.freedesktop.org/wiki/Software/systemd/
.. _biboumi.1.rst: doc/biboumi.1.rst
.. _gcrypt: https://www.gnu.org/software/libgcrypt/
.. _libpq: https://www.postgresql.org/docs/current/static/libpq.html

A cmake/Modules/FindPQ.cmake => cmake/Modules/FindPQ.cmake +43 -0
@@ 0,0 1,43 @@
# - Find libpq
# Find the postgresql front end library
#
# This module defines the following variables:
#   PQ_FOUND  -  True if library and include directory are found
# If set to TRUE, the following are also defined:
#   PQ_INCLUDE_DIRS  -  The directory where to find the header file
#   PQ_LIBRARIES  -  Where to find the library file
#
# For conveniance, these variables are also set. They have the same values
# than the variables above.  The user can thus choose his/her prefered way
# to write them.
#   PQ_LIBRARY
#   PQ_INCLUDE_DIR
#
# This file is in the public domain

include(FindPkgConfig)

if(NOT PQ_FOUND)
  pkg_check_modules(PQ libpq)
endif()

if(NOT PQ_FOUND)
  find_path(PQ_INCLUDE_DIRS NAMES libpq-fe.h
      DOC "The libpq include directory")

  find_library(PQ_LIBRARIES NAMES pq
      DOC "The pq library")

  # Use some standard module to handle the QUIETLY and REQUIRED arguments, and
  # set PQ_FOUND to TRUE if these two variables are set.
  include(FindPackageHandleStandardArgs)
  find_package_handle_standard_args(PQ REQUIRED_VARS PQ_LIBRARIES PQ_INCLUDE_DIRS)

  if(PQ_FOUND)
    set(PQ_LIBRARY ${PQ_LIBRARIES} CACHE INTERNAL "")
    set(PQ_INCLUDE_DIR ${PQ_INCLUDE_DIRS} CACHE INTERNAL "")
    set(PQ_FOUND ${PQ_FOUND} CACHE INTERNAL "")
  endif()
endif()

mark_as_advanced(PQ_INCLUDE_DIRS PQ_LIBRARIES)

M src/database/column.hpp => src/database/column.hpp +7 -2
@@ 13,5 13,10 @@ struct Column
    T value{};
};

struct Id: Column<std::size_t> { static constexpr auto name = "id_";
                                 static constexpr auto options = "PRIMARY KEY AUTOINCREMENT"; };
struct Id: Column<std::size_t> {
    static constexpr std::size_t unset_value = static_cast<std::size_t>(-1);
    static constexpr auto name = "id_";
    static constexpr auto options = "PRIMARY KEY";

    Id(): Column<std::size_t>(-1) {}
};

M src/database/count_query.hpp => src/database/count_query.hpp +5 -7
@@ 2,6 2,7 @@

#include <database/query.hpp>
#include <database/table.hpp>
#include <database/statement.hpp>

#include <string>



@@ 15,20 16,17 @@ struct CountQuery: public Query
      this->body += std::move(name);
    }

    int64_t execute(sqlite3* db)
    int64_t execute(DatabaseEngine& db)
    {
      auto statement = this->prepare(db);
      auto statement = db.prepare(this->body);
      int64_t res = 0;
      if (sqlite3_step(statement.get()) == SQLITE_ROW)
        res = sqlite3_column_int64(statement.get(), 0);
      if (statement->step() != StepResult::Error)
        res = statement->get_column_int64(0);
      else
        {
          log_error("Count request didn’t return a result");
          return 0;
        }
      if (sqlite3_step(statement.get()) != SQLITE_DONE)
        log_warning("Count request returned more than one result.");

      return res;
    }
};

M src/database/database.cpp => src/database/database.cpp +39 -36
@@ 7,16 7,21 @@
#include <utils/time.hpp>

#include <config/config.hpp>
#include <database/sqlite3_engine.hpp>
#include <database/postgresql_engine.hpp>

#include <database/engine.hpp>
#include <database/index.hpp>

#include <memory>

#include <sqlite3.h>

sqlite3* Database::db;
Database::MucLogLineTable Database::muc_log_lines("MucLogLine_");
Database::GlobalOptionsTable Database::global_options("GlobalOptions_");
Database::IrcServerOptionsTable Database::irc_server_options("IrcServerOptions_");
Database::IrcChannelOptionsTable Database::irc_channel_options("IrcChannelOptions_");
std::unique_ptr<DatabaseEngine> Database::db;
Database::MucLogLineTable Database::muc_log_lines("muclogline_");
Database::GlobalOptionsTable Database::global_options("globaloptions_");
Database::IrcServerOptionsTable Database::irc_server_options("ircserveroptions_");
Database::IrcChannelOptionsTable Database::irc_channel_options("ircchanneloptions_");
Database::RosterTable Database::roster("roster");
std::map<Database::CacheKey, Database::EncodingIn::real_type> Database::encoding_in_cache{};



@@ 29,27 34,26 @@ void Database::open(const std::string& filename)
  // Try to open the specified database.
  // Close and replace the previous database pointer if it succeeded. If it did
  // not, just leave things untouched
  sqlite3* new_db;
  auto res = sqlite3_open_v2(filename.data(), &new_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
  Database::close();
  if (res != SQLITE_OK)
    {
      log_error("Failed to open database file ", filename, ": ", sqlite3_errmsg(new_db));
      sqlite3_close(new_db);
      throw std::runtime_error("");
    }
  Database::db = new_db;
  Database::muc_log_lines.create(Database::db);
  Database::muc_log_lines.upgrade(Database::db);
  Database::global_options.create(Database::db);
  Database::global_options.upgrade(Database::db);
  Database::irc_server_options.create(Database::db);
  Database::irc_server_options.upgrade(Database::db);
  Database::irc_channel_options.create(Database::db);
  Database::irc_channel_options.upgrade(Database::db);
  Database::roster.create(Database::db);
  Database::roster.upgrade(Database::db);
  create_index<Database::Owner, Database::IrcChanName, Database::IrcServerName>(Database::db, "archive_index", Database::muc_log_lines.get_name());
  std::unique_ptr<DatabaseEngine> new_db;
  static const auto psql_prefix = "postgresql://"s;
  if (filename.substr(0, psql_prefix.size()) == psql_prefix)
    new_db = PostgresqlEngine::open("dbname="s + filename.substr(psql_prefix.size()));
  else
    new_db = Sqlite3Engine::open(filename);
  if (!new_db)
    return;
  Database::db = std::move(new_db);
  Database::muc_log_lines.create(*Database::db);
  Database::muc_log_lines.upgrade(*Database::db);
  Database::global_options.create(*Database::db);
  Database::global_options.upgrade(*Database::db);
  Database::irc_server_options.create(*Database::db);
  Database::irc_server_options.upgrade(*Database::db);
  Database::irc_channel_options.create(*Database::db);
  Database::irc_channel_options.upgrade(*Database::db);
  Database::roster.create(*Database::db);
  Database::roster.upgrade(*Database::db);
  create_index<Database::Owner, Database::IrcChanName, Database::IrcServerName>(*Database::db, "archive_index", Database::muc_log_lines.get_name());
}




@@ 59,7 63,7 @@ Database::GlobalOptions Database::get_global_options(const std::string& owner)
  request.where() << Owner{} << "=" << owner;

  Database::GlobalOptions options{Database::global_options.get_name()};
  auto result = request.execute(Database::db);
  auto result = request.execute(*Database::db);
  if (result.size() == 1)
    options = result.front();
  else


@@ 73,7 77,7 @@ Database::IrcServerOptions Database::get_irc_server_options(const std::string& o
  request.where() << Owner{} << "=" << owner << " and " << Server{} << "=" << server;

  Database::IrcServerOptions options{Database::irc_server_options.get_name()};
  auto result = request.execute(Database::db);
  auto result = request.execute(*Database::db);
  if (result.size() == 1)
    options = result.front();
  else


@@ 91,7 95,7 @@ Database::IrcChannelOptions Database::get_irc_channel_options(const std::string&
          " and " << Server{} << "=" << server <<\
          " and " << Channel{} << "=" << channel;
  Database::IrcChannelOptions options{Database::irc_channel_options.get_name()};
  auto result = request.execute(Database::db);
  auto result = request.execute(*Database::db);
  if (result.size() == 1)
    options = result.front();
  else


@@ 186,7 190,7 @@ std::vector<Database::MucLogLine> Database::get_muc_logs(const std::string& owne
  if (limit >= 0)
    request.limit() << limit;

  auto result = request.execute(Database::db);
  auto result = request.execute(*Database::db);

  return {result.crbegin(), result.crend()};
}


@@ 207,7 211,7 @@ void Database::delete_roster_item(const std::string& local, const std::string& r
  query << " WHERE " << Database::RemoteJid{} << "=" << remote << \
           " AND " << Database::LocalJid{} << "=" << local;

  query.execute(Database::db);
//  query.execute(*Database::db);
}

bool Database::has_roster_item(const std::string& local, const std::string& remote)


@@ 216,7 220,7 @@ bool Database::has_roster_item(const std::string& local, const std::string& remo
  query.where() << Database::LocalJid{} << "=" << local << \
        " and " << Database::RemoteJid{} << "=" << remote;

  auto res = query.execute(Database::db);
  auto res = query.execute(*Database::db);

  return !res.empty();
}


@@ 226,20 230,19 @@ std::vector<Database::RosterItem> Database::get_contact_list(const std::string& 
  auto query = Database::roster.select();
  query.where() << Database::LocalJid{} << "=" << local;

  return query.execute(Database::db);
  return query.execute(*Database::db);
}

std::vector<Database::RosterItem> Database::get_full_roster()
{
  auto query = Database::roster.select();

  return query.execute(Database::db);
  return query.execute(*Database::db);
}

void Database::close()
{
  sqlite3_close(Database::db);
  Database::db = nullptr;
  Database::db.release();
}

std::string Database::gen_uuid()

M src/database/database.hpp => src/database/database.hpp +20 -13
@@ 7,6 7,8 @@
#include <database/column.hpp>
#include <database/count_query.hpp>

#include <database/engine.hpp>

#include <utils/optional_bool.hpp>

#include <chrono>


@@ 25,11 27,11 @@ class Database

  struct Owner: Column<std::string> { static constexpr auto name = "owner_"; };

  struct IrcChanName: Column<std::string> { static constexpr auto name = "ircChanName_"; };
  struct IrcChanName: Column<std::string> { static constexpr auto name = "ircchanname_"; };

  struct Channel: Column<std::string> { static constexpr auto name = "channel_"; };

  struct IrcServerName: Column<std::string> { static constexpr auto name = "ircServerName_"; };
  struct IrcServerName: Column<std::string> { static constexpr auto name = "ircservername_"; };

  struct Server: Column<std::string> { static constexpr auto name = "server_"; };



@@ 44,30 46,30 @@ class Database
  struct Ports: Column<std::string> { static constexpr auto name = "ports_";
    Ports(): Column<std::string>("6667") {} };

  struct TlsPorts: Column<std::string> { static constexpr auto name = "tlsPorts_";
  struct TlsPorts: Column<std::string> { static constexpr auto name = "tlsports_";
    TlsPorts(): Column<std::string>("6697;6670") {} };

  struct Username: Column<std::string> { static constexpr auto name = "username_"; };

  struct Realname: Column<std::string> { static constexpr auto name = "realname_"; };

  struct AfterConnectionCommand: Column<std::string> { static constexpr auto name = "afterConnectionCommand_"; };
  struct AfterConnectionCommand: Column<std::string> { static constexpr auto name = "afterconnectioncommand_"; };

  struct TrustedFingerprint: Column<std::string> { static constexpr auto name = "trustedFingerprint_"; };
  struct TrustedFingerprint: Column<std::string> { static constexpr auto name = "trustedfingerprint_"; };

  struct EncodingOut: Column<std::string> { static constexpr auto name = "encodingOut_"; };
  struct EncodingOut: Column<std::string> { static constexpr auto name = "encodingout_"; };

  struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingIn_"; };
  struct EncodingIn: Column<std::string> { static constexpr auto name = "encodingin_"; };

  struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxHistoryLength_";
  struct MaxHistoryLength: Column<int> { static constexpr auto name = "maxhistorylength_";
    MaxHistoryLength(): Column<int>(20) {} };

  struct RecordHistory: Column<bool> { static constexpr auto name = "recordHistory_";
  struct RecordHistory: Column<bool> { static constexpr auto name = "recordhistory_";
    RecordHistory(): Column<bool>(true) {}};

  struct RecordHistoryOptional: Column<OptionalBool> { static constexpr auto name = "recordHistory_"; };
  struct RecordHistoryOptional: Column<OptionalBool> { static constexpr auto name = "recordhistory_"; };

  struct VerifyCert: Column<bool> { static constexpr auto name = "verifyCert_";
  struct VerifyCert: Column<bool> { static constexpr auto name = "verifycert_";
    VerifyCert(): Column<bool>(true) {} };

  struct Persistent: Column<bool> { static constexpr auto name = "persistent_";


@@ 134,7 136,7 @@ class Database
  static int64_t count(const TableType& table)
  {
    CountQuery query{table.get_name()};
    return query.execute(Database::db);
    return query.execute(*Database::db);
  }

  static MucLogLineTable muc_log_lines;


@@ 142,7 144,7 @@ class Database
  static IrcServerOptionsTable irc_server_options;
  static IrcChannelOptionsTable irc_channel_options;
  static RosterTable roster;
  static sqlite3* db;
  static std::unique_ptr<DatabaseEngine> db;

  /**
   * Some caches, to avoid doing very frequent query requests for a few options.


@@ 177,6 179,11 @@ class Database
    Database::encoding_in_cache.clear();
  }

  static auto raw_exec(const std::string& query)
  {
    Database::db->raw_exec(query);
  }

 private:
  static std::string gen_uuid();
  static std::map<CacheKey, EncodingIn::real_type> encoding_in_cache;

A src/database/engine.hpp => src/database/engine.hpp +40 -0
@@ 0,0 1,40 @@
#pragma once

/**
 * Interface to provide non-portable behaviour, specific to each
 * database engine we want to support.
 *
 * Everything else (all portable stuf) should go outside of this class.
 */

#include <database/statement.hpp>

#include <memory>
#include <string>
#include <vector>
#include <tuple>
#include <set>

class DatabaseEngine
{
 public:

  DatabaseEngine() = default;

  DatabaseEngine(const DatabaseEngine&) = delete;
  DatabaseEngine& operator=(const DatabaseEngine&) = delete;
  DatabaseEngine(DatabaseEngine&&) = delete;
  DatabaseEngine& operator=(DatabaseEngine&&) = delete;

  virtual std::set<std::string> get_all_columns_from_table(const std::string& table_name) = 0;
  virtual std::tuple<bool, std::string> raw_exec(const std::string& query) = 0;
  virtual std::unique_ptr<Statement> prepare(const std::string& query) = 0;
  virtual void extract_last_insert_rowid(Statement& statement) = 0;
  virtual std::string get_returning_id_sql_string(const std::string&)
  {
    return {};
  }
  virtual std::string id_column_type() = 0;

  int64_t last_inserted_rowid{-1};
};

M src/database/index.hpp => src/database/index.hpp +9 -13
@@ 1,6 1,6 @@
#pragma once

#include <sqlite3.h>
#include <database/engine.hpp>

#include <string>
#include <tuple>


@@ 25,18 25,14 @@ add_column_name(std::string& out)
}

template <typename... Columns>
void create_index(sqlite3* db, const std::string& name, const std::string& table)
void create_index(DatabaseEngine& db, const std::string& name, const std::string& table)
{
  std::string res{"CREATE INDEX IF NOT EXISTS "};
  res += name + " ON " + table + "(";
  add_column_name<0, Columns...>(res);
  res += ")";
  std::string query{"CREATE INDEX IF NOT EXISTS "};
  query += name + " ON " + table + "(";
  add_column_name<0, Columns...>(query);
  query += ")";

  char* error;
  const auto result = sqlite3_exec(db, res.data(), nullptr, nullptr, &error);
  if (result != SQLITE_OK)
    {
      log_error("Error executing query: ", error);
      sqlite3_free(error);
    }
  auto result = db.raw_exec(query);
  if (std::get<0>(result) == false)
    log_error("Error executing query: ", std::get<1>(result));
}

M src/database/insert_query.hpp => src/database/insert_query.hpp +54 -49
@@ 12,62 12,63 @@

#include <sqlite3.h>

template <int N, typename ColumnType, typename... T>
typename std::enable_if<!std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
actual_bind(Statement& statement, std::vector<std::string>& params, const std::tuple<T...>&)
template <std::size_t N=0, typename... T>
typename std::enable_if<N < sizeof...(T), void>::type
update_autoincrement_id(std::tuple<T...>& columns, Statement& statement)
{
  const auto value = params.front();
  params.erase(params.begin());
  if (sqlite3_bind_text(statement.get(), N + 1, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT) != SQLITE_OK)
    log_error("Failed to bind ", value, " to param ", N);
}

template <int N, typename ColumnType, typename... T>
typename std::enable_if<std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
actual_bind(Statement& statement, std::vector<std::string>&, const std::tuple<T...>& columns)
{
  auto&& column = std::get<Id>(columns);
  if (column.value != 0)
  using ColumnType = typename std::decay<decltype(std::get<N>(columns))>::type;
  if (std::is_same<ColumnType, Id>::value)
    {
      if (sqlite3_bind_int64(statement.get(), N + 1, static_cast<sqlite3_int64>(column.value)) != SQLITE_OK)
        log_error("Failed to bind ", column.value, " to id.");
      log_debug("EXTRACTING LAST ID");
      auto&& column = std::get<Id>(columns);
    }
  else if (sqlite3_bind_null(statement.get(), N + 1) != SQLITE_OK)
    log_error("Failed to bind NULL to param ", N);
  update_autoincrement_id<N+1>(columns, statement);
}

template <std::size_t N=0, typename... T>
typename std::enable_if<N == sizeof...(T), void>::type
update_autoincrement_id(std::tuple<T...>&, Statement& statement)
{}

struct InsertQuery: public Query
{
  InsertQuery(const std::string& name):
      Query("INSERT OR REPLACE INTO ")
  template <typename... T>
  InsertQuery(const std::string& name, const std::tuple<T...>& columns):
      Query("INSERT INTO ")
  {
    this->body += name;
    this->insert_col_names(columns);
    this->insert_values(columns);
  }

  template <typename... T>
  void execute(const std::tuple<T...>& columns, sqlite3* db)
  void execute(DatabaseEngine& db, std::tuple<T...>& columns)
  {
    auto statement = this->prepare(db);
    {
      this->bind_param(columns, statement);
      if (sqlite3_step(statement.get()) != SQLITE_DONE)
        log_error("Failed to execute query: ", sqlite3_errmsg(db));
    }
    auto statement = db.prepare(this->body);
    this->bind_param(columns, *statement);

    if (statement->step() != StepResult::Error)
      db.extract_last_insert_rowid(*statement);
    else
      log_error("Failed to extract the rowid from the last INSERT");
  }

  template <int N=0, typename... T>
  typename std::enable_if<N < sizeof...(T), void>::type
  bind_param(const std::tuple<T...>& columns, Statement& statement)
  bind_param(const std::tuple<T...>& columns, Statement& statement, int index=1)
  {
    using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
    auto&& column = std::get<N>(columns);
    using ColumnType = std::decay_t<decltype(column)>;

    if (!std::is_same<ColumnType, Id>::value)
      actual_bind(statement, column.value, index++);

    actual_bind<N, ColumnType>(statement, this->params, columns);
    this->bind_param<N+1>(columns, statement);
    this->bind_param<N+1>(columns, statement, index);
  }

  template <int N=0, typename... T>
  typename std::enable_if<N == sizeof...(T), void>::type
  bind_param(const std::tuple<T...>&, Statement&)
  bind_param(const std::tuple<T...>&, Statement&, int)
  {}

  template <typename... T>


@@ 80,18 81,21 @@ struct InsertQuery: public Query

  template <int N=0, typename... T>
  typename std::enable_if<N < sizeof...(T), void>::type
  insert_value(const std::tuple<T...>& columns)
  insert_value(const std::tuple<T...>& columns, int index=1)
  {
    this->body += "?";
    if (N != sizeof...(T) - 1)
      this->body += ",";
    this->body += " ";
    add_param(*this, std::get<N>(columns));
    this->insert_value<N+1>(columns);
    using ColumnType = std::decay_t<decltype(std::get<N>(columns))>;

    if (!std::is_same<ColumnType, Id>::value)
      {
        this->body += "$" + std::to_string(index++);
        if (N != sizeof...(T) - 1)
          this->body += ", ";
      }
    this->insert_value<N+1>(columns, index);
  }
  template <int N=0, typename... T>
  typename std::enable_if<N == sizeof...(T), void>::type
  insert_value(const std::tuple<T...>&)
  insert_value(const std::tuple<T...>&, const int)
  { }

  template <typename... T>


@@ 99,27 103,28 @@ struct InsertQuery: public Query
  {
    this->body += " (";
    this->insert_col_name(columns);
    this->body += ")\n";
    this->body += ")";
  }

  template <int N=0, typename... T>
  typename std::enable_if<N < sizeof...(T), void>::type
  insert_col_name(const std::tuple<T...>& columns)
  {
    using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
    using ColumnType = std::decay_t<decltype(std::get<N>(columns))>;

    this->body += ColumnType::name;
    if (!std::is_same<ColumnType, Id>::value)
      {
        this->body += ColumnType::name;

    if (N < (sizeof...(T) - 1))
      this->body += ", ";
        if (N < (sizeof...(T) - 1))
          this->body += ", ";
      }

    this->insert_col_name<N+1>(columns);
  }

  template <int N=0, typename... T>
  typename std::enable_if<N == sizeof...(T), void>::type
  insert_col_name(const std::tuple<T...>&)
  {}


 private:
};

A src/database/postgresql_engine.cpp => src/database/postgresql_engine.cpp +77 -0
@@ 0,0 1,77 @@
#include <database/postgresql_engine.hpp>

#include <database/postgresql_statement.hpp>

#include <logger/logger.hpp>

PostgresqlEngine::PostgresqlEngine(PGconn*const conn):
    conn(conn)
{}

PostgresqlEngine::~PostgresqlEngine()
{
  PQfinish(this->conn);
}

std::unique_ptr<DatabaseEngine> PostgresqlEngine::open(const std::string& conninfo)
{
  log_debug("trying to open: ", conninfo);
  PGconn* con = PQconnectdb(conninfo.data());

  if (!con)
    {
      log_error("Failed to allocate a Postgresql connection");
      throw std::runtime_error("");
    }
  const auto status = PQstatus(con);
  if (status != CONNECTION_OK)
    {
      const char* errmsg = PQerrorMessage(con);
      log_error("Postgresql connection failed: ", errmsg);
      throw std::runtime_error("failed to open connection.");
    }
  return std::make_unique<PostgresqlEngine>(con);
}

std::set<std::string> PostgresqlEngine::get_all_columns_from_table(const std::string& table_name)
{
  const auto query = "SELECT column_name from information_schema.columns where table_name='" + table_name + "'";
  auto statement = this->prepare(query);
  std::set<std::string> columns;

  while (statement->step() == StepResult::Row)
    columns.insert(statement->get_column_text(0));

  log_debug("found ", columns.size(), " columns.");
  return columns;
}

std::tuple<bool, std::string> PostgresqlEngine::raw_exec(const std::string& query)
{
  log_debug("raw_exec:", query);
  PGresult* res = PQexec(this->conn, query.data());
  auto res_status = PQresultStatus(res);
  if (res_status != PGRES_COMMAND_OK)
    return std::make_tuple(false, PQresultErrorMessage(res));
  return std::make_tuple(true, std::string{});
}

std::unique_ptr<Statement> PostgresqlEngine::prepare(const std::string& query)
{
  return std::make_unique<PostgresqlStatement>(query, this->conn);
}

void PostgresqlEngine::extract_last_insert_rowid(Statement& statement)
{
  this->last_inserted_rowid = statement.get_column_int64(0);
}

std::string PostgresqlEngine::get_returning_id_sql_string(const std::string& col_name)
{
  return " RETURNING " + col_name;
}

std::string PostgresqlEngine::id_column_type()
{
  return "SERIAL";
}

A src/database/postgresql_engine.hpp => src/database/postgresql_engine.hpp +31 -0
@@ 0,0 1,31 @@
#pragma once

#include <database/engine.hpp>

#include <database/statement.hpp>

#include <libpq-fe.h>

#include <memory>
#include <string>
#include <tuple>
#include <set>

class PostgresqlEngine: public DatabaseEngine
{
 public:
  PostgresqlEngine(PGconn*const conn);

  ~PostgresqlEngine();

  static std::unique_ptr<DatabaseEngine> open(const std::string& string);

  std::set<std::string> get_all_columns_from_table(const std::string& table_name) override final;
  std::tuple<bool, std::string> raw_exec(const std::string& query) override final;
  std::unique_ptr<Statement> prepare(const std::string& query) override;
  void extract_last_insert_rowid(Statement& statement) override;
  std::string get_returning_id_sql_string(const std::string& col_name) override;
  std::string id_column_type() override;
private:
  PGconn* const conn;
};

A src/database/postgresql_statement.hpp => src/database/postgresql_statement.hpp +128 -0
@@ 0,0 1,128 @@
#pragma once

#include <database/statement.hpp>

#include <logger/logger.hpp>

#include <libpq-fe.h>

class PostgresqlStatement: public Statement
{
 public:
  PostgresqlStatement(std::string body, PGconn*const conn):
      body(std::move(body)),
      conn(conn)
  {}
  ~PostgresqlStatement()
  {}
  PostgresqlStatement(const PostgresqlStatement&) = delete;
  PostgresqlStatement& operator=(const PostgresqlStatement&) = delete;
  PostgresqlStatement(PostgresqlStatement&& other) = delete;
  PostgresqlStatement& operator=(PostgresqlStatement&& other) = delete;

  StepResult step() override final
  {
    if (!this->executed)
      {
        this->current_tuple = 0;
        this->executed = true;
        if (!this->execute())
          return StepResult::Error;
      }
    else
      {
        this->current_tuple++;
      }
    if (this->current_tuple < PQntuples(this->result))
      return StepResult::Row;
    return StepResult::Done;
  }

  int64_t get_column_int64(const int col) override
  {
    const char* result = PQgetvalue(this->result, this->current_tuple, col);
    std::istringstream iss;
    iss.str(result);
    int64_t res;
    iss >> res;
    return res;
  }
  std::string get_column_text(const int col) override
  {
    const char* result = PQgetvalue(this->result, this->current_tuple, col);
    return result;
  }
  int get_column_int(const int col) override
  {
    const char* result = PQgetvalue(this->result, this->current_tuple, col);
    std::istringstream iss;
    iss.str(result);
    int res;
    iss >> res;
    return res;
  }

  void bind(std::vector<std::string> params) override
  {

    this->params = std::move(params);
  }

  bool bind_text(const int, const std::string& data) override
  {
    this->params.push_back(data);
    return true;
  }
  bool bind_int64(const int pos, const std::int64_t value) override
  {
    this->params.push_back(std::to_string(value));
    return true;
  }
  bool bind_null(const int pos) override
  {
    this->params.push_back("NULL");
    return true;
  }

 private:

private:
  bool execute()
  {
    std::vector<const char*> params;
    params.reserve(this->params.size());

    for (const auto& param: this->params)
      {
        log_debug("param:", param);
        params.push_back(param.data());
      }

    log_debug("body: ", body);
    this->result = PQexecParams(this->conn, this->body.data(),
                                this->params.size(),
                                nullptr,
                                params.data(),
                                nullptr,
                                nullptr,
                                0);
    const auto status = PQresultStatus(this->result);
    if (status == PGRES_TUPLES_OK)
      {
        log_debug("PGRES_TUPLES_OK");
      }
    else if (status != PGRES_COMMAND_OK)
      {
        log_error("Failed to execute command: ", PQresultErrorMessage(this->result));
        return false;
      }
    return true;
  }

  bool executed{false};
  std::string body;
  PGconn*const conn;
  std::vector<std::string> params;
  PGresult* result{nullptr};
  int current_tuple{0};
};

M src/database/query.cpp => src/database/query.cpp +25 -4
@@ 1,9 1,29 @@
#include <database/query.hpp>
#include <database/column.hpp>

template <>
void add_param<Id>(Query&, const Id&)
{}
void actual_bind(Statement& statement, const std::string& value, int index)
{
  log_debug("binding string:", value, " to col ", index);
  statement.bind_text(index, value);
}

void actual_bind(Statement& statement, const std::size_t value, int index)
{
  log_debug("binding size_t:", value);
  statement.bind_int64(index, value);
}

void actual_bind(Statement& statement, const OptionalBool& value, int index)
{
  log_debug("binding optional_t:", value.to_string());
  if (!value.is_set)
    statement.bind_int64(index, 0);
  else if (value.value)
    statement.bind_int64(index, 1);
  else
    statement.bind_int64(index, -1);
}


void actual_add_param(Query& query, const std::string& val)
{


@@ 28,7 48,8 @@ Query& operator<<(Query& query, const char* str)

Query& operator<<(Query& query, const std::string& str)
{
  query.body += "?";
  query.body += "$" + std::to_string(query.current_param);
  query.current_param++;
  actual_add_param(query, str);
  return query;
}

M src/database/query.hpp => src/database/query.hpp +7 -32
@@ 11,52 11,27 @@

#include <sqlite3.h>

void actual_bind(Statement& statement, const std::string& value, int index);
void actual_bind(Statement& statement, const std::size_t value, int index);
void actual_bind(Statement& statement, const OptionalBool& value, int index);

struct Query
{
    std::string body;
    std::vector<std::string> params;
    int current_param{1};

    Query(std::string str):
        body(std::move(str))
    {}

    Statement prepare(sqlite3* db)
    {
      sqlite3_stmt* stmt;
      auto res = sqlite3_prepare(db, this->body.data(), static_cast<int>(this->body.size()) + 1,
                                 &stmt, nullptr);
      if (res != SQLITE_OK)
        {
          log_error("Error preparing statement: ", sqlite3_errmsg(db));
          return nullptr;
        }
      Statement statement(stmt);
      int i = 1;
      for (const std::string& param: this->params)
        {
          if (sqlite3_bind_text(statement.get(), i, param.data(), static_cast<int>(param.size()), SQLITE_TRANSIENT) != SQLITE_OK)
            log_error("Failed to bind ", param, " to param ", i);
          i++;
        }

      return statement;
    }

    void execute(sqlite3* db)
    {
      auto statement = this->prepare(db);
      while (sqlite3_step(statement.get()) != SQLITE_DONE)
        ;
    }
};

template <typename ColumnType>
void add_param(Query& query, const ColumnType& column)
{
  std::cout << "add_param<ColumnType>" << std::endl;
  actual_add_param(query, column.value);
}
template <>
void add_param<Id>(Query& query, const Id& column);

template <typename T>
void actual_add_param(Query& query, const T& val)


@@ 81,7 56,7 @@ template <typename Integer>
typename std::enable_if<std::is_integral<Integer>::value, Query&>::type
operator<<(Query& query, const Integer& i)
{
  query.body += "?";
  query.body += "$" + std::to_string(query.current_param++);
  actual_add_param(query, i);
  return query;
}

M src/database/row.hpp => src/database/row.hpp +53 -52
@@ 1,72 1,73 @@
#pragma once

#include <database/insert_query.hpp>
#include <database/update_query.hpp>
#include <logger/logger.hpp>

#include <utils/is_one_of.hpp>

#include <type_traits>

#include <sqlite3.h>

template <typename ColumnType, typename... T>
typename std::enable_if<!std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
update_id(std::tuple<T...>&, sqlite3*)
{}

template <typename ColumnType, typename... T>
typename std::enable_if<std::is_same<std::decay_t<ColumnType>, Id>::value, void>::type
update_id(std::tuple<T...>& columns, sqlite3* db)
template <typename... T>
struct Row
{
  auto&& column = std::get<ColumnType>(columns);
  auto res = sqlite3_last_insert_rowid(db);
  column.value = static_cast<Id::real_type>(res);
}
  Row(std::string name):
      table_name(std::move(name))
  {}

template <std::size_t N=0, typename... T>
typename std::enable_if<N < sizeof...(T), void>::type
update_autoincrement_id(std::tuple<T...>& columns, sqlite3* db)
{
  using ColumnType = typename std::remove_reference<decltype(std::get<N>(columns))>::type;
  update_id<ColumnType>(columns, db);
  update_autoincrement_id<N+1>(columns, db);
}
  template <typename Type>
  typename Type::real_type& col()
  {
    auto&& col = std::get<Type>(this->columns);
    return col.value;
  }

template <std::size_t N=0, typename... T>
typename std::enable_if<N == sizeof...(T), void>::type
update_autoincrement_id(std::tuple<T...>&, sqlite3*)
{}
  template <typename Type>
  const auto& col() const
  {
    auto&& col = std::get<Type>(this->columns);
    return col.value;
  }

template <typename... T>
struct Row
{
    Row(std::string name):
    table_name(std::move(name))
    {}
  template <bool Coucou=true>
  void save(std::unique_ptr<DatabaseEngine>& db,  typename std::enable_if<!is_one_of<Id, T...> && Coucou>::type* = nullptr)
  {
    this->insert(*db);
  }

    template <typename Type>
    typename Type::real_type& col()
    {
      auto&& col = std::get<Type>(this->columns);
      return col.value;
    }
  template <bool Coucou=true>
  void save(std::unique_ptr<DatabaseEngine>& db,  typename std::enable_if<is_one_of<Id, T...> && Coucou>::type* = nullptr)
  {
    const Id& id = std::get<Id>(this->columns);
    if (id.value == Id::unset_value)
      {
        this->insert(*db);
        std::get<Id>(this->columns).value = db->last_inserted_rowid;
      }
    else
      this->update(*db);
  }

    template <typename Type>
    const auto& col() const
    {
      auto&& col = std::get<Type>(this->columns);
      return col.value;
    }
 private:
  void insert(DatabaseEngine& db)
  {
    InsertQuery query(this->table_name, this->columns);
    // Ugly workaround for non portable stuff
    query.body += db.get_returning_id_sql_string(Id::name);
    query.execute(db, this->columns);
  }

    void save(sqlite3* db)
    {
      InsertQuery query(this->table_name);
      query.insert_col_names(this->columns);
      query.insert_values(this->columns);
  void update(DatabaseEngine& db)
  {
    UpdateQuery query(this->table_name, this->columns);

      query.execute(this->columns, db);
    query.execute(db, this->columns);
  }

      update_autoincrement_id(this->columns, db);
    }
public:
  std::tuple<T...> columns;
  std::string table_name;

    std::tuple<T...> columns;
    std::string table_name;
};

M src/database/select_query.hpp => src/database/select_query.hpp +14 -10
@@ 1,5 1,7 @@
#pragma once

#include <database/engine.hpp>

#include <database/statement.hpp>
#include <database/query.hpp>
#include <logger/logger.hpp>


@@ 18,24 20,21 @@ template <typename T>
typename std::enable_if<std::is_integral<T>::value, sqlite3_int64>::type
extract_row_value(Statement& statement, const int i)
{
  return sqlite3_column_int64(statement.get(), i);
  return statement.get_column_int64(i);
}

template <typename T>
typename std::enable_if<std::is_same<std::string, T>::value, T>::type
extract_row_value(Statement& statement, const int i)
{
  const auto size = sqlite3_column_bytes(statement.get(), i);
  const unsigned char* str = sqlite3_column_text(statement.get(), i);
  std::string result(reinterpret_cast<const char*>(str), static_cast<std::size_t>(size));
  return result;
  return statement.get_column_text(i);
}

template <typename T>
typename std::enable_if<std::is_same<OptionalBool, T>::value, T>::type
extract_row_value(Statement& statement, const int i)
{
  const auto integer = sqlite3_column_int(statement.get(), i);
  const auto integer = statement.get_column_int(i);
  OptionalBool result;
  if (integer > 0)
    result.set_value(true);


@@ 109,16 108,21 @@ struct SelectQuery: public Query
      return *this;
    }

    auto execute(sqlite3* db)
    auto execute(DatabaseEngine& db)
    {
      auto statement = this->prepare(db);
      std::vector<Row<T...>> rows;
      while (sqlite3_step(statement.get()) == SQLITE_ROW)

      auto statement = db.prepare(this->body);
      statement->bind(std::move(this->params));

      while (statement->step() == StepResult::Row)
        {
          log_debug("one result.");
          Row<T...> row(this->table_name);
          extract_row_values(row, statement);
          extract_row_values(row, *statement);
          rows.push_back(row);
        }

      return rows;
    }


A src/database/sqlite3_engine.cpp => src/database/sqlite3_engine.cpp +93 -0
@@ 0,0 1,93 @@
#include <database/sqlite3_engine.hpp>

#include <database/sqlite3_statement.hpp>

#include <utils/tolower.hpp>
#include <logger/logger.hpp>
#include <vector>

Sqlite3Engine::Sqlite3Engine(sqlite3* db):
    db(db)
{
}

Sqlite3Engine::~Sqlite3Engine()
{
  sqlite3_close(this->db);
}

std::set<std::string> Sqlite3Engine::get_all_columns_from_table(const std::string& table_name)
{
  std::set<std::string> result;
  char* errmsg;
  std::string query{"PRAGMA table_info(" + table_name + ")"};
  int res = sqlite3_exec(this->db, query.data(), [](void* param, int columns_nb, char** columns, char**) -> int {
    constexpr int name_column = 1;
    std::set<std::string>* result = static_cast<std::set<std::string>*>(param);
    if (name_column < columns_nb)
      result->insert(utils::tolower(columns[name_column]));
    return 0;
  }, &result, &errmsg);

  if (res != SQLITE_OK)
    {
      log_error("Error executing ", query, ": ", errmsg);
      sqlite3_free(errmsg);
    }

  log_debug("List of columns in table ", table_name, ":");
  for (const auto& c: result)
    log_debug(c);
  return result;
}

std::unique_ptr<DatabaseEngine> Sqlite3Engine::open(const std::string& filename)
{
  sqlite3* new_db;
  auto res = sqlite3_open_v2(filename.data(), &new_db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr);
  if (res != SQLITE_OK)
    {
      log_error("Failed to open database file ", filename, ": ", sqlite3_errmsg(new_db));
      sqlite3_close(new_db);
      throw std::runtime_error("");
    }
  return std::make_unique<Sqlite3Engine>(new_db);
}

std::tuple<bool, std::string> Sqlite3Engine::raw_exec(const std::string& query)
{
  char* error;
  const auto result = sqlite3_exec(db, query.data(), nullptr, nullptr, &error);
  if (result != SQLITE_OK)
    {
      std::string err_msg(error);
      sqlite3_free(error);
      return std::make_tuple(false, err_msg);
    }
  return std::make_tuple(true, std::string{});
}

std::unique_ptr<Statement> Sqlite3Engine::prepare(const std::string& query)
{
  sqlite3_stmt* stmt;
  log_debug("SQLITE3: ", query);
  auto res = sqlite3_prepare(db, query.data(), static_cast<int>(query.size()) + 1,
                             &stmt, nullptr);
  if (res != SQLITE_OK)
    {
      log_error("Error preparing statement: ", sqlite3_errmsg(db));
      return nullptr;
    }
  return std::make_unique<Sqlite3Statement>(stmt);
}

void Sqlite3Engine::extract_last_insert_rowid(Statement& statement)
{
  this->last_inserted_rowid = sqlite3_last_insert_rowid(this->db);
  log_debug("extracted inserted ID: ", this->last_inserted_rowid);
}

std::string Sqlite3Engine::id_column_type()
{
  return "INTEGER PRIMARY KEY AUTOINCREMENT";
}

A src/database/sqlite3_engine.hpp => src/database/sqlite3_engine.hpp +30 -0
@@ 0,0 1,30 @@
#pragma once

#include <database/engine.hpp>

#include <database/statement.hpp>

#include <sqlite3.h>
#include <memory>
#include <string>
#include <tuple>
#include <set>

class Sqlite3Engine: public DatabaseEngine
{
 public:
  Sqlite3Engine(sqlite3* db);

  ~Sqlite3Engine();

  static std::unique_ptr<DatabaseEngine> open(const std::string& string);

  std::set<std::string> get_all_columns_from_table(const std::string& table_name) override final;
  std::tuple<bool, std::string> raw_exec(const std::string& query) override final;
  std::unique_ptr<Statement> prepare(const std::string& query) override;
  void extract_last_insert_rowid(Statement& statement) override;
  std::string id_column_type() override;
private:
  sqlite3* const db;
};


A src/database/sqlite3_statement.hpp => src/database/sqlite3_statement.hpp +93 -0
@@ 0,0 1,93 @@
#pragma once

#include <database/statement.hpp>

#include <logger/logger.hpp>

#include <sqlite3.h>

class Sqlite3Statement: public Statement
{
 public:
  Sqlite3Statement(sqlite3_stmt* stmt):
      stmt(stmt) {}
  ~Sqlite3Statement()
  {
    sqlite3_finalize(this->stmt);
  }

  StepResult step() override final
  {
    auto res = sqlite3_step(this->get());
    log_debug("step: ", res);
    if (res == SQLITE_ROW)
      return StepResult::Row;
    else if (res == SQLITE_DONE)
      return StepResult::Done;
    else
      return StepResult::Error;
  }

  void bind(std::vector<std::string> params) override
  {
  int i = 1;
  for (const std::string& param: params)
    {
      if (sqlite3_bind_text(this->get(), i, param.data(), static_cast<int>(param.size()), SQLITE_TRANSIENT) != SQLITE_OK)
        log_error("Failed to bind ", param, " to param ", i);
      i++;
    }
  }

  int64_t get_column_int64(const int col) override
  {
    return sqlite3_column_int64(this->get(), col);
  }

  std::string get_column_text(const int col) override
  {
    const auto size = sqlite3_column_bytes(this->get(), col);
    const unsigned char* str = sqlite3_column_text(this->get(), col);
    std::string result(reinterpret_cast<const char*>(str), static_cast<std::size_t>(size));
    return result;
  }

  bool bind_text(const int pos, const std::string& data) override
  {
    return sqlite3_bind_text(this->get(), pos, data.data(), static_cast<int>(data.size()), SQLITE_TRANSIENT) == SQLITE_OK;
  }
  bool bind_int64(const int pos, const std::int64_t value) override
  {
    return sqlite3_bind_int64(this->get(), pos, static_cast<sqlite3_int64>(value)) == SQLITE_OK;
  }
  bool bind_null(const int pos) override
  {
    return sqlite3_bind_null(this->get(), pos) == SQLITE_OK;
  }
  int get_column_int(const int col) override
  {
    return sqlite3_column_int(this->get(), col);
  }

  Sqlite3Statement(const Sqlite3Statement&) = delete;
  Sqlite3Statement& operator=(const Sqlite3Statement&) = delete;
  Sqlite3Statement(Sqlite3Statement&& other):
      stmt(other.stmt)
  {
    other.stmt = nullptr;
  }
  Sqlite3Statement& operator=(Sqlite3Statement&& other)
  {
    this->stmt = other.stmt;
    other.stmt = nullptr;
    return *this;
  }
  sqlite3_stmt* get()
  {
    return this->stmt;
  }

 private:
  sqlite3_stmt* stmt;
  int last_step_result{SQLITE_OK};
};

M src/database/statement.hpp => src/database/statement.hpp +19 -26
@@ 1,35 1,28 @@
#pragma once

#include <sqlite3.h>
#include <cstdint>
#include <string>
#include <vector>

enum class StepResult
{
  Row,
  Done,
  Error,
};

class Statement
{
 public:
  Statement(sqlite3_stmt* stmt):
      stmt(stmt) {}
  ~Statement()
  {
    sqlite3_finalize(this->stmt);
  }
  virtual StepResult step() = 0;

  virtual void bind(std::vector<std::string> params) = 0;

  Statement(const Statement&) = delete;
  Statement& operator=(const Statement&) = delete;
  Statement(Statement&& other):
      stmt(other.stmt)
  {
    other.stmt = nullptr;
  }
  Statement& operator=(Statement&& other)
  {
    this->stmt = other.stmt;
    other.stmt = nullptr;
    return *this;
  }
  sqlite3_stmt* get()
  {
    return this->stmt;
  }
  virtual std::int64_t get_column_int64(const int col) = 0;
  virtual std::string get_column_text(const int col) = 0;
  virtual int get_column_int(const int col) = 0;

 private:
  sqlite3_stmt* stmt;
  virtual bool bind_text(const int pos, const std::string& data) = 0;
  virtual bool bind_int64(const int pos, const std::int64_t value) = 0;
  virtual bool bind_null(const int pos) = 0;
};

D src/database/table.cpp => src/database/table.cpp +0 -23
@@ 1,23 0,0 @@
#include <database/table.hpp>

std::set<std::string> get_all_columns_from_table(sqlite3* db, const std::string& table_name)
{
  std::set<std::string> result;
  char* errmsg;
  std::string query{"PRAGMA table_info(" + table_name + ")"};
  int res = sqlite3_exec(db, query.data(), [](void* param, int columns_nb, char** columns, char**) -> int {
    constexpr int name_column = 1;
    std::set<std::string>* result = static_cast<std::set<std::string>*>(param);
    if (name_column < columns_nb)
      result->insert(columns[name_column]);
    return 0;
  }, &result, &errmsg);

  if (res != SQLITE_OK)
    {
      log_error("Error executing ", query, ": ", errmsg);
      sqlite3_free(errmsg);
    }

  return result;
}

M src/database/table.hpp => src/database/table.hpp +48 -41
@@ 1,7 1,8 @@
#pragma once

#include <database/engine.hpp>

#include <database/select_query.hpp>
#include <database/type_to_sql.hpp>
#include <database/row.hpp>

#include <algorithm>


@@ 10,23 11,27 @@

using namespace std::string_literals;

std::set<std::string> get_all_columns_from_table(sqlite3* db, const std::string& table_name);
template <typename T>
std::string ToSQLType(DatabaseEngine& db)
{
  if (std::is_same<T, Id>::value)
    return db.id_column_type();
  else if (std::is_same<typename T::real_type, std::string>::value)
    return "TEXT";
  else
    return "INTEGER";
}

template <typename ColumnType>
void add_column_to_table(sqlite3* db, const std::string& table_name)
void add_column_to_table(DatabaseEngine& db, const std::string& table_name)
{
  const std::string name = ColumnType::name;
  std::string query{"ALTER TABLE " + table_name + " ADD " + ColumnType::name + " " + TypeToSQLType<typename ColumnType::real_type>::type};
  char* error;
  const auto result = sqlite3_exec(db, query.data(), nullptr, nullptr, &error);
  if (result != SQLITE_OK)
    {
      log_error("Error adding column ", name, " to table ", table_name, ": ",  error);
      sqlite3_free(error);
    }
  std::string query{"ALTER TABLE " + table_name + " ADD " + ColumnType::name + " " + ToSQLType<ColumnType>(db)};
  auto res = db.raw_exec(query);
  if (std::get<0>(res) == false)
    log_error("Error adding column ", name, " to table ", table_name, ": ",  std::get<1>(res));
}


template <typename ColumnType, decltype(ColumnType::options) = nullptr>
void append_option(std::string& s)
{


@@ 50,27 55,24 @@ class Table
      name(std::move(name))
  {}

  void upgrade(sqlite3* db)
  void upgrade(DatabaseEngine& db)
  {
    const auto existing_columns = get_all_columns_from_table(db, this->name);
    const auto existing_columns = db.get_all_columns_from_table(this->name);
    add_column_if_not_exists(db, existing_columns);
  }

  void create(sqlite3* db)
  void create(DatabaseEngine& db)
  {
    std::string res{"CREATE TABLE IF NOT EXISTS "};
    res += this->name;
    res += " (\n";
    this->add_column_create(res);
    res += ")";

    char* error;
    const auto result = sqlite3_exec(db, res.data(), nullptr, nullptr, &error);
    if (result != SQLITE_OK)
      {
        log_error("Error executing query: ", error);
        sqlite3_free(error);
      }
    std::string query{"CREATE TABLE IF NOT EXISTS "};
    query += this->name;
    query += " (\n";
    this->add_column_create(db, query);
    query += ")";

    log_debug("create:" , query);
    auto result = db.raw_exec(query);
    if (std::get<0>(result) == false)
      log_error("Error executing query: ", std::get<1>(result));
  }

  RowType row()


@@ 78,7 80,7 @@ class Table
    return {this->name};
  }

  SelectQuery<T...> select()
  auto select()
  {
    SelectQuery<T...> select(this->name);
    return select;


@@ 93,39 95,44 @@ class Table

  template <std::size_t N=0>
  typename std::enable_if<N < sizeof...(T), void>::type
  add_column_if_not_exists(sqlite3* db, const std::set<std::string>& existing_columns)
  add_column_if_not_exists(DatabaseEngine& db, const std::set<std::string>& existing_columns)
  {
    using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<ColumnTypes>()))>::type;
    if (existing_columns.count(ColumnType::name) != 1)
      {
        add_column_to_table<ColumnType>(db, this->name);
      }
    if (existing_columns.count(ColumnType::name) == 0)
      add_column_to_table<ColumnType>(db, this->name);
    add_column_if_not_exists<N+1>(db, existing_columns);
  }
  template <std::size_t N=0>
  typename std::enable_if<N == sizeof...(T), void>::type
  add_column_if_not_exists(sqlite3*, const std::set<std::string>&)
  add_column_if_not_exists(DatabaseEngine&, const std::set<std::string>&)
  {}

  template <std::size_t N=0>
  typename std::enable_if<N < sizeof...(T), void>::type
  add_column_create(std::string& str)
  add_column_create(DatabaseEngine& db, std::string& str)
  {
    using ColumnType = typename std::remove_reference<decltype(std::get<N>(std::declval<ColumnTypes>()))>::type;
    using RealType = typename ColumnType::real_type;
//    using RealType = typename ColumnType::real_type;
    str += ColumnType::name;
    str += " ";
    str += TypeToSQLType<RealType>::type;
    append_option<ColumnType>(str);
//    if (std::is_same<ColumnType, Id>::value)
//      {
//        str += "INTEGER PRIMARY KEY AUTOINCREMENT";
//      }
//    else
//      {
        str += ToSQLType<ColumnType>(db);
//        append_option<ColumnType>(str);
//      }
    if (N != sizeof...(T) - 1)
      str += ",";
    str += "\n";

    add_column_create<N+1>(str);
    add_column_create<N+1>(db, str);
  }
  template <std::size_t N=0>
  typename std::enable_if<N == sizeof...(T), void>::type
  add_column_create(std::string&)
  add_column_create(DatabaseEngine&, std::string&)
  { }

  const std::string name;

D src/database/type_to_sql.cpp => src/database/type_to_sql.cpp +0 -9
@@ 1,9 0,0 @@
#include <database/type_to_sql.hpp>

template <> const std::string TypeToSQLType<int>::type = "INTEGER";
template <> const std::string TypeToSQLType<std::size_t>::type = "INTEGER";
template <> const std::string TypeToSQLType<long>::type = "INTEGER";
template <> const std::string TypeToSQLType<long long>::type = "INTEGER";
template <> const std::string TypeToSQLType<bool>::type = "INTEGER";
template <> const std::string TypeToSQLType<std::string>::type = "TEXT";
template <> const std::string TypeToSQLType<OptionalBool>::type = "INTEGER";
\ No newline at end of file

D src/database/type_to_sql.hpp => src/database/type_to_sql.hpp +0 -16
@@ 1,16 0,0 @@
#pragma once

#include <utils/optional_bool.hpp>

#include <string>

template <typename T>
struct TypeToSQLType { static const std::string type; };

template <> const std::string TypeToSQLType<int>::type;
template <> const std::string TypeToSQLType<std::size_t>::type;
template <> const std::string TypeToSQLType<long>::type;
template <> const std::string TypeToSQLType<long long>::type;
template <> const std::string TypeToSQLType<bool>::type;
template <> const std::string TypeToSQLType<std::string>::type;
template <> const std::string TypeToSQLType<OptionalBool>::type;
\ No newline at end of file

A src/database/update_query.hpp => src/database/update_query.hpp +100 -0
@@ 0,0 1,100 @@
#pragma once

#include <database/query.hpp>
#include <database/engine.hpp>

using namespace std::string_literals;

template <class T, class... Tuple>
struct Index;

template <class T, class... Types>
struct Index<T, std::tuple<T, Types...>>
{
  static const std::size_t value = 0;
};

template <class T, class U, class... Types>
struct Index<T, std::tuple<U, Types...>>
{
  static const std::size_t value = Index<T, std::tuple<Types...>>::value + 1;
};

struct UpdateQuery: public Query
{
  template <typename... T>
  UpdateQuery(const std::string& name, const std::tuple<T...>& columns):
      Query("UPDATE ")
  {
    this->body += name;
    this->insert_col_names_and_values(columns);
  }

  template <typename... T>
  void insert_col_names_and_values(const std::tuple<T...>& columns)
  {
    this->body += " SET ";
    this->insert_col_name_and_value(columns);
    this->body += " WHERE "s + Id::name + "=$" + std::to_string(this->current_param);
  }

  template <int N=0, typename... T>
  typename std::enable_if<N < sizeof...(T), void>::type
  insert_col_name_and_value(const std::tuple<T...>& columns)
  {
    using ColumnType = std::decay_t<decltype(std::get<N>(columns))>;

    if (!std::is_same<ColumnType, Id>::value)
      {
        this->body += ColumnType::name + "=$"s + std::to_string(this->current_param);
        this->current_param++;

        if (N < (sizeof...(T) - 1))
          this->body += ", ";
      }

    this->insert_col_name_and_value<N+1>(columns);
  }
  template <int N=0, typename... T>
  typename std::enable_if<N == sizeof...(T), void>::type
  insert_col_name_and_value(const std::tuple<T...>&)
  {}


  template <typename... T>
  void execute(DatabaseEngine& db, const std::tuple<T...>& columns)
  {
    auto statement = db.prepare(this->body);
    this->bind_param(columns, *statement);
    this->bind_id(columns, *statement);

    statement->step();
  }

  template <int N=0, typename... T>
  typename std::enable_if<N < sizeof...(T), void>::type
  bind_param(const std::tuple<T...>& columns, Statement& statement, int index=1)
  {
    auto&& column = std::get<N>(columns);
    using ColumnType = std::decay_t<decltype(column)>;

    if (!std::is_same<ColumnType, Id>::value)
      actual_bind(statement, column.value, index++);

    this->bind_param<N+1>(columns, statement, index);
  }

  template <int N=0, typename... T>
  typename std::enable_if<N == sizeof...(T), void>::type
  bind_param(const std::tuple<T...>&, Statement&, int)
  {}

  template <typename... T>
  void bind_id(const std::tuple<T...>& columns, Statement& statement)
  {
    static constexpr auto index = Index<Id, std::tuple<T...>>::value;
    auto&& value = std::get<index>(columns);

    actual_bind(statement, value.value, sizeof...(T));
  }
};

M src/main.cpp => src/main.cpp +2 -0
@@ 6,6 6,8 @@
#include <utils/xdg.hpp>
#include <utils/reload.hpp>

#include <libpq-fe.h>

#ifdef UDNS_FOUND
# include <network/dns_handler.hpp>
#endif

A src/utils/is_one_of.hpp => src/utils/is_one_of.hpp +17 -0
@@ 0,0 1,17 @@
#pragma once

#include <type_traits>

template <typename...>
struct is_one_of_implem {
    static constexpr bool value = false;
};

template <typename F, typename S, typename... T>
struct is_one_of_implem<F, S, T...> {
    static constexpr bool value =
        std::is_same<F, S>::value || is_one_of_implem<F, T...>::value;
};

template<typename... T>
constexpr bool is_one_of = is_one_of_implem<T...>::value;

A src/utils/optional_bool.cpp => src/utils/optional_bool.cpp +8 -0
@@ 0,0 1,8 @@
#include <utils/optional_bool.hpp>


std::ostream& operator<<(std::ostream& os, const OptionalBool& o)
{
  os << o.to_string();
  return os;
}

M src/utils/optional_bool.hpp => src/utils/optional_bool.hpp +3 -1
@@ 20,7 20,7 @@ struct OptionalBool
    this->is_set = false;
  }

  std::string to_string()
  std::string to_string() const
  {
    if (this->is_set == false)
      return "unset";


@@ 33,3 33,5 @@ struct OptionalBool
  bool is_set{false};
  bool value{false};
};

std::ostream& operator<<(std::ostream& os, const OptionalBool& o);

M tests/database.cpp => tests/database.cpp +12 -1
@@ 7,13 7,25 @@
TEST_CASE("Database")
{
#ifdef USE_DATABASE
//  Database::open("postgresql://test");
  Database::open(":memory:");
  Database::raw_exec("DELETE FROM " + Database::irc_server_options.get_name());
  Database::raw_exec("DELETE FROM " + Database::irc_channel_options.get_name());

  SECTION("Basic retrieve and update")
    {
      auto o = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
      CHECK(Database::count(Database::irc_server_options) == 0);
      o.save(Database::db);
      CHECK(Database::count(Database::irc_server_options) == 1);
      o.col<Database::Realname>() = "Different realname";
      CHECK(o.col<Database::Realname>() == "Different realname");
      o.save(Database::db);
      CHECK(o.col<Database::Realname>() == "Different realname");
      CHECK(Database::count(Database::irc_server_options) == 1);

      auto a = Database::get_irc_server_options("zouzou@example.com", "irc.example.com");
      CHECK(a.col<Database::Realname>() == "Different realname");
      auto b = Database::get_irc_server_options("moumou@example.com", "irc.example.com");

      // b does not yet exist in the db, the object is created but not yet


@@ 28,7 40,6 @@ TEST_CASE("Database")

  SECTION("channel options")
    {
      Config::set("db_name", ":memory:");
      auto o = Database::get_irc_channel_options("zouzou@example.com", "irc.example.com", "#foo");

      CHECK(o.col<Database::EncodingIn>() == "");

M tests/utils.cpp => tests/utils.cpp +12 -0
@@ 11,6 11,7 @@
#include <utils/system.hpp>
#include <utils/scopeguard.hpp>
#include <utils/dirname.hpp>
#include <utils/is_one_of.hpp>

using namespace std::string_literals;



@@ 171,3 172,14 @@ TEST_CASE("dirname")
  CHECK(utils::dirname(".") == ".");
  CHECK(utils::dirname("./") == "./");
}

TEST_CASE("is_in")
{
  CHECK((is_one_of<int, float, std::string, int>) == true);
  CHECK((is_one_of<int, float, std::string>) == false);
  CHECK((is_one_of<int>) == false);
  CHECK((is_one_of<int, int>) == true);
  CHECK((is_one_of<bool, int>) == false);
  CHECK((is_one_of<bool, bool>) == true);
  CHECK((is_one_of<bool, bool, bool, bool, bool, int>) == true);
}