KiCad PCB EDA Suite
Loading...
Searching...
No Matches
database_connection.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 <algorithm>
22
23#include <boost/algorithm/string.hpp>
24#include <boost/locale.hpp>
25#include <fmt/format.h>
26#include <nanodbc/nanodbc.h>
27
28// Some outdated definitions are used in sql.h
29// We need to define them for "recent" dev tools
30#define INT64 int64_t
31#define UINT64 uint64_t
32
33#ifdef __MINGW32__
34#define BYTE uint8_t
35#define WORD uint16_t
36#define DWORD uint32_t
37#define HWND uint32_t /* dummy define */
38#endif
39
40#ifdef WIN32
41#include <windows.h> // for sql.h
42#endif
43
44#include <sql.h> // SQL_IDENTIFIER_QUOTE_CHAR
45
46#include <wx/log.h>
47
50#include <core/profile.h>
51
52
53const char* const traceDatabase = "KICAD_DATABASE";
54
62
68nanodbc::string fromUTF8( const std::string& aString )
69{
70 return boost::locale::conv::utf_to_utf<nanodbc::string::value_type>( aString );
71}
72
73
79std::string toUTF8( const nanodbc::string& aString )
80{
81 return boost::locale::conv::utf_to_utf<char>( aString );
82}
83
84
86{
87 std::string name;
88 int type;
89};
90
91
93static std::vector<RESULT_COLUMN> resultColumns( nanodbc::result& aResults )
94{
95 std::vector<RESULT_COLUMN> columns;
96
97 for( short i = 0; i < aResults.columns(); ++i )
98 columns.push_back( { toUTF8( aResults.column_name( i ) ), aResults.column_datatype( i ) } );
99
100 return columns;
101}
102
103
108static void decodeRow( nanodbc::result& aResults, const std::vector<RESULT_COLUMN>& aColumns,
110{
111 for( short i = 0; i < static_cast<short>( aColumns.size() ); ++i )
112 {
113 const RESULT_COLUMN& column = aColumns[i];
114
115 switch( column.type )
116 {
117 case SQL_DOUBLE:
118 case SQL_FLOAT:
119 case SQL_REAL:
120 case SQL_DECIMAL:
121 case SQL_NUMERIC:
122 try
123 {
124 aRow[column.name] = fmt::format( "{:G}", aResults.get<double>( i ) );
125 }
126 catch( nanodbc::null_access_error& )
127 {
128 aRow[column.name] = std::string();
129 }
130
131 break;
132
133 default:
134 aRow[column.name] = toUTF8( aResults.get<nanodbc::string>( i, NANODBC_TEXT( "" ) ) );
135 }
136 }
137}
138
139
140DATABASE_CONNECTION::DATABASE_CONNECTION( const std::string& aDataSourceName,
141 const std::string& aUsername,
142 const std::string& aPassword, int aTimeoutSeconds,
143 bool aConnectNow ) :
144 m_quoteChar( '"' )
145{
146 m_dsn = aDataSourceName;
147 m_user = aUsername;
148 m_pass = aPassword;
149 m_timeout = aTimeoutSeconds;
150
151 init();
152
153 if( aConnectNow )
154 Connect();
155}
156
157
158DATABASE_CONNECTION::DATABASE_CONNECTION( const std::string& aConnectionString,
159 int aTimeoutSeconds, bool aConnectNow ) :
160 m_quoteChar( '"' )
161{
162 m_connectionString = aConnectionString;
163 m_timeout = aTimeoutSeconds;
164
165 init();
166
167 if( aConnectNow )
168 Connect();
169}
170
171
177
178
180{
181 m_cache = std::make_unique<DB_CACHE_TYPE>( 10, 1 );
182}
183
184
185void DATABASE_CONNECTION::SetCacheParams( int aMaxSize, int aMaxAge )
186{
187 if( !m_cache )
188 return;
189
190 if( aMaxSize < 0 )
191 aMaxSize = 0;
192
193 if( aMaxAge < 0 )
194 aMaxAge = 0;
195
196 m_cache->SetMaxSize( static_cast<size_t>( aMaxSize ) );
197 m_cache->SetMaxAge( static_cast<time_t>( aMaxAge ) );
198}
199
200
202{
203 nanodbc::string dsn = fromUTF8( m_dsn );
204 nanodbc::string user = fromUTF8( m_user );
205 nanodbc::string pass = fromUTF8( m_pass );
206 nanodbc::string cs = fromUTF8( m_connectionString );
207
208 try
209 {
210 if( cs.empty() )
211 {
212 wxLogTrace( traceDatabase, wxT( "Creating connection to DSN %s" ), m_dsn );
213 m_conn = std::make_unique<nanodbc::connection>( dsn, user, pass, m_timeout );
214 }
215 else
216 {
217 wxLogTrace( traceDatabase, wxT( "Creating connection with connection string" ) );
218 m_conn = std::make_unique<nanodbc::connection>( cs, m_timeout );
219 }
220 }
221 catch( nanodbc::database_error& e )
222 {
223 m_lastError = e.what();
224 m_lastState = e.state();
225 return false;
226 }
227 catch( std::exception& e )
228 {
229 m_lastError = e.what();
230 m_lastState.clear();
231 return false;
232 }
233
234 m_tables.clear();
235
236 if( IsConnected() )
237 {
238 getQuoteChar();
239
240 try
241 {
242 m_patternEscape = toUTF8( m_conn->get_info<nanodbc::string>( SQL_SEARCH_PATTERN_ESCAPE ) );
243 }
244 catch( std::exception& e )
245 {
246 m_patternEscape.clear();
247 wxLogTrace( traceDatabase, wxT( "Pattern escape unavailable: %s" ), e.what() );
248 }
249 }
250
251 return IsConnected();
252}
253
254
256{
257 if( !m_conn )
258 {
259 wxLogTrace( traceDatabase, wxT( "Note: Disconnect() called without valid connection" ) );
260 return false;
261 }
262
263 try
264 {
265 m_conn->disconnect();
266 }
267 catch( std::exception& exc )
268 {
269 wxLogTrace( traceDatabase, wxT( "Disconnect() error \"%s\" occured." ), exc.what() );
270 return false;
271 }
272
273 return !m_conn->connected();
274}
275
276
278{
279 if( !m_conn )
280 return false;
281
282 return m_conn->connected();
283}
284
285
286bool DATABASE_CONNECTION::CacheTableInfo( const std::string& aTable,
287 const std::set<std::string>& aRequiredColumns,
288 const std::set<std::string>& aOptionalColumns )
289{
290 std::lock_guard lock( m_queryMutex );
291
292 if( !m_conn )
293 return false;
294
295 // Catalog names are lowercase; normalize here so differently-cased columns still match
296 std::set<std::string> requiredLower;
297 std::set<std::string> optionalLower;
298
299 for( const std::string& col : aRequiredColumns )
300 requiredLower.insert( boost::to_lower_copy( col ) );
301
302 for( const std::string& col : aOptionalColumns )
303 optionalLower.insert( boost::to_lower_copy( col ) );
304
305 try
306 {
307 nanodbc::catalog catalog( *m_conn );
308 nanodbc::catalog::tables tables = catalog.find_tables( fromUTF8( escapePattern( aTable ) ) );
309
310 if( !tables.next() )
311 {
312 wxLogTrace( traceDatabase, wxT( "CacheTableInfo: table '%s' not found in catalog" ),
313 aTable );
314 return false;
315 }
316
317 std::string key = toUTF8( tables.table_name() );
318 m_tables[key] = toUTF8( tables.table_type() );
319
320 try
321 {
322 nanodbc::catalog::columns columns =
323 catalog.find_columns( NANODBC_TEXT( "" ), fromUTF8( escapePattern( key ) ) );
324
325 std::set<std::string> columnsInCatalog;
326
327 while( columns.next() )
328 {
329 std::string columnKey = toUTF8( columns.column_name() );
330 std::string columnKeyLower = boost::to_lower_copy( columnKey );
331
332 if( requiredLower.count( columnKeyLower )
333 || optionalLower.count( columnKeyLower ) )
334 {
335 m_columnCache[key][columnKey] = columns.data_type();
336 columnsInCatalog.insert( columnKeyLower );
337 }
338 }
339
340 // SQLite's ODBC driver doesn't report generated columns via SQLColumns, so required
341 // columns are trusted and added even when absent from the catalog
342 for( const std::string& requestedCol : aRequiredColumns )
343 {
344 std::string requestedColLower = boost::to_lower_copy( requestedCol );
345
346 if( !columnsInCatalog.count( requestedColLower ) && !requestedCol.empty() )
347 {
348 wxLogTrace( traceDatabase,
349 wxT( "CacheTableInfo: column '%s' not found in catalog for table "
350 "'%s', adding anyway" ),
351 requestedCol, key );
352
353 m_columnCache[key][requestedCol] = SQL_VARCHAR;
354 }
355 }
356
357 // Unlike required columns, a missing optional column is dropped, not added, but is
358 // still worth a warning since it usually means a misconfigured .kicad_dbl mapping
359 for( const std::string& requestedCol : aOptionalColumns )
360 {
361 std::string requestedColLower = boost::to_lower_copy( requestedCol );
362
363 if( !columnsInCatalog.count( requestedColLower ) && !requestedCol.empty() )
364 {
365 wxLogWarning( wxT( "Database table '%s' has no column '%s'; ignoring the "
366 "misconfigured properties mapping." ),
367 key, requestedCol );
368 }
369 }
370 }
371 catch( nanodbc::database_error& e )
372 {
373 m_lastError = e.what();
374 wxLogTrace( traceDatabase, wxT( "Exception while syncing columns for table '%s': %s" ),
375 key, m_lastError );
376 return false;
377 }
378 }
379 catch( std::exception& e )
380 {
381 m_lastError = e.what();
382 wxLogTrace( traceDatabase, wxT( "Exception while caching table info: %s" ), m_lastError );
383 return false;
384 }
385
386 return true;
387}
388
389
391{
392 if( !m_conn )
393 return false;
394
395 try
396 {
397 nanodbc::string qc = m_conn->get_info<nanodbc::string>( SQL_IDENTIFIER_QUOTE_CHAR );
398
399 if( qc.empty() )
400 return false;
401
402 m_quoteChar = *toUTF8( qc ).begin();
403
404 wxLogTrace( traceDatabase, wxT( "Quote char retrieved: %c" ), m_quoteChar );
405 }
406 catch( std::exception& e )
407 {
408 m_lastError = e.what();
409 wxLogTrace( traceDatabase, wxT( "Exception while querying quote char: %s" ), m_lastError );
410 return false;
411 }
412
413 return true;
414}
415
416
417std::string DATABASE_CONNECTION::quoted( const std::string& aIdentifier ) const
418{
419 std::string quote( 1, m_quoteChar );
420 std::string escaped = aIdentifier;
421
422 boost::replace_all( escaped, quote, quote + quote );
423
424 return quote + escaped + quote;
425}
426
427
428std::string DATABASE_CONNECTION::escapePattern( const std::string& aName ) const
429{
430 // Without an escape character the driver offers no way to match a name literally
431 if( m_patternEscape.empty() )
432 return aName;
433
434 std::string escaped;
435
436 for( char c : aName )
437 {
438 if( c == '_' || c == '%' || c == m_patternEscape.front() )
439 escaped += m_patternEscape;
440
441 escaped += c;
442 }
443
444 return escaped;
445}
446
447
448std::string DATABASE_CONNECTION::columnsFor( const std::string& aTable )
449{
450 if( !m_columnCache.count( aTable ) )
451 {
452 wxLogTrace( traceDatabase, wxT( "columnsFor: requested table %s missing from cache!" ),
453 aTable );
454 return "*";
455 }
456
457 if( m_columnCache[aTable].empty() )
458 {
459 wxLogTrace( traceDatabase, wxT( "columnsFor: requested table %s has no columns mapped!" ),
460 aTable );
461 return "*";
462 }
463
464 std::string ret;
465
466 for( const auto& [ columnName, columnType ] : m_columnCache[aTable] )
467 ret += quoted( columnName ) + ", ";
468
469 // strip tailing ', '
470 ret.resize( ret.length() - 2 );
471
472 return ret;
473}
474
475//next step, make SelectOne take from the SelectAll cache if the SelectOne cache is missing.
476//To do this, need to build a map of PK->ROW for the cache result.
477bool DATABASE_CONNECTION::SelectOne( const std::string& aTable,
478 const std::pair<std::string, std::string>& aWhere,
479 DATABASE_CONNECTION::ROW& aResult )
480{
481 std::lock_guard lock( m_queryMutex );
482
483 if( !m_conn )
484 {
485 wxLogTrace( traceDatabase, wxT( "Called SelectOne without valid connection!" ) );
486 return false;
487 }
488
489 auto tableMapIter = m_tables.find( aTable );
490
491 if( tableMapIter == m_tables.end() )
492 {
493 wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s not found in cache" ),
494 aTable );
495 return false;
496 }
497
498 const std::string& tableName = tableMapIter->first;
500
501 if( m_cache->Get( tableName, cacheEntry ) )
502 {
503 if( cacheEntry.count( aWhere.second ) )
504 {
505 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s` - cache hit" ),
506 tableName, aWhere.second );
507 aResult = cacheEntry.at( aWhere.second );
508 return true;
509 }
510 }
511 else
512 {
513 wxLogTrace( traceDatabase, wxT( "SelectOne: table `%s` not in row cache; will SelectAll" ),
514 tableName, aWhere.second );
515
516 selectAllAndCache( tableName, aWhere.first );
517
518 if( m_cache->Get( tableName, cacheEntry ) )
519 {
520 if( cacheEntry.count( aWhere.second ) )
521 {
522 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s` - cache hit" ),
523 tableName, aWhere.second );
524 aResult = cacheEntry.at( aWhere.second );
525 return true;
526 }
527 }
528 }
529
530 if( !m_columnCache.count( tableName ) )
531 {
532 wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s missing from column cache" ),
533 tableName );
534 return false;
535 }
536
537 auto columnCacheIter = m_columnCache.at( tableName ).find( aWhere.first );
538
539 if( columnCacheIter == m_columnCache.at( tableName ).end() )
540 {
541 wxLogTrace( traceDatabase, wxT( "SelectOne: requested column %s not found in cache for %s" ),
542 aWhere.first, tableName );
543 return false;
544 }
545
546 const std::string& columnName = columnCacheIter->first;
547
548 std::string cacheKey = fmt::format( "{}{}{}", tableName, columnName, aWhere.second );
549
550 std::string queryStr = fmt::format( "SELECT {} FROM {} WHERE {} = ?", columnsFor( tableName ),
551 quoted( tableName ), quoted( columnName ) );
552 nanodbc::string query = fromUTF8( queryStr );
553
554 PROF_TIMER timer;
555 nanodbc::statement statement;
556
557 try
558 {
559 statement.prepare( *m_conn, query );
560 }
561 catch( std::exception& e )
562 {
563 m_lastError = e.what();
564 wxLogTrace( traceDatabase, wxT( "Exception while preparing statement for SelectOne: %s" ),
565 m_lastError );
566
567 // Exception may be due to a connection error; nanodbc won't auto-reconnect
568 Disconnect();
569
570 return false;
571 }
572
573 // Pre-describe parameter as VARCHAR to avoid SQLDescribeParam call. Some ODBC drivers
574 // (Microsoft Access, Excel, CSV) don't implement SQLDescribeParam.
575 try
576 {
577 statement.describe_parameters( { 0 }, { SQL_VARCHAR }, { 255 }, { 0 } );
578 statement.bind( 0, aWhere.second.c_str() );
579 }
580 catch( std::exception& e )
581 {
582 m_lastError = e.what();
583 wxLogTrace( traceDatabase, wxT( "Exception while binding parameter for SelectOne: %s" ),
584 m_lastError );
585
586 Disconnect();
587
588 return false;
589 }
590
591 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s`" ), toUTF8( query ),
592 aWhere.second );
593
594 nanodbc::result results;
595
596 try
597 {
598 results = nanodbc::execute( statement );
599 }
600 catch( std::exception& e )
601 {
602 m_lastError = e.what();
603 wxLogTrace( traceDatabase, wxT( "Exception while executing statement for SelectOne: %s" ),
604 m_lastError );
605
606 // Exception may be due to a connection error; nanodbc won't auto-reconnect
607 Disconnect();
608
609 return false;
610 }
611
612 timer.Stop();
613
614
615 if( !results.first() )
616 {
617 wxLogTrace( traceDatabase, wxT( "SelectOne: no results returned from query" ) );
618 return false;
619 }
620
621 wxLogTrace( traceDatabase, wxT( "SelectOne: %ld results returned from query in %0.1f ms" ),
622 results.rows(), timer.msecs() );
623
624 aResult.clear();
625
626 try
627 {
628 decodeRow( results, resultColumns( results ), aResult );
629 }
630 catch( std::exception& e )
631 {
632 m_lastError = e.what();
633 wxLogTrace( traceDatabase, wxT( "Exception while parsing results from SelectOne: %s" ),
634 m_lastError );
635 return false;
636 }
637
638 return true;
639}
640
641
642bool DATABASE_CONNECTION::selectAllAndCache( const std::string& aTable, const std::string& aKey )
643{
644 try
645 {
646 nanodbc::statement statement( *m_conn );
647
648 nanodbc::string query = fromUTF8( fmt::format( "SELECT {} FROM {}", columnsFor( aTable ),
649 quoted( aTable ) ) );
650
651 PROF_TIMER timer;
652
653 try
654 {
655 statement.prepare( query );
656 }
657 catch( std::exception& e )
658 {
659 m_lastError = e.what();
660 wxLogTrace( traceDatabase,
661 wxT( "Exception while preparing query for selectAllAndCache: %s" ),
662 m_lastError );
663
664 // Exception may be due to a connection error; nanodbc won't auto-reconnect
665 Disconnect();
666
667 return false;
668 }
669
670 nanodbc::result results;
671
672 try
673 {
674 results = nanodbc::execute( statement );
675 }
676 catch( std::exception& e )
677 {
678 m_lastError = e.what();
679 wxLogTrace( traceDatabase,
680 wxT( "Exception while executing query for selectAllAndCache: %s" ),
681 m_lastError );
682
683 // Exception may be due to a connection error; nanodbc won't auto-reconnect
684 Disconnect();
685
686 return false;
687 }
688
689 timer.Stop();
690
692 std::vector<RESULT_COLUMN> columns = resultColumns( results );
693
694 while( results.next() )
695 {
696 ROW result;
697
698 try
699 {
700 decodeRow( results, columns, result );
701 }
702 catch( std::runtime_error& e )
703 {
704 m_lastError = e.what();
705 wxLogTrace( traceDatabase, wxT( "Exception while parsing result %d from selectAllAndCache: %s" ),
706 cacheEntry.size(), m_lastError );
707 return false;
708 }
709
710 if( !result.count( aKey ) )
711 {
712 wxLogTrace( traceDatabase,
713 wxT( "selectAllAndCache: warning: key %s not found in result set" ), aKey );
714 continue;
715 }
716
717 std::string keyStr = std::any_cast<std::string>( result.at( aKey ) );
718 cacheEntry[keyStr] = result;
719 }
720
721 wxLogTrace( traceDatabase, wxT( "selectAllAndCache from %s completed in %0.1f ms" ), aTable,
722 timer.msecs() );
723
724 m_cache->Put( aTable, cacheEntry );
725 return true;
726 }
727 catch( std::exception& e )
728 {
729 m_lastError = e.what();
730 wxLogTrace( traceDatabase, wxT( "Exception in selectAllAndCache: %s" ), m_lastError );
731
732 // Exception may be due to a connection error; nanodbc won't auto-reconnect
733 Disconnect();
734
735 return false;
736 }
737}
738
739
740bool DATABASE_CONNECTION::SelectAll( const std::string& aTable, const std::string& aKey, std::vector<ROW>& aResults )
741{
742 std::lock_guard lock( m_queryMutex );
743
744 if( !m_conn )
745 {
746 wxLogTrace( traceDatabase, wxT( "Called SelectAll without valid connection!" ) );
747 return false;
748 }
749
750 auto tableMapIter = m_tables.find( aTable );
751
752 if( tableMapIter == m_tables.end() )
753 {
754 wxLogTrace( traceDatabase, wxT( "SelectAll: requested table %s not found in cache" ), aTable );
755 return false;
756 }
757
759
760 if( !m_cache->Get( aTable, cacheEntry ) )
761 {
762 if( !selectAllAndCache( aTable, aKey ) )
763 {
764 wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` cache fill failed" ), aTable );
765 return false;
766 }
767
768 // Now it should be filled
769 m_cache->Get( aTable, cacheEntry );
770 }
771 else
772 {
773 wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` - returning cached results" ), aTable );
774 }
775
776 if( !m_cache->Get( aTable, cacheEntry ) )
777 {
778 wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` failed to get results from cache!" ), aTable );
779 return false;
780 }
781
782 aResults.reserve( cacheEntry.size() );
783
784 for( auto &[ key, row ] : cacheEntry )
785 aResults.emplace_back( row );
786
787 return true;
788}
789
790
791bool DATABASE_CONNECTION::SelectSample( const std::string& aTable, size_t aMaxRows,
792 std::vector<std::string>& aColumns, std::vector<ROW>& aRows )
793{
794 std::lock_guard lock( m_queryMutex );
795
796 aColumns.clear();
797 aRows.clear();
798
799 if( !m_conn )
800 return false;
801
802 try
803 {
804 std::string select = fmt::format( "SELECT * FROM {}", quoted( aTable ) );
805 nanodbc::result results;
806
807 // Direct execution keeps the default forward-only cursor; prepare() would force a keyset
808 // cursor that some drivers build over the whole table before returning a row
809 try
810 {
811 // Ordered so every server returns the same rows; the first column is usually the indexed key
812 results = nanodbc::execute( *m_conn, fromUTF8( select + " ORDER BY 1" ), 1, m_timeout );
813 }
814 catch( const nanodbc::database_error& e )
815 {
816 // Some column types cannot be ordered (e.g. Postgres json), so fall back to server order
817 wxLogTrace( traceDatabase, wxT( "SelectSample: ordered query on %s failed (%s); retrying unordered" ),
818 aTable, e.what() );
819 results = nanodbc::execute( *m_conn, fromUTF8( select ), 1, m_timeout );
820 }
821
822 std::vector<RESULT_COLUMN> columns = resultColumns( results );
823
824 for( const RESULT_COLUMN& column : columns )
825 aColumns.push_back( column.name );
826
827 while( aRows.size() < aMaxRows && results.next() )
828 {
829 ROW row;
830 decodeRow( results, columns, row );
831 aRows.emplace_back( std::move( row ) );
832 }
833 }
834 catch( std::exception& e )
835 {
836 m_lastError = e.what();
837 wxLogTrace( traceDatabase, wxT( "SelectSample from %s failed: %s" ), aTable, m_lastError );
838 return false;
839 }
840
841 return true;
842}
843
844
845bool DATABASE_CONNECTION::GetTables( std::vector<std::string>& aTables )
846{
847 std::lock_guard lock( m_queryMutex );
848
849 aTables.clear();
850
851 if( !m_conn )
852 return false;
853
854 try
855 {
856 nanodbc::catalog catalog( *m_conn );
857 nanodbc::string current;
858
859 try
860 {
861 current = m_conn->catalog_name();
862 }
863 catch( const std::exception& )
864 {
865 // Without a current catalog every listed table is offered
866 }
867
868 auto listTables =
869 [&]( const nanodbc::string& aCatalog )
870 {
871 nanodbc::catalog::tables tables = catalog.find_tables( NANODBC_TEXT( "" ),
872 NANODBC_TEXT( "TABLE,VIEW" ),
873 NANODBC_TEXT( "" ), aCatalog );
874
875 while( tables.next() )
876 aTables.emplace_back( toUTF8( tables.table_name() ) );
877 };
878
879 // MariaDB Connector/ODBC 3.1 lists every visible database but queries name bare tables in the
880 // current one; the driver applies its case rules to this restriction, a pattern under ODBC 3
881 listTables( fromUTF8( escapePattern( toUTF8( current ) ) ) );
882
883 // A catalog name the driver reported truncated or spelled differently matches nothing
884 if( aTables.empty() && !current.empty() )
885 listTables( NANODBC_TEXT( "" ) );
886 }
887 catch( std::exception& e )
888 {
889 m_lastError = e.what();
890 wxLogTrace( traceDatabase, wxT( "GetTables failed: %s" ), m_lastError );
891 return false;
892 }
893
894 std::sort( aTables.begin(), aTables.end() );
895 return true;
896}
897
898
899bool DATABASE_CONNECTION::GetColumns( const std::string& aTable, std::vector<std::string>& aColumns )
900{
901 std::lock_guard lock( m_queryMutex );
902
903 aColumns.clear();
904
905 if( !m_conn )
906 return false;
907
908 try
909 {
910 nanodbc::catalog catalog( *m_conn );
911 nanodbc::catalog::columns columns = catalog.find_columns( NANODBC_TEXT( "" ),
912 fromUTF8( escapePattern( aTable ) ) );
913
914 while( columns.next() )
915 aColumns.emplace_back( toUTF8( columns.column_name() ) );
916 }
917 catch( std::exception& e )
918 {
919 m_lastError = e.what();
920 wxLogTrace( traceDatabase, wxT( "GetColumns for %s failed: %s" ), aTable, m_lastError );
921 return false;
922 }
923
924 return true;
925}
926
927
928std::vector<DATABASE_CONNECTION::DATA_SOURCE> DATABASE_CONNECTION::ListDataSourceDetails()
929{
930 std::vector<DATA_SOURCE> sources;
931
932 try
933 {
934 for( const nanodbc::datasource& source : nanodbc::list_datasources() )
935 sources.push_back( { toUTF8( source.name ), toUTF8( source.driver ) } );
936 }
937 catch( std::exception& e )
938 {
939 wxLogTrace( traceDatabase, wxT( "ListDataSourceDetails failed: %s" ), e.what() );
940 }
941
942 return sources;
943}
944
945
946std::vector<std::string> DATABASE_CONNECTION::ListDrivers()
947{
948 std::vector<std::string> names;
949
950 try
951 {
952 for( const nanodbc::driver& driver : nanodbc::list_drivers() )
953 names.emplace_back( toUTF8( driver.name ) );
954 }
955 catch( std::exception& e )
956 {
957 wxLogTrace( traceDatabase, wxT( "ListDrivers failed: %s" ), e.what() );
958 }
959
960 return names;
961}
962
963
965 const std::string& aMessage )
966{
967 std::string message = boost::to_lower_copy( aMessage );
968
969 auto mentions =
970 [&]( std::initializer_list<const char*> aPhrases )
971 {
972 for( const char* phrase : aPhrases )
973 {
974 if( message.find( phrase ) != std::string::npos )
975 return true;
976 }
977
978 return false;
979 };
980
981 // Postgres reports rejected logins as 08001, so the text decides; MySQL's database-level
982 // "access denied" lacks the password clause and is not a login failure
983 if( aState.starts_with( "28" )
984 || mentions( { "password authentication failed", "(using password:", "login failed", "authentication failed",
985 "no password supplied" } ) )
986 {
987 return FAILURE::LOGIN;
988 }
989
990 if( aState == "IM002" || mentions( { "data source name not found" } ) )
991 return FAILURE::NO_DSN;
992
993 if( aState == "IM003" || mentions( { "can't open lib", "could not be loaded" } ) )
994 return FAILURE::NO_DRIVER;
995
996 if( aState.starts_with( "HYT" ) || mentions( { "timeout expired", "timed out" } ) )
997 return FAILURE::TIMEOUT;
998
999 // Class 08 alone also covers failures after the server answered, such as certificate checks
1000 if( mentions( { "connection refused", "could not connect", "can't connect", "unknown host",
1001 "could not translate host name", "no route to host", "network is unreachable" } ) )
1002 {
1003 return FAILURE::UNREACHABLE;
1004 }
1005
1006 return FAILURE::UNKNOWN;
1007}
FAILURE
Why a connection attempt failed, as far as the driver's report tells.
@ TIMEOUT
The server 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.
@ NO_DRIVER
The driver named by the data source could not be loaded.
bool GetColumns(const std::string &aTable, std::vector< std::string > &aColumns)
Lists the columns of a table in catalog order.
static 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 std::vector< std::string > ListDrivers()
std::map< std::string, std::any > ROW
std::map< std::string, std::map< std::string, int > > m_columnCache
Map of table -> map of column name -> data type.
std::string quoted(const std::string &aIdentifier) const
Wraps an identifier in the driver's quote character, doubling any it contains.
void SetCacheParams(int aMaxSize, int aMaxAge)
bool selectAllAndCache(const std::string &aTable, const std::string &aKey)
The caller must already hold m_queryMutex.
bool SelectAll(const std::string &aTable, const std::string &aKey, std::vector< ROW > &aResults)
Retrieves all rows from a database table.
std::string escapePattern(const std::string &aName) const
Escapes catalog search pattern characters so a name matches only itself.
DATABASE_CONNECTION(const std::string &aDataSourceName, const std::string &aUsername, const std::string &aPassword, int aTimeoutSeconds=DEFAULT_TIMEOUT, bool aConnectNow=true)
std::unique_ptr< DB_CACHE_TYPE > m_cache
bool GetTables(std::vector< std::string > &aTables)
Lists the tables and views visible through this connection, sorted by name.
bool SelectSample(const std::string &aTable, size_t aMaxRows, std::vector< std::string > &aColumns, std::vector< ROW > &aRows)
Reads the first rows of a table with a forward-only cursor, bypassing the row cache.
std::string columnsFor(const std::string &aTable)
std::unique_ptr< nanodbc::connection > m_conn
bool SelectOne(const std::string &aTable, const std::pair< std::string, std::string > &aWhere, ROW &aResult)
Retrieves a single row from a database table.
static std::vector< DATA_SOURCE > ListDataSourceDetails()
bool CacheTableInfo(const std::string &aTable, const std::set< std::string > &aRequiredColumns, const std::set< std::string > &aOptionalColumns={})
Caches schema information for a table so that subsequent queries know which columns exist.
std::map< std::string, std::string > m_tables
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
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 void decodeRow(nanodbc::result &aResults, const std::vector< RESULT_COLUMN > &aColumns, DATABASE_CONNECTION::ROW &aRow)
Decodes the current row of a result set into strings keyed by column name.
const char *const traceDatabase
std::string toUTF8(const nanodbc::string &aString)
Converts a string from nanodbc-native to KiCad-native.
nanodbc::string fromUTF8(const std::string &aString)
When Unicode support is enabled in nanodbc, string formats are used matching the appropriate characte...
const char *const traceDatabase
static bool empty(const wxTextEntryBase *aCtrl)
std::string source
std::string message
wxString result
Test unit parsing edge cases and error handling.