KiCad PCB EDA Suite
Loading...
Searching...
No Matches
odbc_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 (C) 2022 Jon Evans <[email protected]>
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or (at your
10 * option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include "odbc_backend.h"
22#include "access_engine_mutex.h"
23#include "db_worker_thread.h"
24
25#include <algorithm>
26#include <charconv>
27#include <mutex>
28#include <regex>
29
30#include <boost/algorithm/string.hpp>
31#include <boost/locale.hpp>
32#include <fmt/format.h>
33#include <nanodbc/nanodbc.h>
34
35#ifdef _WIN32
36#include <windows.h> // for sql.h
37#endif
38
39#include <sql.h> // SQL_IDENTIFIER_QUOTE_CHAR
40#include <sqlext.h> // SQL_LONGVARCHAR
41
42#include <wx/log.h>
43
46#include <core/profile.h>
47
55
61static nanodbc::string fromUTF8( const std::string& aString )
62{
63 return boost::locale::conv::utf_to_utf<nanodbc::string::value_type>( aString );
64}
65
66
72static std::string toUTF8( const nanodbc::string& aString )
73{
74 return boost::locale::conv::utf_to_utf<char>( aString );
75}
76
77
79{
80 std::string name;
81 int type;
82};
83
84
86static std::vector<RESULT_COLUMN> resultColumns( nanodbc::result& aResults )
87{
88 std::vector<RESULT_COLUMN> columns;
89
90 for( short i = 0; i < aResults.columns(); ++i )
91 columns.push_back( { toUTF8( aResults.column_name( i ) ), aResults.column_datatype( i ) } );
92
93 return columns;
94}
95
96
102static std::vector<DB_VALUE> decodeRow( nanodbc::result& aResults, const std::vector<RESULT_COLUMN>& aColumns )
103{
104 std::vector<DB_VALUE> row;
105 row.reserve( aColumns.size() );
106
107 for( short i = 0; i < static_cast<short>( aColumns.size() ); ++i )
108 {
109 switch( aColumns[i].type )
110 {
111 case SQL_DOUBLE:
112 case SQL_FLOAT:
113 case SQL_REAL:
114 try
115 {
116 row.emplace_back( aResults.get<double>( i ) );
117 }
118 catch( nanodbc::null_access_error& )
119 {
120 row.emplace_back( std::monostate() );
121 }
122
123 break;
124
125 case SQL_DECIMAL:
126 case SQL_NUMERIC:
127 // Text avoids the double conversion that would round past 15 digits
128 row.emplace_back(
129 NormalizeDecimalText( toUTF8( aResults.get<nanodbc::string>( i, NANODBC_TEXT( "" ) ) ) ) );
130 break;
131
132 default:
133 row.emplace_back( toUTF8( aResults.get<nanodbc::string>( i, NANODBC_TEXT( "" ) ) ) );
134 }
135 }
136
137 return row;
138}
139
140
141ODBC_BACKEND::ODBC_BACKEND( std::string aDsn, std::string aUser, std::string aPass, std::string aConnectionString,
142 long aTimeout ) :
143 m_dsn( std::move( aDsn ) ),
144 m_user( std::move( aUser ) ),
145 m_pass( std::move( aPass ) ),
146 m_connectionString( std::move( aConnectionString ) ),
147 m_timeout( aTimeout )
148{
149#ifdef _WIN32
151 []( const std::string& aName ) -> std::optional<std::string>
152 {
154 {
155 if( boost::iequals( source.name, aName ) )
156 return source.driver;
157 }
158
159 return std::nullopt;
160 } );
161#endif
162}
163
164
165namespace
166{
167// Handles that could not be freed under the driver lock are kept for good, since freeing them unlocked races
168// the driver unload and waiting for the lock can hang exit
169std::vector<std::unique_ptr<nanodbc::connection>>& leakedConnections()
170{
171 static auto* s_leaked = new std::vector<std::unique_ptr<nanodbc::connection>>;
172
173 return *s_leaked;
174}
175
176std::mutex& leakedConnectionsMutex()
177{
178 static auto* s_mutex = new std::mutex;
179
180 return *s_mutex;
181}
182} // namespace
183
184
186{
187 // An abandoned Access worker can hold the driver lock without bound, and an unopened backend frees nothing
188 if( !m_conn )
189 return;
190
191 std::unique_lock<std::timed_mutex> driverLock;
192
193 if( !takeAccessDriverLock( driverLock ) )
194 {
195 // A handle left inside the Access driver crashes its unload at exit
197
198 std::lock_guard<std::mutex> guard( leakedConnectionsMutex() );
199 leakedConnections().push_back( std::move( m_conn ) );
200 return;
201 }
202
204 m_conn.reset();
205}
206
207
208// The Access driver loads Office libraries when it initializes and unloads, and two libraries doing so at once race
209// Other drivers connect and disconnect in parallel
210bool ODBC_BACKEND::takeAccessDriverLock( std::unique_lock<std::timed_mutex>& aLock )
211{
212 if( !m_accessDriver )
213 return true;
214
215 wxLogTrace( traceDatabase, wxT( "Taking the Access driver lock" ) );
216
217 // Only a contended wait spends the deadline, so a free lock leaves the whole budget for the close
218 aLock = std::unique_lock<std::timed_mutex>( AccessEngineMutex(), std::try_to_lock );
219
220 if( aLock.owns_lock() )
221 return true;
222
223 if( !m_engineDeadline )
224 m_engineDeadline = std::chrono::steady_clock::now() + DB_WORKER_THREAD::CallerBound( m_timeout );
225
227
228 if( !aLock.owns_lock() )
229 {
230 wxLogTrace( traceDatabase, wxT( "The Access driver lock stayed busy" ) );
231 return false;
232 }
233
234 return true;
235}
236
237
238ODBC_BACKEND::SELECTOR ODBC_BACKEND::FirstSelector( const std::string& aConnectionString )
239{
240 size_t pos = 0;
241 const size_t size = aConnectionString.size();
242
243 while( pos < size )
244 {
245 size_t end = aConnectionString.find_first_of( "=;", pos );
246
247 if( end == std::string::npos )
248 break;
249
250 std::string key = boost::trim_copy( aConnectionString.substr( pos, end - pos ) );
251
252 if( aConnectionString[end] == ';' )
253 {
254 pos = end + 1;
255 continue;
256 }
257
258 pos = aConnectionString.find_first_not_of( " \t", end + 1 );
259
260 if( pos == std::string::npos )
261 break;
262
263 std::string value;
264
265 if( aConnectionString[pos] == '{' )
266 {
267 // A doubled closing brace is a literal brace inside the value
268 for( ++pos; pos < size; ++pos )
269 {
270 if( aConnectionString[pos] == '}' )
271 {
272 if( pos + 1 < size && aConnectionString[pos + 1] == '}' )
273 {
274 value += '}';
275 ++pos;
276 continue;
277 }
278
279 ++pos;
280 break;
281 }
282
283 value += aConnectionString[pos];
284 }
285
286 pos = aConnectionString.find( ';', pos );
287 }
288 else
289 {
290 size_t stop = aConnectionString.find( ';', pos );
291 value = boost::trim_copy( aConnectionString.substr( pos, stop - pos ) );
292 pos = stop;
293 }
294
295 if( boost::iequals( key, "DSN" ) )
296 return { SELECTOR::KIND::DSN, value };
297
298 if( boost::iequals( key, "FILEDSN" ) )
299 return { SELECTOR::KIND::FILE_DSN, value };
300
301 if( boost::iequals( key, "DRIVER" ) )
302 return { SELECTOR::KIND::DRIVER, value };
303
304 if( pos == std::string::npos )
305 break;
306
307 ++pos;
308 }
309
310 return {};
311}
312
313
314bool ODBC_BACKEND::NeedsDriverLock( const std::string& aDsn, const std::string& aConnectionString,
315 const DSN_LOOKUP& aLookup )
316{
317 // A driver that cannot be named is locked too, since it may be the Access driver
318 auto lockForDsn =
319 [&]( const std::string& aName )
320 {
321 std::optional<std::string> driver = aLookup( aName );
322
323 return !driver || driver->empty() || IsAccessDriver( *driver );
324 };
325
326 if( aConnectionString.empty() )
327 return lockForDsn( aDsn );
328
329 // ODBC honours the first DSN, FILEDSN or DRIVER keyword in the string
330 SELECTOR selector = FirstSelector( aConnectionString );
331
332 switch( selector.kind )
333 {
334 case SELECTOR::KIND::DRIVER: return selector.value.empty() || IsAccessDriver( selector.value );
335 case SELECTOR::KIND::DSN: return lockForDsn( selector.value );
337 case SELECTOR::KIND::NONE: break;
338 }
339
340 return true;
341}
342
343
344bool ODBC_BACKEND::IsAccessDriver( const std::string& aDriverName )
345{
346 return aDriverName.starts_with( "Microsoft Access Driver" );
347}
348
349
351{
352 nanodbc::string dsn = fromUTF8( m_dsn );
353 nanodbc::string user = fromUTF8( m_user );
354 nanodbc::string pass = fromUTF8( m_pass );
355 nanodbc::string cs = fromUTF8( m_connectionString );
356
358 m_engineDeadline.reset();
359
360 std::unique_lock<std::timed_mutex> driverLock;
361
362 if( !takeAccessDriverLock( driverLock ) )
363 {
366 return false;
367 }
368
369 try
370 {
371 if( cs.empty() )
372 {
373 wxLogTrace( traceDatabase, wxT( "Creating connection to DSN %s" ), m_dsn );
374 m_conn = std::make_unique<nanodbc::connection>( dsn, user, pass, m_timeout );
375 }
376 else
377 {
378 wxLogTrace( traceDatabase, wxT( "Creating connection with connection string" ) );
379 m_conn = std::make_unique<nanodbc::connection>( cs, m_timeout );
380 }
381 }
382 catch( nanodbc::database_error& e )
383 {
384 m_lastError = e.what();
386 return false;
387 }
388 catch( std::exception& e )
389 {
390 m_lastError = e.what();
391 m_lastFailure = ClassifyFailure( std::string(), m_lastError );
392 return false;
393 }
394
395 if( IsConnected() )
396 {
397 getQuoteChar();
398
399 try
400 {
401 m_patternEscape = toUTF8( m_conn->get_info<nanodbc::string>( SQL_SEARCH_PATTERN_ESCAPE ) );
402 }
403 catch( std::exception& e )
404 {
405 m_patternEscape.clear();
406 wxLogTrace( traceDatabase, wxT( "Pattern escape unavailable: %s" ), e.what() );
407 }
408 }
409
410 return IsConnected();
411}
412
413
415{
416 if( !m_conn )
417 {
418 wxLogTrace( traceDatabase, wxT( "Note: Disconnect() called without valid connection" ) );
419 return;
420 }
421
422 std::unique_lock<std::timed_mutex> driverLock;
423
424 if( !takeAccessDriverLock( driverLock ) )
425 {
428 return;
429 }
430
432}
433
434
436{
437 try
438 {
439 m_conn->disconnect();
440 }
441 catch( std::exception& exc )
442 {
443 wxLogTrace( traceDatabase, wxT( "Disconnect() error \"%s\" occured." ), exc.what() );
444 }
445}
446
447
449{
450 if( !m_conn )
451 return false;
452
453 return m_conn->connected();
454}
455
456
457std::optional<DB_TABLE_INFO> ODBC_BACKEND::DescribeTable( const std::string& aTable )
458{
459 if( !m_conn )
460 return std::nullopt;
461
462 // A table that is simply absent leaves no error, so nothing of an earlier failure may remain
463 m_lastError.clear();
465
466 try
467 {
468 nanodbc::catalog catalog( *m_conn );
469 nanodbc::catalog::tables tables = catalog.find_tables( fromUTF8( escapePattern( aTable ) ) );
470
471 if( !tables.next() )
472 {
473 wxLogTrace( traceDatabase, wxT( "DescribeTable: table '%s' not found in catalog" ), aTable );
474 return std::nullopt;
475 }
476
478 info.name = toUTF8( tables.table_name() );
479
480 try
481 {
482 nanodbc::catalog::columns columns =
483 catalog.find_columns( NANODBC_TEXT( "" ), fromUTF8( escapePattern( info.name ) ) );
484
485 std::map<std::string, COLUMN_INFO>& described = m_columnInfo[info.name];
486 described.clear();
487
488 while( columns.next() )
489 {
490 std::string name = toUTF8( columns.column_name() );
491
492 info.columns.push_back( { name, columns.data_type() } );
493 described[name] = { columns.data_type(), columns.column_size(), columns.decimal_digits() };
494 }
495 }
496 catch( nanodbc::database_error& e )
497 {
498 m_lastError = e.what();
499 wxLogTrace( traceDatabase, wxT( "Exception while syncing columns for table '%s': %s" ), info.name,
500 m_lastError );
501
502 info.columns.clear();
503 info.columnsUnavailable = true;
504 }
505
506 return info;
507 }
508 catch( std::exception& e )
509 {
510 m_lastError = e.what();
511 wxLogTrace( traceDatabase, wxT( "Exception while describing table: %s" ), m_lastError );
512 return std::nullopt;
513 }
514}
515
516
517bool ODBC_BACKEND::ListColumns( const std::string& aTable, std::vector<std::string>& aColumns )
518{
519 aColumns.clear();
520
521 if( !m_conn )
522 return false;
523
524 try
525 {
526 nanodbc::catalog catalog( *m_conn );
527 nanodbc::catalog::columns columns = catalog.find_columns( NANODBC_TEXT( "" ),
528 fromUTF8( escapePattern( aTable ) ) );
529
530 while( columns.next() )
531 aColumns.emplace_back( toUTF8( columns.column_name() ) );
532 }
533 catch( std::exception& e )
534 {
535 m_lastError = e.what();
536 wxLogTrace( traceDatabase, wxT( "ListColumns for %s failed: %s" ), aTable, m_lastError );
537 return false;
538 }
539
540 return true;
541}
542
543
545{
546 if( !m_conn )
547 return false;
548
549 try
550 {
551 nanodbc::string qc = m_conn->get_info<nanodbc::string>( SQL_IDENTIFIER_QUOTE_CHAR );
552
553 if( qc.empty() )
554 return false;
555
556 m_quoteChar = *toUTF8( qc ).begin();
557
558 wxLogTrace( traceDatabase, wxT( "Quote char retrieved: %c" ), m_quoteChar );
559 }
560 catch( std::exception& e )
561 {
562 m_lastError = e.what();
563 wxLogTrace( traceDatabase, wxT( "Exception while querying quote char: %s" ), m_lastError );
564 return false;
565 }
566
567 return true;
568}
569
570
571std::string ODBC_BACKEND::quoted( const std::string& aIdentifier ) const
572{
573 std::string quote( 1, m_quoteChar );
574 std::string escaped = aIdentifier;
575
576 boost::replace_all( escaped, quote, quote + quote );
577
578 return quote + escaped + quote;
579}
580
581
582std::string ODBC_BACKEND::escapePattern( const std::string& aName ) const
583{
584 // Without an escape character the driver offers no way to match a name literally
585 if( m_patternEscape.empty() )
586 return aName;
587
588 std::string escaped;
589
590 for( char c : aName )
591 {
592 if( c == '_' || c == '%' || c == m_patternEscape.front() )
593 escaped += m_patternEscape;
594
595 escaped += c;
596 }
597
598 return escaped;
599}
600
601
602std::string ODBC_BACKEND::columnList( const std::vector<std::string>& aColumns ) const
603{
604 if( aColumns.empty() )
605 return "*";
606
607 std::string ret;
608
609 for( const std::string& column : aColumns )
610 ret += quoted( column ) + ", ";
611
612 // strip tailing ', '
613 ret.resize( ret.length() - 2 );
614
615 return ret;
616}
617
618
619// Fallback sizes for when the catalog reports none, and the longest character size worth declaring
620static constexpr unsigned long DEFAULT_KEY_CHAR_SIZE = 255;
621static constexpr unsigned long MAX_KEY_CHAR_SIZE = 8000;
622static constexpr unsigned long DEFAULT_KEY_INTEGER_SIZE = 19; // nanodbc asserts on a zero size when binding numbers
623static constexpr unsigned long DEFAULT_KEY_DECIMAL_SIZE = 38; // widest precision ODBC drivers commonly allow
624
625
627 short aColumnScale, const std::string& aKey )
628{
629 const short type = static_cast<short>( aColumnType );
630
631 // Catalogs report a negative or NULL size for types where it does not apply
632 const unsigned long columnSize = aColumnSize > 0 ? static_cast<unsigned long>( aColumnSize ) : 0;
633
634 switch( type )
635 {
636 case SQL_TINYINT:
637 case SQL_SMALLINT:
638 case SQL_INTEGER:
639 case SQL_BIGINT:
640 {
641 int64_t value = 0;
642 const char* end = aKey.data() + aKey.size();
643 auto parsed = std::from_chars( aKey.data(), end, value );
644
645 // A key that is not a whole number cannot match an integer column, so the driver is left to reject it
646 if( parsed.ec == std::errc() && parsed.ptr == end )
647 return { type, columnSize ? columnSize : DEFAULT_KEY_INTEGER_SIZE, 0, true, value };
648
649 break;
650 }
651
652 case SQL_DECIMAL:
653 case SQL_NUMERIC: return { type, columnSize ? columnSize : DEFAULT_KEY_DECIMAL_SIZE, aColumnScale, false, 0 };
654
655 case SQL_CHAR:
656 case SQL_VARCHAR:
657 case SQL_LONGVARCHAR:
658 case SQL_WCHAR:
659 case SQL_WVARCHAR:
660 case SQL_WLONGVARCHAR:
661 {
662 const bool wide = type == SQL_WCHAR || type == SQL_WVARCHAR || type == SQL_WLONGVARCHAR;
663 const unsigned long size = columnSize ? std::min( columnSize, MAX_KEY_CHAR_SIZE ) : DEFAULT_KEY_CHAR_SIZE;
664
665 return { static_cast<short>( wide ? SQL_WVARCHAR : SQL_VARCHAR ),
666 std::max( size, static_cast<unsigned long>( aKey.size() ) ), 0, false, 0 };
667 }
668
669 default: break;
670 }
671
672 return { SQL_VARCHAR, DEFAULT_KEY_CHAR_SIZE, 0, false, 0 };
673}
674
675
676std::optional<bool> ODBC_BACKEND::ReadRow( const DB_TABLE_INFO& aTable, const std::vector<std::string>& aColumns,
677 const std::string& aKeyColumn, const std::string& aKey,
678 std::vector<std::string>& aLabels, std::vector<DB_VALUE>& aRow )
679{
680 if( !m_conn )
681 {
682 wxLogTrace( traceDatabase, wxT( "Called ReadRow without valid connection!" ) );
683 return false;
684 }
685
686 COLUMN_INFO columnInfo = { SQL_VARCHAR, 0, 0 };
687
688 if( auto table = m_columnInfo.find( aTable.name ); table != m_columnInfo.end() )
689 {
690 if( auto column = table->second.find( aKeyColumn ); column != table->second.end() )
691 columnInfo = column->second;
692 }
693
694 std::string queryStr = fmt::format( "SELECT {} FROM {} WHERE {} = ?", columnList( aColumns ),
695 quoted( aTable.name ), quoted( aKeyColumn ) );
696 nanodbc::string query = fromUTF8( queryStr );
697
698 PROF_TIMER timer;
699 nanodbc::statement statement;
700
701 try
702 {
703 statement.prepare( *m_conn, query );
704 }
705 catch( std::exception& e )
706 {
707 m_lastError = e.what();
708 wxLogTrace( traceDatabase, wxT( "Exception while preparing statement for SelectOne: %s" ),
709 m_lastError );
710
711 // Exception may be due to a connection error; nanodbc won't auto-reconnect
712 Disconnect();
713
714 return false;
715 }
716
717 // Pre-describe the parameter to avoid SQLDescribeParam, which some ODBC drivers (Microsoft Access,
718 // Excel, CSV) don't implement
719 const KEY_PARAMETER key = DescribeKeyParameter( columnInfo.type, columnInfo.size, columnInfo.scale, aKey );
720
721 try
722 {
723 statement.describe_parameters( { 0 }, { key.type }, { key.size }, { key.scale } );
724
725 if( key.asInteger )
726 statement.bind( 0, &key.integerValue );
727 else
728 statement.bind( 0, aKey.c_str() );
729 }
730 catch( std::exception& e )
731 {
732 m_lastError = e.what();
733 wxLogTrace( traceDatabase, wxT( "Exception while binding parameter for SelectOne: %s" ),
734 m_lastError );
735
736 Disconnect();
737
738 return false;
739 }
740
741 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s`" ), toUTF8( query ), aKey );
742
743 nanodbc::result results;
744
745 try
746 {
747 results = nanodbc::execute( statement );
748 }
749 catch( std::exception& e )
750 {
751 m_lastError = e.what();
752 wxLogTrace( traceDatabase, wxT( "Exception while executing statement for SelectOne: %s" ),
753 m_lastError );
754
755 // Exception may be due to a connection error; nanodbc won't auto-reconnect
756 Disconnect();
757
758 return false;
759 }
760
761 timer.Stop();
762
763 if( !results.first() )
764 {
765 wxLogTrace( traceDatabase, wxT( "SelectOne: no results returned from query" ) );
766 return false;
767 }
768
769 wxLogTrace( traceDatabase, wxT( "SelectOne: %ld results returned from query in %0.1f ms" ),
770 results.rows(), timer.msecs() );
771
772 try
773 {
774 const std::vector<RESULT_COLUMN> columns = resultColumns( results );
775
776 aLabels.clear();
777
778 for( const RESULT_COLUMN& column : columns )
779 aLabels.push_back( column.name );
780
781 aRow = decodeRow( results, columns );
782 }
783 catch( std::exception& e )
784 {
785 m_lastError = e.what();
786 wxLogTrace( traceDatabase, wxT( "Exception while parsing results from SelectOne: %s" ),
787 m_lastError );
788 return false;
789 }
790
791 return true;
792}
793
794
795bool ODBC_BACKEND::ReadTable( const DB_TABLE_INFO& aTable, const std::vector<std::string>& aColumns,
796 size_t aMaxRows, std::vector<std::string>& aLabels, const DB_ROW_SINK& aSink )
797{
798 aLabels.clear();
799
800 if( !m_conn )
801 return false;
802
803 const bool all = aMaxRows == ALL_ROWS;
804
805 try
806 {
807 const std::string select = fmt::format( "SELECT {} FROM {}", columnList( aColumns ), quoted( aTable.name ) );
808 PROF_TIMER timer;
809 nanodbc::result results;
810
811 if( all )
812 {
813 nanodbc::statement statement( *m_conn );
814
815 statement.prepare( fromUTF8( select ) );
816 results = nanodbc::execute( statement );
817 }
818 else
819 {
820 // Direct execution keeps the default forward-only cursor; prepare() would force a keyset
821 // cursor that some drivers build over the whole table before returning a row
822 try
823 {
824 // Ordered so every server returns the same rows; the first column is usually the indexed key
825 results = nanodbc::execute( *m_conn, fromUTF8( select + " ORDER BY 1" ), 1, m_timeout );
826 }
827 catch( const nanodbc::database_error& e )
828 {
829 // Some column types cannot be ordered (e.g. Postgres json), so fall back to server order
830 wxLogTrace( traceDatabase, wxT( "ReadTable: ordered query on %s failed (%s); retrying unordered" ),
831 aTable.name, e.what() );
832 results = nanodbc::execute( *m_conn, fromUTF8( select ), 1, m_timeout );
833 }
834 }
835
836 timer.Stop();
837
838 const std::vector<RESULT_COLUMN> columns = resultColumns( results );
839
840 for( const RESULT_COLUMN& column : columns )
841 aLabels.push_back( column.name );
842
843 size_t rows = 0;
844
845 while( rows < aMaxRows && results.next() )
846 {
847 try
848 {
849 aSink( decodeRow( results, columns ) );
850 }
851 catch( std::runtime_error& e )
852 {
853 m_lastError = e.what();
854 wxLogTrace( traceDatabase, wxT( "Exception while parsing result %zu from ReadTable: %s" ), rows,
855 m_lastError );
856 return false;
857 }
858
859 ++rows;
860 }
861
862 wxLogTrace( traceDatabase, wxT( "ReadTable from %s completed in %0.1f ms" ), aTable.name, timer.msecs() );
863 }
864 catch( std::exception& e )
865 {
866 m_lastError = e.what();
867 wxLogTrace( traceDatabase, wxT( "ReadTable from %s failed: %s" ), aTable.name, m_lastError );
868
869 // Exception may be due to a connection error; nanodbc won't auto-reconnect
870 if( all )
871 Disconnect();
872
873 return false;
874 }
875
876 return true;
877}
878
879
880bool ODBC_BACKEND::ListTables( std::vector<std::string>& aTables )
881{
882 aTables.clear();
883
884 if( !m_conn )
885 return false;
886
887 try
888 {
889 nanodbc::catalog catalog( *m_conn );
890 nanodbc::string current;
891
892 try
893 {
894 current = m_conn->catalog_name();
895 }
896 catch( const std::exception& )
897 {
898 // Without a current catalog every listed table is offered
899 }
900
901 auto listTables =
902 [&]( const nanodbc::string& aCatalog )
903 {
904 nanodbc::catalog::tables tables = catalog.find_tables( NANODBC_TEXT( "" ),
905 NANODBC_TEXT( "TABLE,VIEW" ),
906 NANODBC_TEXT( "" ), aCatalog );
907
908 while( tables.next() )
909 aTables.emplace_back( toUTF8( tables.table_name() ) );
910 };
911
912 // MariaDB Connector/ODBC 3.1 lists every visible database but queries name bare tables in the
913 // current one; the driver applies its case rules to this restriction, a pattern under ODBC 3
914 listTables( fromUTF8( escapePattern( toUTF8( current ) ) ) );
915
916 // A catalog name the driver reported truncated or spelled differently matches nothing
917 if( aTables.empty() && !current.empty() )
918 listTables( NANODBC_TEXT( "" ) );
919 }
920 catch( std::exception& e )
921 {
922 m_lastError = e.what();
923 wxLogTrace( traceDatabase, wxT( "ListTables failed: %s" ), m_lastError );
924 return false;
925 }
926
927 std::sort( aTables.begin(), aTables.end() );
928 return true;
929}
930
931
932std::vector<ODBC_BACKEND::DATA_SOURCE> ODBC_BACKEND::ListDataSourceDetails()
933{
934 std::vector<DATA_SOURCE> sources;
935
936 try
937 {
938 for( const nanodbc::datasource& source : nanodbc::list_datasources() )
939 sources.push_back( { toUTF8( source.name ), toUTF8( source.driver ) } );
940 }
941 catch( std::exception& e )
942 {
943 wxLogTrace( traceDatabase, wxT( "ListDataSourceDetails failed: %s" ), e.what() );
944 }
945
946 return sources;
947}
948
949
950std::vector<std::string> ODBC_BACKEND::ListDrivers()
951{
952 std::vector<std::string> names;
953
954 try
955 {
956 for( const nanodbc::driver& driver : nanodbc::list_drivers() )
957 names.emplace_back( toUTF8( driver.name ) );
958 }
959 catch( std::exception& e )
960 {
961 wxLogTrace( traceDatabase, wxT( "ListDrivers failed: %s" ), e.what() );
962 }
963
964 return names;
965}
966
967
968bool ODBC_BACKEND::IsAccessFileInUse( const std::string& aLowerMessage )
969{
970 // Only the driver's own whole message counts, so a name echoed by another driver cannot imitate it. The
971 // templates are the English driver's, so a localized Office keeps the failure unclassified
972 static const std::string ACCESS_TAG = "[odbc microsoft access driver]";
973
974 // A path or user name may hold an apostrophe, so the substitutions take any text up to the next literal
975 static const std::regex LOCKED(
976 R"(\s*(could not use '[^\r\n]*'; file already in use)"
977 R"(|could not lock file)"
978 R"(|database '[^\r\n]*' is exclusively locked)"
979 R"(|you attempted to open a database that is already opened (exclusively )?by user '[^\r\n]*')"
980 R"( on machine '[^\r\n]*')"
981 R"(|the database '[^\r\n]*' is already opened by user '[^\r\n]*' on machine '[^\r\n]*')"
982 R"(|the database has been placed in a state by user '[^\r\n]*' on machine '[^\r\n]*')"
983 R"( that prevents it from being opened or locked)\.?\s*)"
984 R"((try again when the database is available\.?\s*)?)" );
985
986 const size_t tag = aLowerMessage.rfind( ACCESS_TAG );
987
988 return tag != std::string::npos
989 && std::regex_match( aLowerMessage.begin() + tag + ACCESS_TAG.size(), aLowerMessage.end(), LOCKED );
990}
991
992
993DB_FAILURE ODBC_BACKEND::ClassifyFailure( const std::string& aState, const std::string& aMessage )
994{
995 std::string message = boost::to_lower_copy( aMessage );
996
997 auto mentions =
998 [&]( std::initializer_list<const char*> aPhrases )
999 {
1000 for( const char* phrase : aPhrases )
1001 {
1002 if( message.find( phrase ) != std::string::npos )
1003 return true;
1004 }
1005
1006 return false;
1007 };
1008
1009 // Postgres reports rejected logins as 08001, so the text decides; MySQL's database-level
1010 // "access denied" lacks the password clause and is not a login failure
1011 if( aState.starts_with( "28" )
1012 || mentions( { "password authentication failed", "(using password:", "login failed", "authentication failed",
1013 "no password supplied" } ) )
1014 {
1015 return DB_FAILURE::LOGIN;
1016 }
1017
1018 if( aState == "IM002" || mentions( { "data source name not found" } ) )
1019 return DB_FAILURE::NO_DSN;
1020
1021 if( aState == "IM003" || mentions( { "can't open lib", "could not be loaded" } ) )
1022 return DB_FAILURE::NO_DRIVER;
1023
1024 if( aState.starts_with( "HYT" ) || mentions( { "timeout expired", "timed out" } ) )
1025 return DB_FAILURE::TIMEOUT;
1026
1027 if( IsAccessFileInUse( message ) )
1029
1030 // Class 08 alone also covers failures after the server answered, such as certificate checks
1031 if( mentions( { "connection refused", "could not connect", "can't connect", "unknown host",
1032 "could not translate host name", "no route to host", "network is unreachable" } ) )
1033 {
1035 }
1036
1037 return DB_FAILURE::UNKNOWN;
1038}
const char * name
std::timed_mutex & AccessEngineMutex()
The one lock every user of the Microsoft Access engine takes while the engine initializes or unloads.
std::string AccessEngineBusyMessage()
std::unique_lock< std::timed_mutex > LockAccessEngine(std::chrono::steady_clock::time_point aDeadline)
void MarkAccessWorkerAbandoned()
Records that a worker inside the Access engine was abandoned.
static constexpr size_t ALL_ROWS
Passed as ReadTable()'s row limit to read every row.
static std::chrono::milliseconds CallerBound(int aTimeoutSeconds)
std::function< std::optional< std::string >(const std::string &)> DSN_LOOKUP
Resolves a data source name to its driver, or nullopt if it is unknown.
std::optional< DB_TABLE_INFO > DescribeTable(const std::string &aTable) override
std::map< std::string, std::map< std::string, COLUMN_INFO > > m_columnInfo
Map of table -> map of column name -> column description.
std::optional< std::chrono::steady_clock::time_point > m_engineDeadline
When the wait for the Access driver lock ends, fixed by the first step that finds the lock busy and n...
static std::vector< std::string > ListDrivers()
std::string m_dsn
std::string m_pass
void Disconnect() 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.
std::string m_user
std::string m_patternEscape
bool takeAccessDriverLock(std::unique_lock< std::timed_mutex > &aLock)
Takes the lock that keeps the Access driver from initializing or unloading on two threads at once.
std::string quoted(const std::string &aIdentifier) const
Wraps an identifier in the driver's quote character, doubling any it contains.
static bool IsAccessDriver(const std::string &aDriverName)
static DB_FAILURE ClassifyFailure(const std::string &aState, const std::string &aMessage)
Classifies a driver error from its SQLSTATE, falling back to its text since drivers disagree on state...
static KEY_PARAMETER DescribeKeyParameter(int aColumnType, long aColumnSize, short aColumnScale, const std::string &aKey)
Chooses the parameter description for a lookup key from its column's cached ODBC description.
void disconnectLocked()
Closes the connection with the driver lock held by the caller.
bool ListTables(std::vector< std::string > &aTables) override
Lists the tables and views, sorted by name.
bool Connect() override
ODBC_BACKEND(std::string aDsn, std::string aUser, std::string aPass, std::string aConnectionString, long aTimeout)
std::string m_lastError
static bool IsAccessFileInUse(const std::string &aLowerMessage)
std::string m_connectionString
bool IsConnected() const override
static bool NeedsDriverLock(const std::string &aDsn, const std::string &aConnectionString, const DSN_LOOKUP &aLookup)
Decides whether connecting must be serialized because the effective driver is, or may be,...
static SELECTOR FirstSelector(const std::string &aConnectionString)
std::optional< bool > ReadRow(const DB_TABLE_INFO &aTable, const std::vector< std::string > &aColumns, const std::string &aKeyColumn, const std::string &aKey, std::vector< std::string > &aLabels, std::vector< DB_VALUE > &aRow) override
Runs a prepared query whose key parameter is described with the key column's catalog type.
std::string escapePattern(const std::string &aName) const
Escapes catalog search pattern characters so a name matches only itself.
std::unique_ptr< nanodbc::connection > m_conn
DB_FAILURE m_lastFailure
~ODBC_BACKEND() override
bool ListColumns(const std::string &aTable, std::vector< std::string > &aColumns) override
Lists the column names of a table as the catalog reports them, empty if it has none,...
static std::vector< DATA_SOURCE > ListDataSourceDetails()
std::string columnList(const std::vector< std::string > &aColumns) const
A small class to help profiling.
Definition profile.h:46
void Stop()
Save the time when this function was called, and set the counter stane to stop.
Definition profile.h:86
double msecs(bool aSinceLast=false)
Definition profile.h:148
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.
@ TIMEOUT
The source did not answer within the timeout.
@ NO_DSN
The data source name is not configured.
@ LOGIN
The server rejected the user name or password.
@ UNREACHABLE
Nothing answered at the server address, or it refused the connection.
@ FILE_IN_USE
The database file is locked by another process.
@ NO_DRIVER
The driver named by the data source could not be loaded.
std::string NormalizeDecimalText(const std::string &aText)
Trim redundant trailing zeros and a dangling decimal point from decimal text, for example "4700000....
const wxChar *const traceDatabase
Flag to enable database library tracing.
std::string source
std::string message
STL namespace.
static nanodbc::string fromUTF8(const std::string &aString)
When Unicode support is enabled in nanodbc, string formats are used matching the appropriate characte...
static constexpr unsigned long DEFAULT_KEY_INTEGER_SIZE
static constexpr unsigned long DEFAULT_KEY_DECIMAL_SIZE
static std::vector< RESULT_COLUMN > resultColumns(nanodbc::result &aResults)
Column names and types are the same for every row, so read them once per result set.
static std::vector< DB_VALUE > decodeRow(nanodbc::result &aResults, const std::vector< RESULT_COLUMN > &aColumns)
Decodes the current row of a result set into values in column order.
static std::string toUTF8(const nanodbc::string &aString)
Converts a string from nanodbc-native to KiCad-native.
static constexpr unsigned long DEFAULT_KEY_CHAR_SIZE
static constexpr unsigned long MAX_KEY_CHAR_SIZE
std::string name
The table's name as the backend spells it.
ODBC description of a column, so a lookup key can be bound with the type the driver expects.
short scale
Digits right of the decimal point.
int type
SQL data type.
long size
Character length or numeric precision, zero or negative when unknown.
A data source configured in the ODBC driver manager.
How ReadRow describes its key parameter to the driver.
short type
ODBC SQL data type.
int64_t integerValue
The key as a number, set when asInteger is true.
bool asInteger
Bind integerValue as a 64-bit integer instead of the key text.
short scale
Digits right of the decimal point.
unsigned long size
Column size or precision.
The keyword ODBC uses to choose where a connection string connects.
std::string name
const std::string table
struct DROP_TABLE cs
std::vector< DATABASE_CONNECTION::ROW > rows
VECTOR2I end