Commit 8cfdd93a authored by Hugo Beauzée-Luyssen's avatar Hugo Beauzée-Luyssen

Introduce a SqliteConnection wrapper.

It allows us not to share connections between threads, and will do for
the time being.
It's quite unsafe, as TLS is used to store the connection, but doesn't
mark the connections as released.
This is mostly commited as such to move forward, we'll have to rewrite
the databases interactions in a near future
Now I'll be in the corner, crying.
parent c1c86b05
......@@ -17,8 +17,7 @@ class IShowEpisode;
class IVideoTrack;
class ILogger;
class IArtist;
struct sqlite3;
class SqliteConnection;
typedef std::shared_ptr<IFile> FilePtr;
typedef std::shared_ptr<IFolder> FolderPtr;
......@@ -32,7 +31,7 @@ typedef std::shared_ptr<IAudioTrack> AudioTrackPtr;
typedef std::shared_ptr<IVideoTrack> VideoTrackPtr;
typedef std::shared_ptr<IArtist> ArtistPtr;
typedef std::weak_ptr<sqlite3> DBConnection;
typedef SqliteConnection* DBConnection;
enum ServiceStatus
{
......
......@@ -75,6 +75,7 @@ list(APPEND SRC_LIST ${HEADERS_LIST}
discoverer/DiscovererWorker.cpp
Utils.cpp
database/SqliteConnection.cpp
logging/IostreamLogger.cpp
logging/Logger.cpp
......
......@@ -16,6 +16,7 @@
#include "Show.h"
#include "ShowEpisode.h"
#include "database/SqliteTools.h"
#include "database/SqliteConnection.h"
#include "Utils.h"
#include "VideoTrack.h"
......@@ -76,6 +77,8 @@ MediaLibrary::~MediaLibrary()
VideoTrack::clear();
AudioTrack::clear();
Artist::clear();
// Explicitely release the connection's TLS
m_dbConnection->release();
}
void MediaLibrary::setFsFactory(std::shared_ptr<factory::IFileSystem> fsFactory)
......@@ -89,6 +92,7 @@ bool MediaLibrary::initialize( const std::string& dbPath, const std::string& sna
m_fsFactory.reset( new factory::FileSystemDefaultFactory );
m_snapshotPath = snapshotPath;
m_callback = mlCallback;
m_dbConnection.reset( new SqliteConnection( dbPath ) );
if ( mlCallback != nullptr )
{
......@@ -111,40 +115,30 @@ bool MediaLibrary::initialize( const std::string& dbPath, const std::string& sna
addMetadataService( std::move( thumbnailerService ) );
}
sqlite3* dbConnection;
int res = sqlite3_open( dbPath.c_str(), &dbConnection );
if ( res != SQLITE_OK )
return false;
m_dbConnection.reset( dbConnection, &sqlite3_close );
if ( sqlite::Tools::executeRequest( DBConnection(m_dbConnection), "PRAGMA foreign_keys = ON" ) == false )
{
LOG_ERROR( "Failed to enable foreign keys" );
return false;
}
if ( ! ( File::createTable( m_dbConnection ) &&
Folder::createTable( m_dbConnection ) &&
Label::createTable( m_dbConnection ) &&
Album::createTable( m_dbConnection ) &&
AlbumTrack::createTable( m_dbConnection ) &&
Show::createTable( m_dbConnection ) &&
ShowEpisode::createTable( m_dbConnection ) &&
Movie::createTable( m_dbConnection ) &&
VideoTrack::createTable( m_dbConnection ) &&
AudioTrack::createTable( m_dbConnection ) &&
Artist::createTable( m_dbConnection ) ) )
if ( ! ( File::createTable( m_dbConnection.get() ) &&
Folder::createTable( m_dbConnection.get() ) &&
Label::createTable( m_dbConnection.get() ) &&
Album::createTable( m_dbConnection.get() ) &&
AlbumTrack::createTable( m_dbConnection.get() ) &&
Show::createTable( m_dbConnection.get() ) &&
ShowEpisode::createTable( m_dbConnection.get() ) &&
Movie::createTable( m_dbConnection.get() ) &&
VideoTrack::createTable( m_dbConnection.get() ) &&
AudioTrack::createTable( m_dbConnection.get() ) &&
Artist::createTable( m_dbConnection.get() ) ) )
{
LOG_ERROR( "Failed to create database structure" );
return false;
}
m_discoverer->setCallback( m_callback );
m_discoverer->addDiscoverer( std::unique_ptr<IDiscoverer>( new FsDiscoverer( m_fsFactory, this, m_dbConnection ) ) );
m_discoverer->addDiscoverer( std::unique_ptr<IDiscoverer>( new FsDiscoverer( m_fsFactory, this, m_dbConnection.get() ) ) );
m_discoverer->reload();
return true;
}
std::vector<FilePtr> MediaLibrary::files()
{
return File::fetchAll( m_dbConnection );
return File::fetchAll( m_dbConnection.get() );
}
std::vector<FilePtr> MediaLibrary::audioFiles()
......@@ -152,19 +146,19 @@ std::vector<FilePtr> MediaLibrary::audioFiles()
static const std::string req = "SELECT * FROM " + policy::FileTable::Name + " WHERE type = ?";
//FIXME: Replace this with template magic in sqlite's traits
using type_t = std::underlying_type<IFile::Type>::type;
return sqlite::Tools::fetchAll<File, IFile>( m_dbConnection, req, static_cast<type_t>( IFile::Type::AudioType ) );
return sqlite::Tools::fetchAll<File, IFile>( m_dbConnection.get(), req, static_cast<type_t>( IFile::Type::AudioType ) );
}
std::vector<FilePtr> MediaLibrary::videoFiles()
{
static const std::string req = "SELECT * FROM " + policy::FileTable::Name + " WHERE type = ?";
using type_t = std::underlying_type<IFile::Type>::type;
return sqlite::Tools::fetchAll<File, IFile>( m_dbConnection, req, static_cast<type_t>( IFile::Type::VideoType ) );
return sqlite::Tools::fetchAll<File, IFile>( m_dbConnection.get(), req, static_cast<type_t>( IFile::Type::VideoType ) );
}
FilePtr MediaLibrary::file( const std::string& path )
{
return File::fetch( m_dbConnection, path );
return File::fetch( m_dbConnection.get(), path );
}
FilePtr MediaLibrary::addFile( const std::string& path, FolderPtr parentFolder )
......@@ -194,7 +188,7 @@ FilePtr MediaLibrary::addFile( const std::string& path, FolderPtr parentFolder )
if ( type == IFile::Type::UnknownType )
return false;
auto fptr = File::create( m_dbConnection, type, file.get(), parentFolder != nullptr ? parentFolder->id() : 0 );
auto fptr = File::create( m_dbConnection.get(), type, file.get(), parentFolder != nullptr ? parentFolder->id() : 0 );
if ( fptr == nullptr )
{
LOG_ERROR( "Failed to add file ", file->fullPath(), " to the media library" );
......@@ -203,28 +197,28 @@ FilePtr MediaLibrary::addFile( const std::string& path, FolderPtr parentFolder )
LOG_INFO( "Adding ", file->name() );
if ( m_callback != nullptr )
m_callback->onFileAdded( fptr );
// m_parser->parse( fptr, m_callback );
m_parser->parse( fptr, m_callback );
return fptr;
}
FolderPtr MediaLibrary::folder( const std::string& path )
{
return Folder::fetch( m_dbConnection, path );
return Folder::fetch( m_dbConnection.get(), path );
}
bool MediaLibrary::deleteFile( const std::string& mrl )
{
return File::destroy( m_dbConnection, mrl );
return File::destroy( m_dbConnection.get(), mrl );
}
bool MediaLibrary::deleteFile( FilePtr file )
{
return File::destroy( m_dbConnection, std::static_pointer_cast<File>( file ) );
return File::destroy( m_dbConnection.get(), std::static_pointer_cast<File>( file ) );
}
bool MediaLibrary::deleteFolder( FolderPtr folder )
{
if ( Folder::destroy( m_dbConnection, std::static_pointer_cast<Folder>( folder ) ) == false )
if ( Folder::destroy( m_dbConnection.get(), std::static_pointer_cast<Folder>( folder ) ) == false )
return false;
File::clear();
return true;
......@@ -232,17 +226,17 @@ bool MediaLibrary::deleteFolder( FolderPtr folder )
LabelPtr MediaLibrary::createLabel( const std::string& label )
{
return Label::create( m_dbConnection, label );
return Label::create( m_dbConnection.get(), label );
}
bool MediaLibrary::deleteLabel( const std::string& text )
{
return Label::destroy( m_dbConnection, text );
return Label::destroy( m_dbConnection.get(), text );
}
bool MediaLibrary::deleteLabel( LabelPtr label )
{
return Label::destroy( m_dbConnection, std::static_pointer_cast<Label>( label ) );
return Label::destroy( m_dbConnection.get(), std::static_pointer_cast<Label>( label ) );
}
AlbumPtr MediaLibrary::album(const std::string& title )
......@@ -250,59 +244,59 @@ AlbumPtr MediaLibrary::album(const std::string& title )
// We can't use Cache helper, since albums are cached by primary keys
static const std::string req = "SELECT * FROM " + policy::AlbumTable::Name +
" WHERE title = ?";
return sqlite::Tools::fetchOne<Album>( DBConnection( m_dbConnection ), req, title );
return sqlite::Tools::fetchOne<Album>( m_dbConnection.get(), req, title );
}
AlbumPtr MediaLibrary::createAlbum(const std::string& title )
{
return Album::create( m_dbConnection, title );
return Album::create( m_dbConnection.get(), title );
}
std::vector<AlbumPtr> MediaLibrary::albums()
{
return Album::fetchAll( m_dbConnection );
return Album::fetchAll( m_dbConnection.get() );
}
ShowPtr MediaLibrary::show(const std::string& name)
{
static const std::string req = "SELECT * FROM " + policy::ShowTable::Name
+ " WHERE name = ?";
return sqlite::Tools::fetchOne<Show>( m_dbConnection, req, name );
return sqlite::Tools::fetchOne<Show>( m_dbConnection.get(), req, name );
}
ShowPtr MediaLibrary::createShow(const std::string& name)
{
return Show::create( m_dbConnection, name );
return Show::create( m_dbConnection.get(), name );
}
MoviePtr MediaLibrary::movie( const std::string& title )
{
static const std::string req = "SELECT * FROM " + policy::MovieTable::Name
+ " WHERE title = ?";
return sqlite::Tools::fetchOne<Movie>( m_dbConnection, req, title );
return sqlite::Tools::fetchOne<Movie>( m_dbConnection.get(), req, title );
}
MoviePtr MediaLibrary::createMovie( const std::string& title )
{
return Movie::create( m_dbConnection, title );
return Movie::create( m_dbConnection.get(), title );
}
ArtistPtr MediaLibrary::artist(const std::string &name)
{
static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name
+ " WHERE name = ?";
return sqlite::Tools::fetchOne<Artist>( m_dbConnection, req, name );
return sqlite::Tools::fetchOne<Artist>( m_dbConnection.get(), req, name );
}
ArtistPtr MediaLibrary::createArtist( const std::string& name )
{
return Artist::create( m_dbConnection, name );
return Artist::create( m_dbConnection.get(), name );
}
std::vector<ArtistPtr> MediaLibrary::artists() const
{
static const std::string req = "SELECT * FROM " + policy::ArtistTable::Name;
return sqlite::Tools::fetchAll<Artist, IArtist>( m_dbConnection, req );
return sqlite::Tools::fetchAll<Artist, IArtist>( m_dbConnection.get(), req );
}
void MediaLibrary::addMetadataService(std::unique_ptr<IMetadataService> service)
......
......@@ -3,6 +3,7 @@
class Parser;
class DiscovererWorker;
class SqliteConnection;
#include <sqlite3.h>
......@@ -62,7 +63,7 @@ class MediaLibrary : public IMediaLibrary
void reload();
private:
std::shared_ptr<sqlite3> m_dbConnection;
std::unique_ptr<SqliteConnection> m_dbConnection;
std::shared_ptr<factory::IFileSystem> m_fsFactory;
std::string m_snapshotPath;
IMediaLibraryCb* m_callback;
......
......@@ -53,7 +53,7 @@ template <typename IMPL, typename INTF, typename TABLEPOLICY, typename CACHEPOLI
class Cache
{
public:
static std::shared_ptr<IMPL> fetch( DBConnection dbConnectionWeak, const typename CACHEPOLICY::KeyType& key )
static std::shared_ptr<IMPL> fetch( DBConnection dbConnection, const typename CACHEPOLICY::KeyType& key )
{
Lock lock( Mutex );
auto it = Store.find( key );
......@@ -61,7 +61,7 @@ class Cache
return it->second;
static const std::string req = "SELECT * FROM " + TABLEPOLICY::Name +
" WHERE " + TABLEPOLICY::CacheColumn + " = ?";
auto res = sqlite::Tools::fetchOne<IMPL>( dbConnectionWeak, req.c_str(), key );
auto res = sqlite::Tools::fetchOne<IMPL>( dbConnection, req.c_str(), key );
if ( res == nullptr )
return nullptr;
Store[key] = res;
......@@ -78,7 +78,7 @@ class Cache
return sqlite::Tools::fetchAll<IMPL, INTF>( dbConnectionWeak, req.c_str() );
}
static std::shared_ptr<IMPL> load( std::shared_ptr<sqlite3> dbConnection, sqlite3_stmt* stmt )
static std::shared_ptr<IMPL> load( DBConnection dbConnection, sqlite3_stmt* stmt )
{
auto cacheKey = CACHEPOLICY::key( stmt );
......@@ -91,7 +91,7 @@ class Cache
return inst;
}
static bool destroy( DBConnection dbConnectionWeak, const typename CACHEPOLICY::KeyType& key )
static bool destroy( DBConnection dbConnection, const typename CACHEPOLICY::KeyType& key )
{
Lock lock( Mutex );
auto it = Store.find( key );
......@@ -99,19 +99,19 @@ class Cache
Store.erase( it );
static const std::string req = "DELETE FROM " + TABLEPOLICY::Name + " WHERE " +
TABLEPOLICY::CacheColumn + " = ?";
return sqlite::Tools::executeDelete( dbConnectionWeak, req.c_str(), key );
return sqlite::Tools::executeDelete( dbConnection, req.c_str(), key );
}
static bool destroy( DBConnection dbConnectionWeak, const std::shared_ptr<IMPL>& self )
static bool destroy( DBConnection dbConnection, const std::shared_ptr<IMPL>& self )
{
const auto& key = CACHEPOLICY::key( self );
return destroy( dbConnectionWeak, key );
return destroy( dbConnection, key );
}
static bool destroy( DBConnection dbConnectionWeak, const IMPL* self )
static bool destroy( DBConnection dbConnection, const IMPL* self )
{
const auto& key = CACHEPOLICY::key( self );
return destroy( dbConnectionWeak, key );
return destroy( dbConnection, key );
}
static void clear()
......@@ -141,9 +141,9 @@ class Cache
* Create a new instance of the cache class.
*/
template <typename... Args>
static bool insert( DBConnection dbConnectionWeak, std::shared_ptr<IMPL> self, const std::string& req, Args&&... args )
static bool insert( DBConnection dbConnection, std::shared_ptr<IMPL> self, const std::string& req, Args&&... args )
{
unsigned int pKey = sqlite::Tools::insert( dbConnectionWeak, req, std::forward<Args>( args )... );
unsigned int pKey = sqlite::Tools::insert( dbConnection, req, std::forward<Args>( args )... );
if ( pKey == 0 )
return false;
(self.get())->*TABLEPOLICY::PrimaryKey = pKey;
......
#include "SqliteConnection.h"
#include "database/SqliteTools.h"
thread_local sqlite3* dbConnection;
SqliteConnection::SqliteConnection( const std::string &dbPath )
: m_dbPath( dbPath )
{
if ( sqlite3_threadsafe() == 0 )
throw std::runtime_error( "SQLite isn't built with threadsafe mode" );
if ( sqlite3_config( SQLITE_CONFIG_MULTITHREAD ) == SQLITE_ERROR )
throw std::runtime_error( "Failed to enable sqlite multithreaded mode" );
}
sqlite3 *SqliteConnection::getConn()
{
if ( dbConnection == nullptr )
{
auto res = sqlite3_open( m_dbPath.c_str(), &dbConnection );
if ( res != SQLITE_OK )
throw std::runtime_error( "Failed to connect to database" );
res = sqlite3_busy_timeout( dbConnection, 500 );
if ( res != SQLITE_OK )
LOG_WARN( "Failed to enable sqlite busy timeout" );
if ( sqlite::Tools::executeRequest( this, "PRAGMA foreign_keys = ON" ) == false )
throw std::runtime_error( "Failed to enable foreign keys" );
std::unique_lock<std::mutex> lock( m_connMutex );
m_conns.emplace(std::this_thread::get_id(), ConnPtr( dbConnection, &sqlite3_close ) );
}
return dbConnection;
}
void SqliteConnection::release()
{
std::unique_lock<std::mutex> lock( m_connMutex );
m_conns.erase( std::this_thread::get_id() );
dbConnection = nullptr;
}
#ifndef SQLITECONNECTION_H
#define SQLITECONNECTION_H
#include <memory>
#include <sqlite3.h>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <unordered_map>
class SqliteConnection
{
public:
SqliteConnection( const std::string& dbPath );
// Returns the current thread's connection
// This will initiate a connection if required
sqlite3* getConn();
// Release the current thread's connection
void release();
private:
using ConnPtr = std::unique_ptr<sqlite3, int(*)(sqlite3*)>;
const std::string m_dbPath;
std::mutex m_connMutex;
std::unordered_map<std::thread::id, ConnPtr> m_conns;
};
#endif // SQLITECONNECTION_H
......@@ -8,8 +8,10 @@
#include <string>
#include <vector>
#include <iostream>
#include <thread>
#include "Types.h"
#include "database/SqliteConnection.h"
#include "logging/Logger.h"
namespace sqlite
......@@ -102,12 +104,8 @@ class Tools
* be discarded.
*/
template <typename IMPL, typename INTF, typename... Args>
static std::vector<std::shared_ptr<INTF> > fetchAll( DBConnection dbConnectionWeak, const std::string& req, Args&&... args )
static std::vector<std::shared_ptr<INTF> > fetchAll( DBConnection dbConnection, const std::string& req, Args&&... args )
{
auto dbConnection = dbConnectionWeak.lock();
if ( dbConnection == nullptr )
throw std::runtime_error("Invalid SQlite connection");
std::vector<std::shared_ptr<INTF>> results;
auto stmt = prepareRequest( dbConnection, req, std::forward<Args>( args )...);
if ( stmt == nullptr )
......@@ -123,47 +121,55 @@ class Tools
}
template <typename T, typename... Args>
static std::shared_ptr<T> fetchOne( DBConnection dbConnectionWeak, const std::string& req, Args&&... args )
static std::shared_ptr<T> fetchOne( DBConnection dbConnection, const std::string& req, Args&&... args )
{
auto dbConnection = dbConnectionWeak.lock();
if ( dbConnection == nullptr )
return nullptr;
std::shared_ptr<T> result;
auto stmt = prepareRequest( dbConnection, req, std::forward<Args>( args )... );
if ( stmt == nullptr )
return nullptr;
if ( sqlite3_step( stmt.get() ) != SQLITE_ROW )
return result;
result = T::load( dbConnection, stmt.get() );
return result;
int res = sqlite3_step( stmt.get() );
if ( res != SQLITE_ROW )
return nullptr;
return T::load( dbConnection, stmt.get() );
}
template <typename... Args>
static bool executeDelete( DBConnection dbConnectionWeak, const std::string& req, Args&&... args )
static bool executeRequest( DBConnection dbConnection, const std::string& req, Args&&... args )
{
auto dbConnection = dbConnectionWeak.lock();
if ( dbConnection == nullptr )
auto stmt = prepareRequest( dbConnection, req, std::forward<Args>( args )... );
if ( stmt == nullptr )
return false;
if ( executeRequest( dbConnection, req, std::forward<Args>( args )... ) == false )
int res;
do
{
res = sqlite3_step( stmt.get() );
} while ( res == SQLITE_ROW );
if ( res != SQLITE_DONE )
{
#if SQLITE_VERSION_NUMBER >= 3007015
auto err = sqlite3_errstr( res );
#else
auto err = res;
#endif
LOG_ERROR( "Failed to execute <", req, ">\nInvalid result: ", err,
": ", sqlite3_errmsg( dbConnection->getConn() ) );
return false;
return sqlite3_changes( dbConnection.get() ) > 0;
}
return true;
}
template <typename... Args>
static bool executeUpdate( DBConnection dbConnectionWeak, const std::string& req, Args&&... args )
static bool executeDelete( DBConnection dbConnection, const std::string& req, Args&&... args )
{
// The code would be exactly the same, do not freak out because it calls executeDelete :)
return executeDelete( dbConnectionWeak, req, std::forward<Args>( args )... );
if ( executeRequest( dbConnection, req, std::forward<Args>( args )... ) == false )
return false;
return sqlite3_changes( dbConnection->getConn() ) > 0;
}
template <typename... Args>
static bool executeRequest( DBConnection dbConnectionWeak, const std::string& req, Args&&... args )
static bool executeUpdate( DBConnection dbConnectionWeak, const std::string& req, Args&&... args )
{
auto dbConnection = dbConnectionWeak.lock();
if ( dbConnection == nullptr )
return false;
return executeRequest( dbConnection, req, std::forward<Args>( args )... );
// The code would be exactly the same, do not freak out because it calls executeDelete :)
return executeDelete( dbConnectionWeak, req, std::forward<Args>( args )... );
}
/**
......@@ -171,70 +177,40 @@ class Tools
* Returns 0 (which is an invalid sqlite primary key) when insertion fails.
*/
template <typename... Args>
static unsigned int insert( DBConnection dbConnectionWeak, const std::string& req, Args&&... args )
static unsigned int insert( DBConnection dbConnection, const std::string& req, Args&&... args )
{
auto dbConnection = dbConnectionWeak.lock();
if ( dbConnection == nullptr )
return false;
if ( executeRequest( dbConnection, req, std::forward<Args>( args )... ) == false )
return 0;
return sqlite3_last_insert_rowid( dbConnection.get() );
return sqlite3_last_insert_rowid( dbConnection->getConn() );
}
private:
template <typename... Args>
static StmtPtr prepareRequest( std::shared_ptr<sqlite3> dbConnectionPtr, const std::string& req, Args&&... args )
static StmtPtr prepareRequest( DBConnection dbConnection, const std::string& req, Args&&... args )
{
return _prepareRequest<1>( dbConnectionPtr, req, std::forward<Args>( args )... );
return _prepareRequest<1>( dbConnection, req, std::forward<Args>( args )... );
}
template <unsigned int>
static StmtPtr _prepareRequest( std::shared_ptr<sqlite3> dbConnection, const std::string& req )
static StmtPtr _prepareRequest( DBConnection dbConnection, const std::string& req )
{
assert( dbConnection != nullptr );
sqlite3_stmt* stmt = nullptr;
int res = sqlite3_prepare_v2( dbConnection.get(), req.c_str(), -1, &stmt, NULL );
int res = sqlite3_prepare_v2( dbConnection->getConn(), req.c_str(), -1, &stmt, NULL );
if ( res != SQLITE_OK )
{
LOG_ERROR( "Failed to execute request: ", req,
sqlite3_errmsg( dbConnection.get() ) );
LOG_ERROR( "Failed to execute request: ", req, ' ',
sqlite3_errmsg( dbConnection->getConn() ) );
}
return StmtPtr( stmt, &sqlite3_finalize );
}
template <unsigned int COLIDX, typename T, typename... Args>
static StmtPtr _prepareRequest( std::shared_ptr<sqlite3> dbConnectionPtr, const std::string& req, T&& arg, Args&&... args )
static StmtPtr _prepareRequest( DBConnection dbConnection, const std::string& req, T&& arg, Args&&... args )
{
auto stmt = _prepareRequest<COLIDX + 1>( dbConnectionPtr, req, std::forward<Args>( args )... );
auto stmt = _prepareRequest<COLIDX + 1>( dbConnection, req, std::forward<Args>( args )... );
Traits<T>::Bind( stmt.get(), COLIDX, std::forward<T>( arg ) );
return stmt;
}
template <typename... Args>
static bool executeRequest( std::shared_ptr<sqlite3> dbConnection, const std::string& req, Args&&... args )
{
auto stmt = prepareRequest( dbConnection, req, std::forward<Args>( args )... );
if ( stmt == nullptr )
return false;
int res;
do
{
res = sqlite3_step( stmt.get() );
} while ( res == SQLITE_ROW );
if ( res != SQLITE_DONE )
{
#if SQLITE_VERSION_NUMBER >= 3007015
auto err = sqlite3_errstr( res );
#else
auto err = res;
#endif
LOG_ERROR( "Failed to execute <", req, ">\nInvalid result: ", err,
": ", sqlite3_errmsg( dbConnection.get() ) );
return false;
}
return true;
}
};
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment