KiCad PCB EDA Suite
Loading...
Searching...
No Matches
sqlite_backend.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include "sqlite_backend.h"
21
22#include "db_backend_support.h"
23
24#include <algorithm>
25#include <cctype>
26#include <cstdint>
27#include <filesystem>
28#include <fstream>
29#include <iterator>
30#include <mutex>
31#include <thread>
32
33#include <sqlite3.h>
34
35#include <wx/intl.h>
36#include <wx/log.h>
37#include <wx/string.h>
38
39#if defined( _WIN32 )
40#include <windows.h>
41#else
42#include <sys/stat.h>
43#endif
44
45#if defined( __linux__ )
46#include <sys/vfs.h>
47#elif defined( __APPLE__ )
48#include <sys/mount.h>
49#include <sys/param.h>
50#endif
51
52
53namespace
54{
55
56
58struct CALL_RESULT
59{
60 bool ok = true;
62 std::string detail;
63};
64
65
66CALL_RESULT failed( DB_FAILURE aFailure, std::string aDetail )
67{
68 return { false, aFailure, std::move( aDetail ) };
69}
70
71
73struct CATALOG_RESULT
74{
75 CALL_RESULT call;
76 std::vector<std::string> names;
77 std::string table;
78 bool found = false;
79 std::vector<DB_COLUMN> columns;
80};
81
82
83std::filesystem::path pathFromUtf8( const std::string& aPath )
84{
85 return std::filesystem::path( std::u8string( aPath.begin(), aPath.end() ) );
86}
87
88
89std::string quoted( const std::string& aIdentifier )
90{
91 std::string result = "\"";
92
93 for( char c : aIdentifier )
94 {
95 if( c == '"' )
96 result += '"';
97
98 result += c;
99 }
100
101 return result + "\"";
102}
103
104
105CALL_RESULT sqliteError( sqlite3* aDb, int aCode )
106{
107 const std::string message = aDb ? sqlite3_errmsg( aDb ) : sqlite3_errstr( aCode );
108
109 // A failed lock request is a conflict with another program, not a lost file
110 if( aCode == SQLITE_IOERR_LOCK )
111 return failed( DB_FAILURE::FILE_IN_USE, message );
112
113 switch( aCode & 0xFF )
114 {
115 case SQLITE_BUSY:
116 case SQLITE_LOCKED: return failed( DB_FAILURE::FILE_IN_USE, message );
117 case SQLITE_IOERR:
118 case SQLITE_CANTOPEN: return failed( DB_FAILURE::UNREACHABLE, message );
119 default: return failed( DB_FAILURE::UNKNOWN, message );
120 }
121}
122
123
125class STATEMENT
126{
127public:
128 STATEMENT( sqlite3* aDb, const std::string& aSql ) :
129 m_db( aDb )
130 {
131 m_code = sqlite3_prepare_v2( aDb, aSql.c_str(), static_cast<int>( aSql.size() ), &m_stmt, nullptr );
132 }
133
134 ~STATEMENT() { sqlite3_finalize( m_stmt ); }
135
136 STATEMENT( const STATEMENT& ) = delete;
137 STATEMENT& operator=( const STATEMENT& ) = delete;
138
139 bool Ok() const { return m_code == SQLITE_OK; }
140
141 CALL_RESULT Error() const { return sqliteError( m_db, m_code ); }
142
143 sqlite3_stmt* Get() const { return m_stmt; }
144
145 int Step() { return sqlite3_step( m_stmt ); }
146
147 std::string Text( int aColumn ) const
148 {
149 const unsigned char* text = sqlite3_column_text( m_stmt, aColumn );
150 const int bytes = sqlite3_column_bytes( m_stmt, aColumn );
151
152 return text ? std::string( reinterpret_cast<const char*>( text ), static_cast<size_t>( bytes ) )
153 : std::string();
154 }
155
156private:
157 sqlite3* m_db;
158 sqlite3_stmt* m_stmt = nullptr;
159 int m_code = SQLITE_OK;
160};
161
162
164struct FILE_ID
165{
166 bool valid = false;
167 uint64_t volume = 0;
168 uint64_t index = 0;
169
170 bool operator==( const FILE_ID& aOther ) const
171 {
172 return valid == aOther.valid && volume == aOther.volume && index == aOther.index;
173 }
174};
175
176
177FILE_ID fileIdOf( const std::string& aPath )
178{
179 FILE_ID id;
180
181#ifdef _WIN32
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 );
185
186 if( file == INVALID_HANDLE_VALUE )
187 return id;
188
189 BY_HANDLE_FILE_INFORMATION info;
190
191 if( GetFileInformationByHandle( file, &info ) )
192 {
193 id.valid = true;
194 id.volume = info.dwVolumeSerialNumber;
195 id.index = ( static_cast<uint64_t>( info.nFileIndexHigh ) << 32 ) | info.nFileIndexLow;
196 }
197
198 CloseHandle( file );
199#else
200 struct stat info;
201
202 if( stat( aPath.c_str(), &info ) == 0 )
203 {
204 id.valid = true;
205 id.volume = static_cast<uint64_t>( info.st_dev );
206 id.index = static_cast<uint64_t>( info.st_ino );
207 }
208#endif
209
210 return id;
211}
212
213
215int journalFormat( const std::string& aPath )
216{
217 std::ifstream file( pathFromUtf8( aPath ), std::ios::binary );
218 char header[20] = {};
219
220 if( !file.read( header, sizeof header ) )
221 return 0;
222
223 return header[18] == header[19] ? header[18] : 0;
224}
225
226
227// SQLite opens files on Windows without FILE_SHARE_DELETE, so an idle handle stops a sync client from replacing a
228// local library. A share is not replaced that way, and reopening it every read upsets other clients' leases
229bool closeWhenIdleFor( const std::string& aPath )
230{
231#ifdef _WIN32
232 return !SQLITE_BACKEND::IsNetworkPath( aPath );
233#else
234 return false;
235#endif
236}
237} // namespace
238
239
242{
243 SQLITE_DB( std::string aPath, std::string aUri, int aTimeout ) :
244 path( std::move( aPath ) ),
245 uri( std::move( aUri ) ),
246 timeout( aTimeout )
247 {
248 }
249
250 // Whichever thread drops the last reference, it is never the caller that does the I/O
252 {
253 if( !handle )
254 return;
255
257 {
258 try
259 {
260 std::thread( [h = handle]() { closeHandle( h ); } ).detach();
261 handle = nullptr;
262 return;
263 }
264 catch( const std::system_error& )
265 {
266 // No thread to spare, so the close happens here after all
267 }
268 }
269
270 Close();
271 }
272
273 SQLITE_DB( const SQLITE_DB& ) = delete;
274 SQLITE_DB& operator=( const SQLITE_DB& ) = delete;
275
276 void Close()
277 {
278 if( handle )
280
281 handle = nullptr;
282 }
283
284 CALL_RESULT Open();
285
287 CALL_RESULT Connect();
288
290 CALL_RESULT Refresh();
291
292 static void closeHandle( sqlite3* aHandle ) { sqlite3_close_v2( aHandle ); }
293
294 sqlite3* handle = nullptr;
295 std::string path;
296 std::string uri;
298 FILE_ID id;
299 bool closeWhenIdle = false;
300};
301
302
303namespace
304{
305
306
308class READ_SNAPSHOT
309{
310public:
311 explicit READ_SNAPSHOT( SQLITE_DB& aDb ) :
312 m_db( aDb )
313 {
314 }
315
316 ~READ_SNAPSHOT()
317 {
318 rollback();
319
320 if( m_db.closeWhenIdle )
321 m_db.Close();
322 }
323
324 READ_SNAPSHOT( const READ_SNAPSHOT& ) = delete;
325 READ_SNAPSHOT& operator=( const READ_SNAPSHOT& ) = delete;
326
327 CALL_RESULT Begin()
328 {
329 CALL_RESULT result = m_db.Refresh();
330
331 return result.ok ? begin() : result;
332 }
333
334 CALL_RESULT Commit()
335 {
336 const int code = sqlite3_exec( m_db.handle, "COMMIT", nullptr, nullptr, nullptr );
337
338 if( code != SQLITE_OK )
339 return sqliteError( m_db.handle, code );
340
341 m_open = false;
342
343 return {};
344 }
345
346private:
347 void rollback()
348 {
349 if( m_open && m_db.handle )
350 sqlite3_exec( m_db.handle, "ROLLBACK", nullptr, nullptr, nullptr );
351
352 m_open = false;
353 }
354
355 // SQLite only learns the file's state once it reads, so step a query before asking the journal mode
356 CALL_RESULT begin()
357 {
358 sqlite3* db = m_db.handle;
359 int code = sqlite3_exec( db, "BEGIN", nullptr, nullptr, nullptr );
360
361 if( code != SQLITE_OK )
362 return sqliteError( db, code );
363
364 m_open = true;
365
366 {
367 STATEMENT probe( db, "SELECT 1 FROM sqlite_master LIMIT 1" );
368
369 if( !probe.Ok() )
370 return probe.Error();
371
372 code = probe.Step();
373
374 if( code != SQLITE_ROW && code != SQLITE_DONE )
375 return sqliteError( db, code );
376 }
377
378 std::string mode;
379
380 {
381 STATEMENT pragma( db, "PRAGMA main.journal_mode" );
382
383 if( !pragma.Ok() )
384 return pragma.Error();
385
386 if( pragma.Step() == SQLITE_ROW )
387 mode = pragma.Text( 0 );
388 }
389
390 std::transform( mode.begin(), mode.end(), mode.begin(),
391 []( unsigned char c )
392 {
393 return static_cast<char>( std::tolower( c ) );
394 } );
395
396 if( mode == "wal" && SQLITE_BACKEND::IsNetworkPath( m_db.path ) )
397 return failed( DB_FAILURE::NETWORK_WAL, std::string() );
398
399 return {};
400 }
401
402 SQLITE_DB& m_db;
403 bool m_open = false;
404};
405} // namespace
406
407
408CALL_RESULT SQLITE_DB::Open()
409{
410 closeWhenIdle = closeWhenIdleFor( path );
411
412 std::error_code ec;
413
414 if( !std::filesystem::exists( pathFromUtf8( path ), ec ) )
415 return failed( DB_FAILURE::FILE_NOT_FOUND, std::string() );
416
417 // A WAL database on a network drive is refused before SQLite can create a wal-index on the share
418 if( journalFormat( path ) == 2 && SQLITE_BACKEND::IsNetworkPath( path ) )
419 return failed( DB_FAILURE::NETWORK_WAL, std::string() );
420
421 // A rename can land between the open and the stat, so the file is opened again until both agree
422 static constexpr int ATTEMPTS = 3;
423
424 for( int attempt = 1;; ++attempt )
425 {
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 );
428
429 if( code != SQLITE_OK )
430 {
431 CALL_RESULT result = sqliteError( handle, code );
432
433 Close();
434 return result;
435 }
436
437 sqlite3_extended_result_codes( handle, 1 );
438 sqlite3_busy_timeout( handle, timeout * 1000 );
439
440 // Recording the identity from before the open lets Refresh see a swap that came later
441 id = before;
442
443 const FILE_ID after = fileIdOf( path );
444
445 if( !before.valid || !after.valid || before == after || attempt >= ATTEMPTS )
446 return {};
447
448 Close();
449 }
450}
451
452
454{
455 CALL_RESULT result = Open();
456
457 if( !result.ok )
458 return result;
459
460 // Opening reads nothing, so take a snapshot now to learn whether this is a database at all
461 {
462 READ_SNAPSHOT snapshot( *this );
463
464 result = snapshot.Begin();
465
466 if( result.ok )
467 result = snapshot.Commit();
468 }
469
470 if( !result.ok )
471 Close();
472
473 return result;
474}
475
476
478{
479 const FILE_ID now = fileIdOf( path );
480
481 if( handle && ( !now.valid || now == id ) )
482 return {};
483
484 Close();
485
486 return Open();
487}
488
489
490namespace
491{
493int typeCode( const std::string& aDeclared )
494{
495 std::string upper = aDeclared;
496
497 std::transform( upper.begin(), upper.end(), upper.begin(),
498 []( unsigned char c )
499 {
500 return static_cast<char>( std::toupper( c ) );
501 } );
502
503 auto has = [&]( const char* aPart )
504 {
505 return upper.find( aPart ) != std::string::npos;
506 };
507
508 if( has( "INT" ) )
509 return 4; // SQL_INTEGER
510
511 if( has( "CHAR" ) || has( "CLOB" ) || has( "TEXT" ) )
512 return 12; // SQL_VARCHAR
513
514 if( has( "BLOB" ) )
515 return -3; // SQL_VARBINARY
516
517 if( has( "REAL" ) || has( "FLOA" ) || has( "DOUB" ) )
518 return 8; // SQL_DOUBLE
519
520 return 2; // SQL_NUMERIC
521}
522
523
525CALL_RESULT readColumns( sqlite3* aDb, const std::string& aTable, std::vector<DB_COLUMN>& aColumns )
526{
527 STATEMENT info( aDb, "PRAGMA table_xinfo(" + quoted( aTable ) + ")" );
528
529 if( !info.Ok() )
530 return info.Error();
531
532 int code;
533
534 while( ( code = info.Step() ) == SQLITE_ROW )
535 {
536 const int hidden = sqlite3_column_int( info.Get(), 6 );
537
538 // 1 is a virtual table's hidden column, 2 and 3 are generated columns
539 if( hidden == 1 )
540 continue;
541
542 DB_COLUMN column;
543
544 column.name = info.Text( 1 );
545 column.type = typeCode( info.Text( 2 ) );
546 column.supported = column.type != -3;
547 aColumns.push_back( std::move( column ) );
548 }
549
550 if( code != SQLITE_DONE )
551 return sqliteError( aDb, code );
552
553 return {};
554}
555
556
557CATALOG_RESULT listTables( SQLITE_DB& aConn )
558{
559 CATALOG_RESULT result;
560 READ_SNAPSHOT snapshot( aConn );
561
562 result.call = snapshot.Begin();
563
564 if( !result.call.ok )
565 return result;
566
567 sqlite3* db = aConn.handle;
568
569 {
570 STATEMENT list( db, "SELECT name FROM sqlite_master WHERE type IN ('table', 'view') "
571 "AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' ORDER BY name" );
572
573 if( !list.Ok() )
574 {
575 result.call = list.Error();
576 return result;
577 }
578
579 int code;
580
581 while( ( code = list.Step() ) == SQLITE_ROW )
582 result.names.push_back( list.Text( 0 ) );
583
584 if( code != SQLITE_DONE )
585 {
586 result.call = sqliteError( db, code );
587 return result;
588 }
589 }
590
591 result.call = snapshot.Commit();
592 return result;
593}
594
595
596CATALOG_RESULT describeTable( SQLITE_DB& aConn, const std::string& aTable )
597{
598 CATALOG_RESULT result;
599 READ_SNAPSHOT snapshot( aConn );
600
601 result.call = snapshot.Begin();
602
603 if( !result.call.ok )
604 return result;
605
606 sqlite3* db = aConn.handle;
607
608 result.table = aTable;
609
610 // Scoped so the lookup statement is finalized before the snapshot commits
611 {
612 STATEMENT find( db, "SELECT name FROM sqlite_master WHERE type IN ('table', 'view') "
613 "AND name = ?1 COLLATE NOCASE" );
614
615 if( !find.Ok() )
616 {
617 result.call = find.Error();
618 return result;
619 }
620
621 sqlite3_bind_text( find.Get(), 1, aTable.c_str(), static_cast<int>( aTable.size() ), SQLITE_TRANSIENT );
622
623 int code;
624
625 while( ( code = find.Step() ) == SQLITE_ROW )
626 {
627 const std::string name = find.Text( 0 );
628
629 // The exact spelling wins when names differ only in case
630 if( !result.found || name == aTable )
631 {
632 result.found = true;
633 result.table = name;
634 }
635 }
636
637 if( code != SQLITE_DONE )
638 {
639 result.call = sqliteError( db, code );
640 return result;
641 }
642 }
643
644 if( result.found )
645 {
646 result.call = readColumns( db, result.table, result.columns );
647
648 if( !result.call.ok )
649 return result;
650 }
651
652 result.call = snapshot.Commit();
653 return result;
654}
655
656
657DB_VALUE cellValue( sqlite3_stmt* aStmt, int aColumn )
658{
659 switch( sqlite3_column_type( aStmt, aColumn ) )
660 {
661 case SQLITE_INTEGER: return static_cast<int64_t>( sqlite3_column_int64( aStmt, aColumn ) );
662 case SQLITE_FLOAT: return sqlite3_column_double( aStmt, aColumn );
663
664 case SQLITE_TEXT:
665 {
666 const unsigned char* text = sqlite3_column_text( aStmt, aColumn );
667 const int bytes = sqlite3_column_bytes( aStmt, aColumn );
668
669 return std::string( reinterpret_cast<const char*>( text ), static_cast<size_t>( bytes ) );
670 }
671
672 // There is no way to show raw bytes as a field value
673 case SQLITE_BLOB: return std::string();
674 default: return std::monostate();
675 }
676}
677
678} // namespace
679
680
681std::string SQLITE_BACKEND::FileUri( const std::string& aPath, bool aWindowsPath )
682{
683 std::string path = DB_WORKER_THREAD::NormalizePath( aPath, aWindowsPath );
684
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] ) );
688
689 // Extra leading slashes name the same root on Unix, and would otherwise read as an authority
690 if( !aWindowsPath )
691 {
692 const size_t first = path.find_first_not_of( '/' );
693
694 if( first != std::string::npos && first > 1 )
695 path.erase( 0, first - 1 );
696 }
697
698 static const char hex[] = "0123456789ABCDEF";
699 std::string encoded;
700
701 for( size_t i = 0; i < path.size(); ++i )
702 {
703 const unsigned char c = static_cast<unsigned char>( path[i] );
704 const bool plain = std::isalnum( c ) && c < 0x80;
705
706 if( plain || c == '-' || c == '.' || c == '_' || c == '~' || c == '/' || ( drive && i == 1 && c == ':' ) )
707 {
708 encoded += static_cast<char>( c );
709 continue;
710 }
711
712 encoded += '%';
713 encoded += hex[c >> 4];
714 encoded += hex[c & 0x0F];
715 }
716
717 // SQLite takes only an empty authority, so a UNC path keeps its two slashes as the path's own
718 if( unc )
719 return "file://" + encoded + "?mode=ro";
720
721 return std::string( "file:" ) + ( drive ? "/" : "" ) + encoded + "?mode=ro";
722}
723
724
725bool SQLITE_BACKEND::IsUncPath( const std::string& aPath )
726{
727 const std::string path = DB_WORKER_THREAD::NormalizePath( aPath, true );
728
729 return path.size() > 2 && path[0] == '/' && path[1] == '/' && path[2] != '/';
730}
731
732
733bool SQLITE_BACKEND::IsRemoteFuseMount( const std::string& aMountInfo, const std::string& aPath )
734{
735 static const char* remote[] = { "sshfs", "rclone",
736 "s3fs", "gcsfuse",
737 "smbnetfs", "google-drive-ocamlfuse",
738 "gvfsd-fuse" };
739
740 auto unescape = []( const std::string& aField )
741 {
742 std::string result;
743
744 for( size_t i = 0; i < aField.size(); ++i )
745 {
746 if( aField[i] == '\\' && i + 3 < aField.size() && std::isdigit( aField[i + 1] ) )
747 {
748 result += static_cast<char>( std::stoi( aField.substr( i + 1, 3 ), nullptr, 8 ) );
749 i += 3;
750 }
751 else
752 {
753 result += aField[i];
754 }
755 }
756
757 return result;
758 };
759
760 size_t bestLength = 0;
761 bool found = false;
762 std::string bestType;
763 size_t lineStart = 0;
764
765 while( lineStart < aMountInfo.size() )
766 {
767 size_t lineEnd = aMountInfo.find( '\n', lineStart );
768
769 if( lineEnd == std::string::npos )
770 lineEnd = aMountInfo.size();
771
772 const std::string line = aMountInfo.substr( lineStart, lineEnd - lineStart );
773
774 lineStart = lineEnd + 1;
775
776 const size_t separator = line.find( " - " );
777
778 if( separator == std::string::npos )
779 continue;
780
781 std::vector<std::string> head;
782
783 for( size_t pos = 0; pos < separator; )
784 {
785 size_t end = line.find( ' ', pos );
786
787 if( end == std::string::npos || end > separator )
788 end = separator;
789
790 head.push_back( line.substr( pos, end - pos ) );
791 pos = end + 1;
792 }
793
794 if( head.size() < 5 )
795 continue;
796
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 );
801
802 const bool inside = mount == "/" || aPath == mount
803 || ( aPath.size() > mount.size() && aPath.compare( 0, mount.size(), mount ) == 0
804 && aPath[mount.size()] == '/' );
805
806 if( inside && mount.size() >= bestLength )
807 {
808 bestLength = mount.size();
809 bestType = type;
810 found = true;
811 }
812 }
813
814 if( !found || bestType.rfind( "fuse.", 0 ) != 0 )
815 return false;
816
817 const std::string subtype = bestType.substr( 5 );
818
819 return std::any_of( std::begin( remote ), std::end( remote ),
820 [&]( const char* aName )
821 {
822 return subtype == aName;
823 } );
824}
825
826
827#if defined( _WIN32 )
828namespace
829{
831std::string resolvedWindowsPath( const std::string& aPath )
832{
833 const std::wstring wide = wxString::FromUTF8( aPath ).ToStdWstring();
834 std::wstring resolved = wide;
835 std::wstring opened = wide;
836
837 if( DWORD size = GetFullPathNameW( wide.c_str(), 0, nullptr, nullptr ); size > 0 )
838 {
839 std::vector<wchar_t> full( size );
840 DWORD length = GetFullPathNameW( wide.c_str(), size, full.data(), nullptr );
841
842 if( length > 0 && length < size )
843 opened.assign( full.data(), length );
844 }
845
846 // Past MAX_PATH the open needs the extended prefix unless the system enables long paths
847 if( opened.size() > 2 && opened[1] == L':' && opened[2] == L'\\' )
848 opened = L"\\\\?\\" + opened;
849
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 );
852
853 if( handle != INVALID_HANDLE_VALUE )
854 {
855 std::vector<wchar_t> buffer( MAX_PATH + 1 );
856 DWORD length = GetFinalPathNameByHandleW( handle, buffer.data(), static_cast<DWORD>( buffer.size() ),
857 VOLUME_NAME_DOS );
858
859 if( length >= buffer.size() )
860 {
861 buffer.resize( length + 1 );
862 length = GetFinalPathNameByHandleW( handle, buffer.data(), static_cast<DWORD>( buffer.size() ),
863 VOLUME_NAME_DOS );
864 }
865
866 if( length > 0 && length < buffer.size() )
867 resolved.assign( buffer.data(), length );
868
869 CloseHandle( handle );
870 }
871
872 static const std::wstring extended = L"\\\\?\\";
873 static const std::wstring extendedUnc = L"\\\\?\\UNC\\";
874
875 // The prefix means a UNC path only when UNC follows it
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() );
880
881 return wxString( resolved ).ToUTF8().data();
882}
883} // namespace
884#endif
885
886
887bool SQLITE_BACKEND::IsNetworkPath( const std::string& aPath )
888{
889#if defined( _WIN32 )
890 if( IsUncPath( aPath ) )
891 return true;
892
893 // A junction or symlink may lead from a local folder to a share, so the target is classified
894 const std::string target = resolvedWindowsPath( aPath );
895
896 if( IsUncPath( target ) )
897 return true;
898
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' );
901
902 if( !GetVolumePathNameW( wide.c_str(), root.data(), static_cast<DWORD>( root.size() ) ) )
903 return false;
904
905 return GetDriveTypeW( root.data() ) == DRIVE_REMOTE;
906#elif defined( __linux__ )
907 struct statfs info;
908
909 if( statfs( aPath.c_str(), &info ) != 0 )
910 return false;
911
912 // Magic numbers from linux/magic.h, written out so the build does not depend on that header
913 switch( static_cast<uint32_t>( info.f_type ) )
914 {
915 case 0x6969: // NFS
916 case 0x517B: // SMB
917 case 0xFF534D42: // CIFS
918 case 0xFE534D42: // SMB2
919 case 0x73757245: // Coda
920 case 0x5346414F: // AFS
921 case 0x564C: // NCP
922 case 0x01021997: // 9p
923 case 0x00C36400: // Ceph
924 case 0x0BD00BD0: // Lustre
925 case 0x01161970: // GFS2
926 case 0x7461636F: // OCFS2
927 return true;
928 case 0x65735546: // FUSE, which is network only for the remote subtypes
929 {
930 std::error_code ec;
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>() );
934
935 return IsRemoteFuseMount( text, ec ? aPath : real );
936 }
937
938 default: return false;
939 }
940#elif defined( __APPLE__ )
941 struct statfs info;
942
943 if( statfs( aPath.c_str(), &info ) != 0 )
944 return false;
945
946 return ( info.f_flags & MNT_LOCAL ) == 0;
947#else
948 return false;
949#endif
950}
951
952
953std::chrono::milliseconds SQLITE_BACKEND::callerBound() const
954{
956}
957
958
959SQLITE_BACKEND::SQLITE_BACKEND( std::string aPath, int aTimeoutSeconds ) :
960 m_path( std::move( aPath ) ),
961 m_timeout( aTimeoutSeconds ),
962 m_worker( std::make_unique<DB_WORKER_THREAD>( DB_WORKER_THREAD::SourceKeyFor( m_path ) ) )
963{
964 m_worker->SetShutdownBound( DB_WORKER_THREAD::CallerBound( m_timeout ) );
965}
966
967
972
973
974void SQLITE_BACKEND::fail( DB_FAILURE aFailure, std::string aError )
975{
976 m_lastFailure = aFailure;
977 m_lastError = std::move( aError );
978
979 wxLogTrace( traceDatabase, wxT( "SQLITE_BACKEND: %s" ), wxString::FromUTF8( m_lastError ) );
980}
981
982
984static std::string describeFailure( const std::string& aPath, const CALL_RESULT& aResult )
985{
986 const wxString path = wxString::FromUTF8( aPath );
987
988 switch( aResult.failure )
989 {
991 return utf8( wxString::Format( _( "database file %s was not found" ), path ) );
992
994 return utf8( wxString::Format( _( "database file %s is locked by another program (%s)" ), path,
995 wxString::FromUTF8( aResult.detail ) ) );
996
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" ),
1000 path ) );
1001
1003 return utf8( wxString::Format( _( "database file %s could not be reached (%s)" ), path,
1004 wxString::FromUTF8( aResult.detail ) ) );
1005
1006 default:
1007 return utf8( wxString::Format( _( "database file %s could not be read (%s)" ), path,
1008 wxString::FromUTF8( aResult.detail ) ) );
1009 }
1010}
1011
1012
1014static bool invalidates( DB_FAILURE aFailure )
1015{
1016 return aFailure == DB_FAILURE::NETWORK_WAL || aFailure == DB_FAILURE::UNREACHABLE
1017 || aFailure == DB_FAILURE::FILE_NOT_FOUND;
1018}
1019
1020
1021template <typename R>
1022std::optional<R> SQLITE_BACKEND::runOnWorker( std::function<R( DB_JOB_CONTEXT& )> aJob, const DB_ROW_SINK* aSink )
1023{
1024 std::optional<R> result = m_worker->Run<R>( std::move( aJob ), callerBound(), aSink );
1025
1026 if( result )
1027 return result;
1028
1029 const DB_WORKER_FAILURE failure = DescribeWorkerOutcome( m_worker->LastOutcome(), m_path );
1030
1032 {
1034 }
1036 {
1037 // The abandoned job holds the last reference and closes the connection when the blocked call returns
1038 m_connected = false;
1039 m_db.reset();
1040 }
1041
1042 fail( failure.failure, failure.message );
1043
1044 return std::nullopt;
1045}
1046
1047
1048void SQLITE_BACKEND::failCall( DB_FAILURE aFailure, std::string aMessage )
1049{
1050 fail( aFailure, std::move( aMessage ) );
1051
1052 if( invalidates( aFailure ) )
1054}
1055
1056
1058{
1059 if( m_db )
1060 return true;
1061
1062 fail( DB_FAILURE::UNKNOWN, utf8( _( "not connected to a database" ) ) );
1063 return false;
1064}
1065
1066
1068{
1069 std::shared_ptr<SQLITE_DB> db = std::move( m_db );
1070
1071 m_connected = false;
1072
1073 if( !db )
1074 return;
1075
1076 // Copies of the closure may outlive the call, so the close is explicit rather than left to the last reference
1077 m_worker->RunCleanup(
1078 [db]()
1079 {
1080 db->Close();
1081 },
1082 callerBound() );
1083}
1084
1085
1087{
1088 m_lastError.clear();
1090
1091 if( m_connected )
1092 return true;
1093
1094 if( m_path.empty() )
1095 {
1096 fail( DB_FAILURE::FILE_NOT_FOUND, utf8( _( "no database file is set" ) ) );
1097 return false;
1098 }
1099
1100#ifdef _WIN32
1101 const bool windows = true;
1102#else
1103 const bool windows = false;
1104#endif
1105
1106 auto db = std::make_shared<SQLITE_DB>( m_path, FileUri( m_path, windows ), m_timeout );
1107
1108 std::optional<CALL_RESULT> result = runOnWorker<CALL_RESULT>(
1109 [db]( DB_JOB_CONTEXT& )
1110 {
1111 return db->Connect();
1112 },
1113 nullptr );
1114
1115 if( !result )
1116 return false;
1117
1118 if( !result->ok )
1119 {
1120 fail( result->failure, describeFailure( m_path, *result ) );
1121 return false;
1122 }
1123
1124 m_db = db;
1125 m_connected = true;
1126 return true;
1127}
1128
1129
1134
1135
1136bool SQLITE_BACKEND::ListTables( std::vector<std::string>& aTables )
1137{
1138 aTables.clear();
1139 m_lastError.clear();
1140
1141 if( !requireConnection() )
1142 return false;
1143
1144 std::shared_ptr<SQLITE_DB> db = m_db;
1145
1146 std::optional<CATALOG_RESULT> result = runOnWorker<CATALOG_RESULT>(
1147 [db]( DB_JOB_CONTEXT& )
1148 {
1149 return listTables( *db );
1150 },
1151 nullptr );
1152
1153 if( !result )
1154 return false;
1155
1156 if( !result->call.ok )
1157 {
1158 failCall( result->call.failure, describeFailure( m_path, result->call ) );
1159
1160 return false;
1161 }
1162
1163 aTables = std::move( result->names );
1164 std::sort( aTables.begin(), aTables.end() );
1165 return true;
1166}
1167
1168
1169std::optional<DB_TABLE_INFO> SQLITE_BACKEND::DescribeTable( const std::string& aTable )
1170{
1171 // A table that is simply absent leaves no error, so nothing of an earlier failure may remain
1172 m_lastError.clear();
1174
1175 if( !requireConnection() )
1176 return std::nullopt;
1177
1178 std::shared_ptr<SQLITE_DB> db = m_db;
1179
1180 std::optional<CATALOG_RESULT> result = runOnWorker<CATALOG_RESULT>(
1181 [db, aTable]( DB_JOB_CONTEXT& )
1182 {
1183 return describeTable( *db, aTable );
1184 },
1185 nullptr );
1186
1187 if( !result )
1188 return std::nullopt;
1189
1190 if( !result->call.ok )
1191 {
1192 failCall( result->call.failure, describeFailure( m_path, result->call ) );
1193
1194 return std::nullopt;
1195 }
1196
1197 if( !result->found )
1198 return std::nullopt;
1199
1201
1202 info.name = std::move( result->table );
1203 info.columns = std::move( result->columns );
1204
1205 return info;
1206}
1207
1208
1209bool SQLITE_BACKEND::ReadTable( const DB_TABLE_INFO& aTable, const std::vector<std::string>& aColumns,
1210 size_t aMaxRows, std::vector<std::string>& aLabels, const DB_ROW_SINK& aSink )
1211{
1212 aLabels.clear();
1213 m_lastError.clear();
1214
1215 if( !requireConnection() )
1216 return false;
1217
1218 std::string sql = "SELECT ";
1219
1220 if( aColumns.empty() )
1221 {
1222 sql += "*";
1223 }
1224 else
1225 {
1226 for( size_t i = 0; i < aColumns.size(); ++i )
1227 sql += ( i ? ", " : "" ) + quoted( aColumns[i] );
1228 }
1229
1230 sql += " FROM " + quoted( aTable.name );
1231
1232 if( aMaxRows != ALL_ROWS )
1233 sql += " ORDER BY 1 LIMIT " + std::to_string( aMaxRows );
1234
1235 DB_READ_DELIVERY delivery( aLabels, aSink );
1236 auto labels = delivery.Labels();
1237 std::shared_ptr<SQLITE_DB> db = m_db;
1238 const bool fetchRows = aMaxRows != 0;
1239
1240 std::optional<CALL_RESULT> result = runOnWorker<CALL_RESULT>(
1241 [db, sql, labels, fetchRows]( DB_JOB_CONTEXT& aContext ) -> CALL_RESULT
1242 {
1243 READ_SNAPSHOT snapshot( *db );
1244 CALL_RESULT began = snapshot.Begin();
1245
1246 if( !began.ok )
1247 return began;
1248
1249 {
1250 STATEMENT query( db->handle, sql );
1251
1252 if( !query.Ok() )
1253 return query.Error();
1254
1255 const int count = sqlite3_column_count( query.Get() );
1256
1257 for( int i = 0; i < count; ++i )
1258 labels->emplace_back( sqlite3_column_name( query.Get(), i ) );
1259
1260 int code = SQLITE_ROW;
1261
1262 while( fetchRows && ( code = query.Step() ) == SQLITE_ROW )
1263 {
1264 std::vector<DB_VALUE> row;
1265 row.reserve( static_cast<size_t>( count ) );
1266
1267 for( int i = 0; i < count; ++i )
1268 row.push_back( cellValue( query.Get(), i ) );
1269
1270 if( !aContext.Emit( std::move( row ) ) )
1271 return failed( DB_FAILURE::UNKNOWN, "the rows were not accepted" );
1272 }
1273
1274 if( fetchRows && code != SQLITE_DONE )
1275 return sqliteError( db->handle, code );
1276 }
1277
1278 if( !aContext.Flush() )
1279 return failed( DB_FAILURE::UNKNOWN, "the rows were not accepted" );
1280
1281 return snapshot.Commit();
1282 },
1283 delivery.Sink() );
1284
1285 if( !result )
1286 return false;
1287
1288 delivery.Finish();
1289
1290 if( !result->ok )
1291 {
1292 aLabels.clear();
1293
1294 if( delivery.SinkThrew() )
1296 else
1297 fail( result->failure, describeFailure( m_path, *result ) );
1298
1299 if( invalidates( result->failure ) )
1301
1302 return false;
1303 }
1304
1305 return true;
1306}
const char * name
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.
DB_FAILURE m_lastFailure
void fail(DB_FAILURE aFailure, std::string aError)
SQLITE_BACKEND(std::string aPath, int aTimeoutSeconds)
~SQLITE_BACKEND() override
std::string m_lastError
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::string m_path
bool Connect() override
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)
#define _(s)
const wxChar *const traceDatabase
Flag to enable database library tracing.
std::string message
STL namespace.
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.
std::string name
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.
CALL_RESULT Open()
SQLITE_DB & operator=(const SQLITE_DB &)=delete
std::string path
static void closeHandle(sqlite3 *aHandle)
SQLITE_DB(const SQLITE_DB &)=delete
std::string uri
SQLITE_DB(std::string aPath, std::string aUri, int aTimeout)
sqlite3 * handle
static std::vector< uint8_t > bytes(const std::string &aText)
std::string path
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.
typedef HANDLE