45#if defined( __linux__ )
47#elif defined( __APPLE__ )
66CALL_RESULT failed(
DB_FAILURE aFailure, std::string aDetail )
68 return {
false, aFailure, std::move( aDetail ) };
76 std::vector<std::string> names;
79 std::vector<DB_COLUMN> columns;
83std::filesystem::path pathFromUtf8(
const std::string& aPath )
85 return std::filesystem::path( std::u8string( aPath.begin(), aPath.end() ) );
89std::string quoted(
const std::string& aIdentifier )
93 for(
char c : aIdentifier )
105CALL_RESULT sqliteError( sqlite3* aDb,
int aCode )
107 const std::string
message = aDb ? sqlite3_errmsg( aDb ) : sqlite3_errstr( aCode );
110 if( aCode == SQLITE_IOERR_LOCK )
113 switch( aCode & 0xFF )
128 STATEMENT( sqlite3* aDb,
const std::string& aSql ) :
131 m_code = sqlite3_prepare_v2( aDb, aSql.c_str(),
static_cast<int>( aSql.size() ), &m_stmt,
nullptr );
134 ~STATEMENT() { sqlite3_finalize( m_stmt ); }
136 STATEMENT(
const STATEMENT& ) =
delete;
137 STATEMENT& operator=(
const STATEMENT& ) =
delete;
139 bool Ok()
const {
return m_code == SQLITE_OK; }
141 CALL_RESULT
Error()
const {
return sqliteError( m_db, m_code ); }
143 sqlite3_stmt* Get()
const {
return m_stmt; }
145 int Step() {
return sqlite3_step( m_stmt ); }
147 std::string
Text(
int aColumn )
const
149 const unsigned char*
text = sqlite3_column_text( m_stmt, aColumn );
150 const int bytes = sqlite3_column_bytes( m_stmt, aColumn );
152 return text ? std::string(
reinterpret_cast<const char*
>(
text ),
static_cast<size_t>(
bytes ) )
158 sqlite3_stmt* m_stmt =
nullptr;
159 int m_code = SQLITE_OK;
170 bool operator==(
const FILE_ID& aOther )
const
172 return valid == aOther.valid && volume == aOther.volume && index == aOther.index;
177FILE_ID fileIdOf(
const std::string& aPath )
182 HANDLE file = CreateFileW( pathFromUtf8( aPath ).c_str(), 0,
183 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING,
184 FILE_FLAG_BACKUP_SEMANTICS,
nullptr );
186 if( file == INVALID_HANDLE_VALUE )
189 BY_HANDLE_FILE_INFORMATION
info;
191 if( GetFileInformationByHandle( file, &
info ) )
194 id.volume =
info.dwVolumeSerialNumber;
195 id.index = (
static_cast<uint64_t
>(
info.nFileIndexHigh ) << 32 ) |
info.nFileIndexLow;
202 if( stat( aPath.c_str(), &
info ) == 0 )
205 id.volume =
static_cast<uint64_t
>(
info.st_dev );
206 id.index =
static_cast<uint64_t
>(
info.st_ino );
215int journalFormat(
const std::string& aPath )
217 std::ifstream file( pathFromUtf8( aPath ), std::ios::binary );
218 char header[20] = {};
220 if( !file.read( header,
sizeof header ) )
223 return header[18] == header[19] ? header[18] : 0;
229bool closeWhenIdleFor(
const std::string& aPath )
243 SQLITE_DB( std::string aPath, std::string aUri,
int aTimeout ) :
264 catch(
const std::system_error& )
292 static void closeHandle( sqlite3* aHandle ) { sqlite3_close_v2( aHandle ); }
311 explicit READ_SNAPSHOT(
SQLITE_DB& aDb ) :
320 if( m_db.closeWhenIdle )
324 READ_SNAPSHOT(
const READ_SNAPSHOT& ) =
delete;
325 READ_SNAPSHOT& operator=(
const READ_SNAPSHOT& ) =
delete;
329 CALL_RESULT
result = m_db.Refresh();
336 const int code = sqlite3_exec( m_db.handle,
"COMMIT",
nullptr,
nullptr,
nullptr );
338 if( code != SQLITE_OK )
339 return sqliteError( m_db.handle, code );
349 if( m_open && m_db.handle )
350 sqlite3_exec( m_db.handle,
"ROLLBACK",
nullptr,
nullptr,
nullptr );
358 sqlite3* db = m_db.handle;
359 int code = sqlite3_exec( db,
"BEGIN",
nullptr,
nullptr,
nullptr );
361 if( code != SQLITE_OK )
362 return sqliteError( db, code );
367 STATEMENT probe( db,
"SELECT 1 FROM sqlite_master LIMIT 1" );
370 return probe.Error();
374 if( code != SQLITE_ROW && code != SQLITE_DONE )
375 return sqliteError( db, code );
381 STATEMENT pragma( db,
"PRAGMA main.journal_mode" );
384 return pragma.Error();
386 if( pragma.Step() == SQLITE_ROW )
387 mode = pragma.Text( 0 );
390 std::transform( mode.begin(), mode.end(), mode.begin(),
391 [](
unsigned char c )
393 return static_cast<char>( std::tolower( c ) );
414 if( !std::filesystem::exists( pathFromUtf8(
path ), ec ) )
422 static constexpr int ATTEMPTS = 3;
424 for(
int attempt = 1;; ++attempt )
426 const FILE_ID before = fileIdOf(
path );
427 const int code = sqlite3_open_v2(
uri.c_str(), &
handle, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI,
nullptr );
429 if( code != SQLITE_OK )
437 sqlite3_extended_result_codes(
handle, 1 );
443 const FILE_ID after = fileIdOf(
path );
445 if( !before.valid || !after.valid || before == after || attempt >= ATTEMPTS )
462 READ_SNAPSHOT snapshot( *
this );
464 result = snapshot.Begin();
467 result = snapshot.Commit();
479 const FILE_ID now = fileIdOf(
path );
481 if(
handle && ( !now.valid || now ==
id ) )
493int typeCode(
const std::string& aDeclared )
495 std::string upper = aDeclared;
497 std::transform( upper.begin(), upper.end(), upper.begin(),
498 [](
unsigned char c )
500 return static_cast<char>( std::toupper( c ) );
503 auto has = [&](
const char* aPart )
505 return upper.find( aPart ) != std::string::npos;
511 if( has(
"CHAR" ) || has(
"CLOB" ) || has(
"TEXT" ) )
517 if( has(
"REAL" ) || has(
"FLOA" ) || has(
"DOUB" ) )
525CALL_RESULT readColumns( sqlite3* aDb,
const std::string& aTable, std::vector<DB_COLUMN>& aColumns )
527 STATEMENT
info( aDb,
"PRAGMA table_xinfo(" + quoted( aTable ) +
")" );
534 while( ( code =
info.Step() ) == SQLITE_ROW )
536 const int hidden = sqlite3_column_int(
info.Get(), 6 );
545 column.
type = typeCode(
info.Text( 2 ) );
547 aColumns.push_back( std::move( column ) );
550 if( code != SQLITE_DONE )
551 return sqliteError( aDb, code );
557CATALOG_RESULT listTables(
SQLITE_DB& aConn )
560 READ_SNAPSHOT snapshot( aConn );
562 result.call = snapshot.Begin();
567 sqlite3* db = aConn.
handle;
570 STATEMENT list( db,
"SELECT name FROM sqlite_master WHERE type IN ('table', 'view') "
571 "AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' ORDER BY name" );
575 result.call = list.Error();
581 while( ( code = list.Step() ) == SQLITE_ROW )
582 result.names.push_back( list.Text( 0 ) );
584 if( code != SQLITE_DONE )
586 result.call = sqliteError( db, code );
591 result.call = snapshot.Commit();
596CATALOG_RESULT describeTable(
SQLITE_DB& aConn,
const std::string& aTable )
599 READ_SNAPSHOT snapshot( aConn );
601 result.call = snapshot.Begin();
606 sqlite3* db = aConn.
handle;
612 STATEMENT
find( db,
"SELECT name FROM sqlite_master WHERE type IN ('table', 'view') "
613 "AND name = ?1 COLLATE NOCASE" );
621 sqlite3_bind_text(
find.Get(), 1, aTable.c_str(),
static_cast<int>( aTable.size() ), SQLITE_TRANSIENT );
625 while( ( code =
find.Step() ) == SQLITE_ROW )
627 const std::string
name =
find.Text( 0 );
637 if( code != SQLITE_DONE )
639 result.call = sqliteError( db, code );
652 result.call = snapshot.Commit();
657DB_VALUE cellValue( sqlite3_stmt* aStmt,
int aColumn )
659 switch( sqlite3_column_type( aStmt, aColumn ) )
661 case SQLITE_INTEGER:
return static_cast<int64_t
>( sqlite3_column_int64( aStmt, aColumn ) );
662 case SQLITE_FLOAT:
return sqlite3_column_double( aStmt, aColumn );
666 const unsigned char*
text = sqlite3_column_text( aStmt, aColumn );
667 const int bytes = sqlite3_column_bytes( aStmt, aColumn );
669 return std::string(
reinterpret_cast<const char*
>(
text ),
static_cast<size_t>(
bytes ) );
673 case SQLITE_BLOB:
return std::string();
674 default:
return std::monostate();
685 const bool unc = aWindowsPath &&
path.rfind(
"//", 0 ) == 0;
686 const bool drive = aWindowsPath &&
path.size() >= 2 &&
path[1] ==
':'
687 && std::isalpha(
static_cast<unsigned char>(
path[0] ) );
692 const size_t first =
path.find_first_not_of(
'/' );
694 if( first != std::string::npos && first > 1 )
695 path.erase( 0, first - 1 );
698 static const char hex[] =
"0123456789ABCDEF";
701 for(
size_t i = 0; i <
path.size(); ++i )
703 const unsigned char c =
static_cast<unsigned char>(
path[i] );
704 const bool plain = std::isalnum( c ) && c < 0x80;
706 if( plain || c ==
'-' || c ==
'.' || c ==
'_' || c ==
'~' || c ==
'/' || ( drive && i == 1 && c ==
':' ) )
708 encoded +=
static_cast<char>( c );
713 encoded += hex[c >> 4];
714 encoded += hex[c & 0x0F];
719 return "file://" + encoded +
"?mode=ro";
721 return std::string(
"file:" ) + ( drive ?
"/" :
"" ) + encoded +
"?mode=ro";
729 return path.size() > 2 &&
path[0] ==
'/' &&
path[1] ==
'/' &&
path[2] !=
'/';
735 static const char* remote[] = {
"sshfs",
"rclone",
737 "smbnetfs",
"google-drive-ocamlfuse",
740 auto unescape = [](
const std::string& aField )
744 for(
size_t i = 0; i < aField.size(); ++i )
746 if( aField[i] ==
'\\' && i + 3 < aField.size() && std::isdigit( aField[i + 1] ) )
748 result +=
static_cast<char>( std::stoi( aField.substr( i + 1, 3 ),
nullptr, 8 ) );
760 size_t bestLength = 0;
762 std::string bestType;
763 size_t lineStart = 0;
765 while( lineStart < aMountInfo.size() )
767 size_t lineEnd = aMountInfo.find(
'\n', lineStart );
769 if( lineEnd == std::string::npos )
770 lineEnd = aMountInfo.size();
772 const std::string line = aMountInfo.substr( lineStart, lineEnd - lineStart );
774 lineStart = lineEnd + 1;
776 const size_t separator = line.find(
" - " );
778 if( separator == std::string::npos )
781 std::vector<std::string> head;
783 for(
size_t pos = 0; pos < separator; )
785 size_t end = line.find(
' ', pos );
787 if(
end == std::string::npos ||
end > separator )
790 head.push_back( line.substr( pos,
end - pos ) );
794 if( head.size() < 5 )
797 const std::string mount = unescape( head[4] );
798 const size_t typeEnd = line.find(
' ', separator + 3 );
799 const std::string type = line.substr( separator + 3, typeEnd == std::string::npos ? std::string::npos
800 : typeEnd - separator - 3 );
802 const bool inside = mount ==
"/" || aPath == mount
803 || ( aPath.size() > mount.size() && aPath.compare( 0, mount.size(), mount ) == 0
804 && aPath[mount.size()] ==
'/' );
806 if( inside && mount.size() >= bestLength )
808 bestLength = mount.size();
814 if( !found || bestType.rfind(
"fuse.", 0 ) != 0 )
817 const std::string subtype = bestType.substr( 5 );
819 return std::any_of( std::begin( remote ), std::end( remote ),
820 [&](
const char* aName )
822 return subtype == aName;
831std::string resolvedWindowsPath(
const std::string& aPath )
833 const std::wstring wide = wxString::FromUTF8( aPath ).ToStdWstring();
834 std::wstring resolved = wide;
835 std::wstring opened = wide;
837 if( DWORD size = GetFullPathNameW( wide.c_str(), 0,
nullptr,
nullptr ); size > 0 )
839 std::vector<wchar_t> full( size );
840 DWORD length = GetFullPathNameW( wide.c_str(), size, full.data(),
nullptr );
842 if( length > 0 && length < size )
843 opened.assign( full.data(), length );
847 if( opened.size() > 2 && opened[1] ==
L':' && opened[2] ==
L'\\' )
848 opened =
L"\\\\?\\" + opened;
850 HANDLE handle = CreateFileW( opened.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
851 OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS,
nullptr );
853 if( handle != INVALID_HANDLE_VALUE )
855 std::vector<wchar_t> buffer( MAX_PATH + 1 );
856 DWORD length = GetFinalPathNameByHandleW( handle, buffer.data(),
static_cast<DWORD
>( buffer.size() ),
859 if( length >= buffer.size() )
861 buffer.resize( length + 1 );
862 length = GetFinalPathNameByHandleW( handle, buffer.data(),
static_cast<DWORD
>( buffer.size() ),
866 if( length > 0 && length < buffer.size() )
867 resolved.assign( buffer.data(), length );
869 CloseHandle( handle );
872 static const std::wstring extended =
L"\\\\?\\";
873 static const std::wstring extendedUnc =
L"\\\\?\\UNC\\";
876 if( resolved.compare( 0, extendedUnc.size(), extendedUnc ) == 0 )
877 resolved =
L"\\\\" + resolved.substr( extendedUnc.size() );
878 else if( resolved.compare( 0, extended.size(), extended ) == 0 )
879 resolved = resolved.substr( extended.size() );
881 return wxString( resolved ).ToUTF8().data();
894 const std::string target = resolvedWindowsPath( aPath );
899 const std::wstring wide = wxString::FromUTF8( target ).ToStdWstring();
900 std::vector<wchar_t> root( std::max<size_t>( wide.size(), MAX_PATH ) + 1,
L'\0' );
902 if( !GetVolumePathNameW( wide.c_str(), root.data(),
static_cast<DWORD
>( root.size() ) ) )
905 return GetDriveTypeW( root.data() ) == DRIVE_REMOTE;
906#elif defined( __linux__ )
909 if( statfs( aPath.c_str(), &
info ) != 0 )
913 switch(
static_cast<uint32_t
>(
info.f_type ) )
931 std::string real = std::filesystem::weakly_canonical( pathFromUtf8( aPath ), ec ).string();
932 std::ifstream mounts(
"/proc/self/mountinfo" );
933 std::string
text( ( std::istreambuf_iterator<char>( mounts ) ), std::istreambuf_iterator<char>() );
938 default:
return false;
940#elif defined( __APPLE__ )
943 if( statfs( aPath.c_str(), &
info ) != 0 )
946 return (
info.f_flags & MNT_LOCAL ) == 0;
984static std::string
describeFailure(
const std::string& aPath,
const CALL_RESULT& aResult )
986 const wxString
path = wxString::FromUTF8( aPath );
988 switch( aResult.failure )
991 return utf8( wxString::Format(
_(
"database file %s was not found" ),
path ) );
994 return utf8( wxString::Format(
_(
"database file %s is locked by another program (%s)" ),
path,
995 wxString::FromUTF8( aResult.detail ) ) );
998 return utf8( wxString::Format(
_(
"database file %s uses write-ahead logging on a network drive, which "
999 "SQLite cannot read safely; set journal_mode=DELETE on the database" ),
1003 return utf8( wxString::Format(
_(
"database file %s could not be reached (%s)" ),
path,
1004 wxString::FromUTF8( aResult.detail ) ) );
1007 return utf8( wxString::Format(
_(
"database file %s could not be read (%s)" ),
path,
1008 wxString::FromUTF8( aResult.detail ) ) );
1021template <
typename R>
1044 return std::nullopt;
1050 fail( aFailure, std::move( aMessage ) );
1069 std::shared_ptr<SQLITE_DB> db = std::move(
m_db );
1101 const bool windows =
true;
1103 const bool windows =
false;
1111 return db->Connect();
1144 std::shared_ptr<SQLITE_DB> db =
m_db;
1149 return listTables( *db );
1163 aTables = std::move(
result->names );
1164 std::sort( aTables.begin(), aTables.end() );
1176 return std::nullopt;
1178 std::shared_ptr<SQLITE_DB> db =
m_db;
1183 return describeTable( *db, aTable );
1188 return std::nullopt;
1194 return std::nullopt;
1198 return std::nullopt;
1210 size_t aMaxRows, std::vector<std::string>& aLabels,
const DB_ROW_SINK& aSink )
1218 std::string sql =
"SELECT ";
1220 if( aColumns.empty() )
1226 for(
size_t i = 0; i < aColumns.size(); ++i )
1227 sql += ( i ?
", " :
"" ) + quoted( aColumns[i] );
1230 sql +=
" FROM " + quoted( aTable.
name );
1233 sql +=
" ORDER BY 1 LIMIT " + std::to_string( aMaxRows );
1236 auto labels = delivery.
Labels();
1237 std::shared_ptr<SQLITE_DB> db =
m_db;
1238 const bool fetchRows = aMaxRows != 0;
1241 [db, sql, labels, fetchRows](
DB_JOB_CONTEXT& aContext ) -> CALL_RESULT
1243 READ_SNAPSHOT snapshot( *db );
1244 CALL_RESULT began = snapshot.Begin();
1250 STATEMENT query( db->handle, sql );
1253 return query.Error();
1255 const int count = sqlite3_column_count( query.Get() );
1257 for(
int i = 0; i < count; ++i )
1258 labels->emplace_back( sqlite3_column_name( query.Get(), i ) );
1260 int code = SQLITE_ROW;
1262 while( fetchRows && ( code = query.Step() ) == SQLITE_ROW )
1264 std::vector<DB_VALUE> row;
1265 row.reserve(
static_cast<size_t>( count ) );
1267 for(
int i = 0; i < count; ++i )
1268 row.push_back( cellValue( query.Get(), i ) );
1270 if( !aContext.
Emit( std::move( row ) ) )
1274 if( fetchRows && code != SQLITE_DONE )
1275 return sqliteError( db->handle, code );
1278 if( !aContext.
Flush() )
1281 return snapshot.Commit();
static std::string describeFailure(const std::string &aPath, const CALL_RESULT &aResult, const std::string &aPassword)
Composes the message for a failed call, naming the file.
static bool invalidates(DB_FAILURE aFailure)
A failure after which the connection can no longer be trusted.
bool operator==(const wxAuiPaneInfo &aLhs, const wxAuiPaneInfo &aRhs)
static constexpr size_t ALL_ROWS
Passed as ReadTable()'s row limit to read every row.
What a job running on a DB_WORKER_THREAD uses to hand rows to the caller.
bool Flush()
Delivers the rows still held back and waits until the caller's sink has taken every one.
bool Emit(std::vector< DB_VALUE > &&aRow)
Queues a row for the caller's sink.
Hands rows from a worker job to a caller's sink and gives the caller the column labels.
bool SinkThrew() const
True if the caller's sink threw.
const std::shared_ptr< std::vector< std::string > > & Labels() const
The labels the job fills in.
void Finish()
Copies the labels out if no row did, after the call returned a result.
const DB_ROW_SINK * Sink() const
The sink to pass to the worker call.
static std::string SinkFailureMessage()
The message for a read the caller's sink gave up on.
Runs one job at a time on its own thread so that a call stuck in filesystem I/O, which nothing can in...
static std::chrono::milliseconds CallerBound(int aTimeoutSeconds)
static std::string NormalizePath(const std::string &aPath, bool aWindowsPath)
Rewrites the spellings of one file that Windows treats alike into one form, without touching the file...
static bool OnWorkerThread()
static bool IsUncPath(const std::string &aPath)
std::unique_ptr< DB_WORKER_THREAD > m_worker
void failCall(DB_FAILURE aFailure, std::string aMessage)
Records a failure of a call that ran, and drops the connection if the failure means it cannot be trus...
bool ListTables(std::vector< std::string > &aTables) override
Lists the tables and views, sorted by name.
void fail(DB_FAILURE aFailure, std::string aError)
SQLITE_BACKEND(std::string aPath, int aTimeoutSeconds)
~SQLITE_BACKEND() override
bool ReadTable(const DB_TABLE_INFO &aTable, const std::vector< std::string > &aColumns, size_t aMaxRows, std::vector< std::string > &aLabels, const DB_ROW_SINK &aSink) override
Reads rows of a table, handing each to aSink.
static std::string FileUri(const std::string &aPath, bool aWindowsPath)
Builds the read-only SQLite URI for a file.
void Disconnect() override
bool requireConnection()
Fails with a message and returns false unless a connection is open.
std::chrono::milliseconds callerBound() const
std::atomic< bool > m_connected
std::shared_ptr< SQLITE_DB > m_db
void dropConnection()
Forgets the connection and closes it on a worker thread, never on the caller.
static bool IsNetworkPath(const std::string &aPath)
std::optional< R > runOnWorker(std::function< R(DB_JOB_CONTEXT &)> aJob, const DB_ROW_SINK *aSink)
Runs aJob on the worker, recording why it did not finish as the failure.
static bool IsRemoteFuseMount(const std::string &aMountInfo, const std::string &aPath)
Decides from the text of /proc/self/mountinfo whether aPath lies on a FUSE mount that reaches a remot...
std::optional< DB_TABLE_INFO > DescribeTable(const std::string &aTable) override
std::function< void(std::vector< DB_VALUE > &&)> DB_ROW_SINK
Receives one row at a time, in the order of the labels the read produced.
DB_FAILURE
Why a connection attempt failed, as far as the backend's report tells.
@ NETWORK_WAL
A write-ahead-log database on a network path cannot be read safely.
@ UNREACHABLE
Nothing answered at the server address, or it refused the connection.
@ FILE_IN_USE
The database file is locked by another process.
@ FILE_NOT_FOUND
The database file does not exist.
std::variant< std::monostate, std::string, int64_t, double, bool > DB_VALUE
A single database cell, where monostate is SQL NULL.
DB_WORKER_FAILURE DescribeWorkerOutcome(DB_WORKER_THREAD::OUTCOME aOutcome, const std::string &aPath)
Maps the outcome of a call that returned no result to a failure and a connection decision.
std::string utf8(const wxString &aText)
const wxChar *const traceDatabase
Flag to enable database library tracing.
static bool invalidates(DB_FAILURE aFailure)
A failure after which the connection can no longer be trusted.
static std::string describeFailure(const std::string &aPath, const CALL_RESULT &aResult)
Composes the message for a failed call, naming the file.
A column as the backend reports it.
bool supported
False for columns the backend can never read, such as attachments.
std::string name
The table's name as the backend spells it.
What a backend reports, and what it does with its connection, when a worker call did not complete.
@ DROP
The connection is closed through the worker.
@ FORGET
The stuck job releases the connection itself when its blocked call returns.
Owns one connection, which a worker thread opens and uses.
CALL_RESULT Refresh()
Reopens the connection if the path now names a different file than the one opened.
CALL_RESULT Connect()
Opens the file and proves it is a database.
SQLITE_DB & operator=(const SQLITE_DB &)=delete
static void closeHandle(sqlite3 *aHandle)
SQLITE_DB(const SQLITE_DB &)=delete
SQLITE_DB(std::string aPath, std::string aUri, int aTimeout)
static std::vector< uint8_t > bytes(const std::string &aText)
wxString result
Test unit parsing edge cases and error handling.