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 <boost/algorithm/string.hpp>
22#include <boost/locale.hpp>
23#include <fmt/format.h>
24#include <nanodbc/nanodbc.h>
25
26// Some outdated definitions are used in sql.h
27// We need to define them for "recent" dev tools
28#define INT64 int64_t
29#define UINT64 uint64_t
30
31#ifdef __MINGW32__
32#define BYTE uint8_t
33#define WORD uint16_t
34#define DWORD uint32_t
35#define HWND uint32_t /* dummy define */
36#endif
37
38#ifdef WIN32
39#include <windows.h> // for sql.h
40#endif
41
42#include <sql.h> // SQL_IDENTIFIER_QUOTE_CHAR
43
44#include <wx/log.h>
45
48#include <core/profile.h>
49
50
51const char* const traceDatabase = "KICAD_DATABASE";
52
60
66nanodbc::string fromUTF8( const std::string& aString )
67{
68 return boost::locale::conv::utf_to_utf<nanodbc::string::value_type>( aString );
69}
70
71
77std::string toUTF8( const nanodbc::string& aString )
78{
79 return boost::locale::conv::utf_to_utf<char>( aString );
80}
81
82
83DATABASE_CONNECTION::DATABASE_CONNECTION( const std::string& aDataSourceName,
84 const std::string& aUsername,
85 const std::string& aPassword, int aTimeoutSeconds,
86 bool aConnectNow ) :
87 m_quoteChar( '"' )
88{
89 m_dsn = aDataSourceName;
90 m_user = aUsername;
91 m_pass = aPassword;
92 m_timeout = aTimeoutSeconds;
93
94 init();
95
96 if( aConnectNow )
97 Connect();
98}
99
100
101DATABASE_CONNECTION::DATABASE_CONNECTION( const std::string& aConnectionString,
102 int aTimeoutSeconds, bool aConnectNow ) :
103 m_quoteChar( '"' )
104{
105 m_connectionString = aConnectionString;
106 m_timeout = aTimeoutSeconds;
107
108 init();
109
110 if( aConnectNow )
111 Connect();
112}
113
114
120
121
123{
124 m_cache = std::make_unique<DB_CACHE_TYPE>( 10, 1 );
125}
126
127
128void DATABASE_CONNECTION::SetCacheParams( int aMaxSize, int aMaxAge )
129{
130 if( !m_cache )
131 return;
132
133 if( aMaxSize < 0 )
134 aMaxSize = 0;
135
136 if( aMaxAge < 0 )
137 aMaxAge = 0;
138
139 m_cache->SetMaxSize( static_cast<size_t>( aMaxSize ) );
140 m_cache->SetMaxAge( static_cast<time_t>( aMaxAge ) );
141}
142
143
145{
146 nanodbc::string dsn = fromUTF8( m_dsn );
147 nanodbc::string user = fromUTF8( m_user );
148 nanodbc::string pass = fromUTF8( m_pass );
149 nanodbc::string cs = fromUTF8( m_connectionString );
150
151 try
152 {
153 if( cs.empty() )
154 {
155 wxLogTrace( traceDatabase, wxT( "Creating connection to DSN %s" ), m_dsn );
156 m_conn = std::make_unique<nanodbc::connection>( dsn, user, pass, m_timeout );
157 }
158 else
159 {
160 wxLogTrace( traceDatabase, wxT( "Creating connection with connection string" ) );
161 m_conn = std::make_unique<nanodbc::connection>( cs, m_timeout );
162 }
163 }
164 catch( std::exception& e )
165 {
166 m_lastError = e.what();
167 return false;
168 }
169
170 m_tables.clear();
171
172 if( IsConnected() )
173 getQuoteChar();
174
175 return IsConnected();
176}
177
178
180{
181 if( !m_conn )
182 {
183 wxLogTrace( traceDatabase, wxT( "Note: Disconnect() called without valid connection" ) );
184 return false;
185 }
186
187 try
188 {
189 m_conn->disconnect();
190 }
191 catch( std::exception& exc )
192 {
193 wxLogTrace( traceDatabase, wxT( "Disconnect() error \"%s\" occured." ), exc.what() );
194 return false;
195 }
196
197 return !m_conn->connected();
198}
199
200
202{
203 if( !m_conn )
204 return false;
205
206 return m_conn->connected();
207}
208
209
210bool DATABASE_CONNECTION::CacheTableInfo( const std::string& aTable,
211 const std::set<std::string>& aRequiredColumns,
212 const std::set<std::string>& aOptionalColumns )
213{
214 std::lock_guard lock( m_queryMutex );
215
216 if( !m_conn )
217 return false;
218
219 // Catalog names are lowercase; normalize here so differently-cased columns still match
220 std::set<std::string> requiredLower;
221 std::set<std::string> optionalLower;
222
223 for( const std::string& col : aRequiredColumns )
224 requiredLower.insert( boost::to_lower_copy( col ) );
225
226 for( const std::string& col : aOptionalColumns )
227 optionalLower.insert( boost::to_lower_copy( col ) );
228
229 try
230 {
231 nanodbc::catalog catalog( *m_conn );
232 nanodbc::catalog::tables tables = catalog.find_tables( fromUTF8( aTable ) );
233
234 if( !tables.next() )
235 {
236 wxLogTrace( traceDatabase, wxT( "CacheTableInfo: table '%s' not found in catalog" ),
237 aTable );
238 return false;
239 }
240
241 std::string key = toUTF8( tables.table_name() );
242 m_tables[key] = toUTF8( tables.table_type() );
243
244 try
245 {
246 nanodbc::catalog::columns columns =
247 catalog.find_columns( NANODBC_TEXT( "" ), tables.table_name() );
248
249 std::set<std::string> columnsInCatalog;
250
251 while( columns.next() )
252 {
253 std::string columnKey = toUTF8( columns.column_name() );
254 std::string columnKeyLower = boost::to_lower_copy( columnKey );
255
256 if( requiredLower.count( columnKeyLower )
257 || optionalLower.count( columnKeyLower ) )
258 {
259 m_columnCache[key][columnKey] = columns.data_type();
260 columnsInCatalog.insert( columnKeyLower );
261 }
262 }
263
264 // SQLite's ODBC driver doesn't report generated columns via SQLColumns, so required
265 // columns are trusted and added even when absent from the catalog
266 for( const std::string& requestedCol : aRequiredColumns )
267 {
268 std::string requestedColLower = boost::to_lower_copy( requestedCol );
269
270 if( !columnsInCatalog.count( requestedColLower ) && !requestedCol.empty() )
271 {
272 wxLogTrace( traceDatabase,
273 wxT( "CacheTableInfo: column '%s' not found in catalog for table "
274 "'%s', adding anyway" ),
275 requestedCol, key );
276
277 m_columnCache[key][requestedCol] = SQL_VARCHAR;
278 }
279 }
280
281 // Unlike required columns, a missing optional column is dropped, not added, but is
282 // still worth a warning since it usually means a misconfigured .kicad_dbl mapping
283 for( const std::string& requestedCol : aOptionalColumns )
284 {
285 std::string requestedColLower = boost::to_lower_copy( requestedCol );
286
287 if( !columnsInCatalog.count( requestedColLower ) && !requestedCol.empty() )
288 {
289 wxLogWarning( wxT( "Database table '%s' has no column '%s'; ignoring the "
290 "misconfigured properties mapping." ),
291 key, requestedCol );
292 }
293 }
294 }
295 catch( nanodbc::database_error& e )
296 {
297 m_lastError = e.what();
298 wxLogTrace( traceDatabase, wxT( "Exception while syncing columns for table '%s': %s" ),
299 key, m_lastError );
300 return false;
301 }
302 }
303 catch( std::exception& e )
304 {
305 m_lastError = e.what();
306 wxLogTrace( traceDatabase, wxT( "Exception while caching table info: %s" ), m_lastError );
307 return false;
308 }
309
310 return true;
311}
312
313
315{
316 if( !m_conn )
317 return false;
318
319 try
320 {
321 nanodbc::string qc = m_conn->get_info<nanodbc::string>( SQL_IDENTIFIER_QUOTE_CHAR );
322
323 if( qc.empty() )
324 return false;
325
326 m_quoteChar = *toUTF8( qc ).begin();
327
328 wxLogTrace( traceDatabase, wxT( "Quote char retrieved: %c" ), m_quoteChar );
329 }
330 catch( std::exception& e )
331 {
332 m_lastError = e.what();
333 wxLogTrace( traceDatabase, wxT( "Exception while querying quote char: %s" ), m_lastError );
334 return false;
335 }
336
337 return true;
338}
339
340
341std::string DATABASE_CONNECTION::columnsFor( const std::string& aTable )
342{
343 if( !m_columnCache.count( aTable ) )
344 {
345 wxLogTrace( traceDatabase, wxT( "columnsFor: requested table %s missing from cache!" ),
346 aTable );
347 return "*";
348 }
349
350 if( m_columnCache[aTable].empty() )
351 {
352 wxLogTrace( traceDatabase, wxT( "columnsFor: requested table %s has no columns mapped!" ),
353 aTable );
354 return "*";
355 }
356
357 std::string ret;
358
359 for( const auto& [ columnName, columnType ] : m_columnCache[aTable] )
360 ret += fmt::format( "{}{}{}, ", m_quoteChar, columnName, m_quoteChar );
361
362 // strip tailing ', '
363 ret.resize( ret.length() - 2 );
364
365 return ret;
366}
367
368//next step, make SelectOne take from the SelectAll cache if the SelectOne cache is missing.
369//To do this, need to build a map of PK->ROW for the cache result.
370bool DATABASE_CONNECTION::SelectOne( const std::string& aTable,
371 const std::pair<std::string, std::string>& aWhere,
372 DATABASE_CONNECTION::ROW& aResult )
373{
374 std::lock_guard lock( m_queryMutex );
375
376 if( !m_conn )
377 {
378 wxLogTrace( traceDatabase, wxT( "Called SelectOne without valid connection!" ) );
379 return false;
380 }
381
382 auto tableMapIter = m_tables.find( aTable );
383
384 if( tableMapIter == m_tables.end() )
385 {
386 wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s not found in cache" ),
387 aTable );
388 return false;
389 }
390
391 const std::string& tableName = tableMapIter->first;
393
394 if( m_cache->Get( tableName, cacheEntry ) )
395 {
396 if( cacheEntry.count( aWhere.second ) )
397 {
398 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s` - cache hit" ),
399 tableName, aWhere.second );
400 aResult = cacheEntry.at( aWhere.second );
401 return true;
402 }
403 }
404 else
405 {
406 wxLogTrace( traceDatabase, wxT( "SelectOne: table `%s` not in row cache; will SelectAll" ),
407 tableName, aWhere.second );
408
409 selectAllAndCache( tableName, aWhere.first );
410
411 if( m_cache->Get( tableName, cacheEntry ) )
412 {
413 if( cacheEntry.count( aWhere.second ) )
414 {
415 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s` - cache hit" ),
416 tableName, aWhere.second );
417 aResult = cacheEntry.at( aWhere.second );
418 return true;
419 }
420 }
421 }
422
423 if( !m_columnCache.count( tableName ) )
424 {
425 wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s missing from column cache" ),
426 tableName );
427 return false;
428 }
429
430 auto columnCacheIter = m_columnCache.at( tableName ).find( aWhere.first );
431
432 if( columnCacheIter == m_columnCache.at( tableName ).end() )
433 {
434 wxLogTrace( traceDatabase, wxT( "SelectOne: requested column %s not found in cache for %s" ),
435 aWhere.first, tableName );
436 return false;
437 }
438
439 const std::string& columnName = columnCacheIter->first;
440
441 std::string cacheKey = fmt::format( "{}{}{}", tableName, columnName, aWhere.second );
442
443 std::string queryStr = fmt::format( "SELECT {} FROM {}{}{} WHERE {}{}{} = ?",
444 columnsFor( tableName ),
445 m_quoteChar, tableName, m_quoteChar,
446 m_quoteChar, columnName, m_quoteChar );
447 nanodbc::string query = fromUTF8( queryStr );
448
449 PROF_TIMER timer;
450 nanodbc::statement statement;
451
452 try
453 {
454 statement.prepare( *m_conn, query );
455 }
456 catch( std::exception& e )
457 {
458 m_lastError = e.what();
459 wxLogTrace( traceDatabase, wxT( "Exception while preparing statement for SelectOne: %s" ),
460 m_lastError );
461
462 // Exception may be due to a connection error; nanodbc won't auto-reconnect
463 Disconnect();
464
465 return false;
466 }
467
468 // Pre-describe parameter as VARCHAR to avoid SQLDescribeParam call. Some ODBC drivers
469 // (Microsoft Access, Excel, CSV) don't implement SQLDescribeParam.
470 try
471 {
472 statement.describe_parameters( { 0 }, { SQL_VARCHAR }, { 255 }, { 0 } );
473 statement.bind( 0, aWhere.second.c_str() );
474 }
475 catch( std::exception& e )
476 {
477 m_lastError = e.what();
478 wxLogTrace( traceDatabase, wxT( "Exception while binding parameter for SelectOne: %s" ),
479 m_lastError );
480
481 Disconnect();
482
483 return false;
484 }
485
486 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s`" ), toUTF8( query ),
487 aWhere.second );
488
489 nanodbc::result results;
490
491 try
492 {
493 results = nanodbc::execute( statement );
494 }
495 catch( std::exception& e )
496 {
497 m_lastError = e.what();
498 wxLogTrace( traceDatabase, wxT( "Exception while executing statement for SelectOne: %s" ),
499 m_lastError );
500
501 // Exception may be due to a connection error; nanodbc won't auto-reconnect
502 Disconnect();
503
504 return false;
505 }
506
507 timer.Stop();
508
509
510 if( !results.first() )
511 {
512 wxLogTrace( traceDatabase, wxT( "SelectOne: no results returned from query" ) );
513 return false;
514 }
515
516 wxLogTrace( traceDatabase, wxT( "SelectOne: %ld results returned from query in %0.1f ms" ),
517 results.rows(), timer.msecs() );
518
519 aResult.clear();
520
521 try
522 {
523 for( short i = 0; i < results.columns(); ++i )
524 {
525 std::string column = toUTF8( results.column_name( i ) );
526
527 switch( results.column_datatype( i ) )
528 {
529 case SQL_DOUBLE:
530 case SQL_FLOAT:
531 case SQL_REAL:
532 case SQL_DECIMAL:
533 case SQL_NUMERIC:
534 {
535 try
536 {
537 aResult[column] = fmt::format( "{:G}", results.get<double>( i ) );
538 }
539 catch( nanodbc::null_access_error& )
540 {
541 // Column was empty (null)
542 aResult[column] = std::string();
543 }
544
545 break;
546 }
547
548 default:
549 aResult[column] = toUTF8( results.get<nanodbc::string>( i, NANODBC_TEXT( "" ) ) );
550 }
551 }
552 }
553 catch( std::exception& e )
554 {
555 m_lastError = e.what();
556 wxLogTrace( traceDatabase, wxT( "Exception while parsing results from SelectOne: %s" ),
557 m_lastError );
558 return false;
559 }
560
561 return true;
562}
563
564
565bool DATABASE_CONNECTION::selectAllAndCache( const std::string& aTable, const std::string& aKey )
566{
567 try
568 {
569 nanodbc::statement statement( *m_conn );
570
571 nanodbc::string query = fromUTF8( fmt::format( "SELECT {} FROM {}{}{}",
572 columnsFor( aTable ),
573 m_quoteChar, aTable, m_quoteChar ) );
574
575 PROF_TIMER timer;
576
577 try
578 {
579 statement.prepare( query );
580 }
581 catch( std::exception& e )
582 {
583 m_lastError = e.what();
584 wxLogTrace( traceDatabase,
585 wxT( "Exception while preparing query for selectAllAndCache: %s" ),
586 m_lastError );
587
588 // Exception may be due to a connection error; nanodbc won't auto-reconnect
589 Disconnect();
590
591 return false;
592 }
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,
604 wxT( "Exception while executing query for selectAllAndCache: %s" ),
605 m_lastError );
606
607 // Exception may be due to a connection error; nanodbc won't auto-reconnect
608 Disconnect();
609
610 return false;
611 }
612
613 timer.Stop();
614
616
617 auto handleException =
618 [&]( std::runtime_error& aException, const std::string& aExtraContext = "" )
619 {
620 m_lastError = aException.what();
621 std::string extra = aExtraContext.empty() ? "" : ": " + aExtraContext;
622 wxLogTrace( traceDatabase,
623 wxT( "Exception while parsing result %d from selectAllAndCache: %s%s" ),
624 cacheEntry.size(), m_lastError, extra );
625 };
626
627 while( results.next() )
628 {
629 short columnCount = 0;
630 ROW result;
631
632 try
633 {
634 columnCount = results.columns();
635 }
636 catch( nanodbc::database_error& e )
637 {
638 handleException( e );
639 return false;
640 }
641
642 for( short j = 0; j < columnCount; ++j )
643 {
644 std::string column;
645 std::string columnExtraDbgInfo;
646 int datatype = SQL_UNKNOWN_TYPE;
647
648 try
649 {
650 column = toUTF8( results.column_name( j ) );
651 datatype = results.column_datatype( j );
652 columnExtraDbgInfo = fmt::format( "column index {}, name '{}', type {}",
653 j,
654 column,
655 datatype );
656 }
657 catch( nanodbc::index_range_error& e )
658 {
659 handleException( e, columnExtraDbgInfo );
660 return false;
661 }
662
663 switch( datatype )
664 {
665 case SQL_DOUBLE:
666 case SQL_FLOAT:
667 case SQL_REAL:
668 case SQL_DECIMAL:
669 case SQL_NUMERIC:
670 try
671 {
672 result[column] = fmt::format( "{:G}", results.get<double>( j ) );
673 }
674 catch( nanodbc::null_access_error& )
675 {
676 // Column was empty (null)
677 result[column] = std::string();
678 }
679 catch( std::runtime_error& e )
680 {
681 handleException( e, columnExtraDbgInfo );
682 return false;
683 }
684
685 break;
686
687 default:
688 try
689 {
690 result[column] = toUTF8( results.get<nanodbc::string>( j, NANODBC_TEXT( "" ) ) );
691 }
692 catch( std::runtime_error& e )
693 {
694 handleException( e, columnExtraDbgInfo );
695 return false;
696 }
697 }
698 }
699
700 if( !result.count( aKey ) )
701 {
702 wxLogTrace( traceDatabase,
703 wxT( "selectAllAndCache: warning: key %s not found in result set" ), aKey );
704 continue;
705 }
706
707 std::string keyStr = std::any_cast<std::string>( result.at( aKey ) );
708 cacheEntry[keyStr] = result;
709 }
710
711 wxLogTrace( traceDatabase, wxT( "selectAllAndCache from %s completed in %0.1f ms" ), aTable,
712 timer.msecs() );
713
714 m_cache->Put( aTable, cacheEntry );
715 return true;
716 }
717 catch( std::exception& e )
718 {
719 m_lastError = e.what();
720 wxLogTrace( traceDatabase, wxT( "Exception in selectAllAndCache: %s" ), m_lastError );
721
722 // Exception may be due to a connection error; nanodbc won't auto-reconnect
723 Disconnect();
724
725 return false;
726 }
727}
728
729
730bool DATABASE_CONNECTION::SelectAll( const std::string& aTable, const std::string& aKey, std::vector<ROW>& aResults )
731{
732 std::lock_guard lock( m_queryMutex );
733
734 if( !m_conn )
735 {
736 wxLogTrace( traceDatabase, wxT( "Called SelectAll without valid connection!" ) );
737 return false;
738 }
739
740 auto tableMapIter = m_tables.find( aTable );
741
742 if( tableMapIter == m_tables.end() )
743 {
744 wxLogTrace( traceDatabase, wxT( "SelectAll: requested table %s not found in cache" ), aTable );
745 return false;
746 }
747
749
750 if( !m_cache->Get( aTable, cacheEntry ) )
751 {
752 if( !selectAllAndCache( aTable, aKey ) )
753 {
754 wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` cache fill failed" ), aTable );
755 return false;
756 }
757
758 // Now it should be filled
759 m_cache->Get( aTable, cacheEntry );
760 }
761 else
762 {
763 wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` - returning cached results" ), aTable );
764 }
765
766 if( !m_cache->Get( aTable, cacheEntry ) )
767 {
768 wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` failed to get results from cache!" ), aTable );
769 return false;
770 }
771
772 aResults.reserve( cacheEntry.size() );
773
774 for( auto &[ key, row ] : cacheEntry )
775 aResults.emplace_back( row );
776
777 return true;
778}
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.
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.
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
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.
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:147
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)
wxString result
Test unit parsing edge cases and error handling.