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 along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20
21#include <boost/algorithm/string.hpp>
22#include <boost/locale.hpp>
23#include <fmt/core.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>& aColumns )
212{
213 if( !m_conn )
214 return false;
215
216 try
217 {
218 nanodbc::catalog catalog( *m_conn );
219 nanodbc::catalog::tables tables = catalog.find_tables( fromUTF8( aTable ) );
220
221 if( !tables.next() )
222 {
223 wxLogTrace( traceDatabase, wxT( "CacheTableInfo: table '%s' not found in catalog" ),
224 aTable );
225 return false;
226 }
227
228 std::string key = toUTF8( tables.table_name() );
229 m_tables[key] = toUTF8( tables.table_type() );
230
231 try
232 {
233 nanodbc::catalog::columns columns =
234 catalog.find_columns( NANODBC_TEXT( "" ), tables.table_name() );
235
236 std::set<std::string> columnsInCatalog;
237
238 while( columns.next() )
239 {
240 std::string columnKey = toUTF8( columns.column_name() );
241 std::string columnKeyLower = boost::to_lower_copy( columnKey );
242
243 if( aColumns.count( columnKeyLower ) )
244 {
245 m_columnCache[key][columnKey] = columns.data_type();
246 columnsInCatalog.insert( columnKeyLower );
247 }
248 }
249
250 // Some ODBC drivers (notably SQLite) don't report all columns via SQLColumns.
251 // For example, SQLite's PRAGMA table_info used by its ODBC driver doesn't return
252 // generated columns. Trust the user's configuration and add any requested columns
253 // that weren't found in the catalog. The actual query will fail with a clear error
254 // if the column doesn't exist, which is better than silently ignoring it.
255 for( const std::string& requestedCol : aColumns )
256 {
257 if( !columnsInCatalog.count( requestedCol ) && !requestedCol.empty() )
258 {
259 wxLogTrace( traceDatabase,
260 wxT( "CacheTableInfo: column '%s' not found in catalog for table "
261 "'%s', adding anyway" ),
262 requestedCol, key );
263
264 m_columnCache[key][requestedCol] = SQL_VARCHAR;
265 }
266 }
267 }
268 catch( nanodbc::database_error& e )
269 {
270 m_lastError = e.what();
271 wxLogTrace( traceDatabase, wxT( "Exception while syncing columns for table '%s': %s" ),
272 key, m_lastError );
273 return false;
274 }
275 }
276 catch( std::exception& e )
277 {
278 m_lastError = e.what();
279 wxLogTrace( traceDatabase, wxT( "Exception while caching table info: %s" ), m_lastError );
280 return false;
281 }
282
283 return true;
284}
285
286
288{
289 if( !m_conn )
290 return false;
291
292 try
293 {
294 nanodbc::string qc = m_conn->get_info<nanodbc::string>( SQL_IDENTIFIER_QUOTE_CHAR );
295
296 if( qc.empty() )
297 return false;
298
299 m_quoteChar = *toUTF8( qc ).begin();
300
301 wxLogTrace( traceDatabase, wxT( "Quote char retrieved: %c" ), m_quoteChar );
302 }
303 catch( std::exception& e )
304 {
305 m_lastError = e.what();
306 wxLogTrace( traceDatabase, wxT( "Exception while querying quote char: %s" ), m_lastError );
307 return false;
308 }
309
310 return true;
311}
312
313
314std::string DATABASE_CONNECTION::columnsFor( const std::string& aTable )
315{
316 if( !m_columnCache.count( aTable ) )
317 {
318 wxLogTrace( traceDatabase, wxT( "columnsFor: requested table %s missing from cache!" ),
319 aTable );
320 return "*";
321 }
322
323 if( m_columnCache[aTable].empty() )
324 {
325 wxLogTrace( traceDatabase, wxT( "columnsFor: requested table %s has no columns mapped!" ),
326 aTable );
327 return "*";
328 }
329
330 std::string ret;
331
332 for( const auto& [ columnName, columnType ] : m_columnCache[aTable] )
333 ret += fmt::format( "{}{}{}, ", m_quoteChar, columnName, m_quoteChar );
334
335 // strip tailing ', '
336 ret.resize( ret.length() - 2 );
337
338 return ret;
339}
340
341//next step, make SelectOne take from the SelectAll cache if the SelectOne cache is missing.
342//To do this, need to build a map of PK->ROW for the cache result.
343bool DATABASE_CONNECTION::SelectOne( const std::string& aTable,
344 const std::pair<std::string, std::string>& aWhere,
345 DATABASE_CONNECTION::ROW& aResult )
346{
347 if( !m_conn )
348 {
349 wxLogTrace( traceDatabase, wxT( "Called SelectOne without valid connection!" ) );
350 return false;
351 }
352
353 auto tableMapIter = m_tables.find( aTable );
354
355 if( tableMapIter == m_tables.end() )
356 {
357 wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s not found in cache" ),
358 aTable );
359 return false;
360 }
361
362 const std::string& tableName = tableMapIter->first;
364
365 if( m_cache->Get( tableName, cacheEntry ) )
366 {
367 if( cacheEntry.count( aWhere.second ) )
368 {
369 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s` - cache hit" ),
370 tableName, aWhere.second );
371 aResult = cacheEntry.at( aWhere.second );
372 return true;
373 }
374 }
375 else
376 {
377 wxLogTrace( traceDatabase, wxT( "SelectOne: table `%s` not in row cache; will SelectAll" ),
378 tableName, aWhere.second );
379
380 selectAllAndCache( tableName, aWhere.first );
381
382 if( m_cache->Get( tableName, cacheEntry ) )
383 {
384 if( cacheEntry.count( aWhere.second ) )
385 {
386 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s` - cache hit" ),
387 tableName, aWhere.second );
388 aResult = cacheEntry.at( aWhere.second );
389 return true;
390 }
391 }
392 }
393
394 if( !m_columnCache.count( tableName ) )
395 {
396 wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s missing from column cache" ),
397 tableName );
398 return false;
399 }
400
401 auto columnCacheIter = m_columnCache.at( tableName ).find( aWhere.first );
402
403 if( columnCacheIter == m_columnCache.at( tableName ).end() )
404 {
405 wxLogTrace( traceDatabase, wxT( "SelectOne: requested column %s not found in cache for %s" ),
406 aWhere.first, tableName );
407 return false;
408 }
409
410 const std::string& columnName = columnCacheIter->first;
411
412 std::string cacheKey = fmt::format( "{}{}{}", tableName, columnName, aWhere.second );
413
414 std::string queryStr = fmt::format( "SELECT {} FROM {}{}{} WHERE {}{}{} = ?",
415 columnsFor( tableName ),
416 m_quoteChar, tableName, m_quoteChar,
417 m_quoteChar, columnName, m_quoteChar );
418 nanodbc::string query = fromUTF8( queryStr );
419
420 PROF_TIMER timer;
421 nanodbc::statement statement;
422
423 try
424 {
425 statement.prepare( *m_conn, query );
426 }
427 catch( std::exception& e )
428 {
429 m_lastError = e.what();
430 wxLogTrace( traceDatabase, wxT( "Exception while preparing statement for SelectOne: %s" ),
431 m_lastError );
432
433 // Exception may be due to a connection error; nanodbc won't auto-reconnect
434 Disconnect();
435
436 return false;
437 }
438
439 // Pre-describe parameter as VARCHAR to avoid SQLDescribeParam call. Some ODBC drivers
440 // (Microsoft Access, Excel, CSV) don't implement SQLDescribeParam.
441 try
442 {
443 statement.describe_parameters( { 0 }, { SQL_VARCHAR }, { 255 }, { 0 } );
444 statement.bind( 0, aWhere.second.c_str() );
445 }
446 catch( std::exception& e )
447 {
448 m_lastError = e.what();
449 wxLogTrace( traceDatabase, wxT( "Exception while binding parameter for SelectOne: %s" ),
450 m_lastError );
451
452 Disconnect();
453
454 return false;
455 }
456
457 wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s`" ), toUTF8( query ),
458 aWhere.second );
459
460 nanodbc::result results;
461
462 try
463 {
464 results = nanodbc::execute( statement );
465 }
466 catch( std::exception& e )
467 {
468 m_lastError = e.what();
469 wxLogTrace( traceDatabase, wxT( "Exception while executing statement for SelectOne: %s" ),
470 m_lastError );
471
472 // Exception may be due to a connection error; nanodbc won't auto-reconnect
473 Disconnect();
474
475 return false;
476 }
477
478 timer.Stop();
479
480
481 if( !results.first() )
482 {
483 wxLogTrace( traceDatabase, wxT( "SelectOne: no results returned from query" ) );
484 return false;
485 }
486
487 wxLogTrace( traceDatabase, wxT( "SelectOne: %ld results returned from query in %0.1f ms" ),
488 results.rows(), timer.msecs() );
489
490 aResult.clear();
491
492 try
493 {
494 for( short i = 0; i < results.columns(); ++i )
495 {
496 std::string column = toUTF8( results.column_name( i ) );
497
498 switch( results.column_datatype( i ) )
499 {
500 case SQL_DOUBLE:
501 case SQL_FLOAT:
502 case SQL_REAL:
503 case SQL_DECIMAL:
504 case SQL_NUMERIC:
505 {
506 try
507 {
508 aResult[column] = fmt::format( "{:G}", results.get<double>( i ) );
509 }
510 catch( nanodbc::null_access_error& )
511 {
512 // Column was empty (null)
513 aResult[column] = std::string();
514 }
515
516 break;
517 }
518
519 default:
520 aResult[column] = toUTF8( results.get<nanodbc::string>( i, NANODBC_TEXT( "" ) ) );
521 }
522 }
523 }
524 catch( std::exception& e )
525 {
526 m_lastError = e.what();
527 wxLogTrace( traceDatabase, wxT( "Exception while parsing results from SelectOne: %s" ),
528 m_lastError );
529 return false;
530 }
531
532 return true;
533}
534
535
536bool DATABASE_CONNECTION::selectAllAndCache( const std::string& aTable, const std::string& aKey )
537{
538 try
539 {
540 nanodbc::statement statement( *m_conn );
541
542 nanodbc::string query = fromUTF8( fmt::format( "SELECT {} FROM {}{}{}",
543 columnsFor( aTable ),
544 m_quoteChar, aTable, m_quoteChar ) );
545
546 wxLogTrace( traceDatabase, wxT( "selectAllAndCache: `%s`" ), toUTF8( query ) );
547
548 PROF_TIMER timer;
549
550 try
551 {
552 statement.prepare( query );
553 }
554 catch( std::exception& e )
555 {
556 m_lastError = e.what();
557 wxLogTrace( traceDatabase,
558 wxT( "Exception while preparing query for selectAllAndCache: %s" ),
559 m_lastError );
560
561 // Exception may be due to a connection error; nanodbc won't auto-reconnect
562 Disconnect();
563
564 return false;
565 }
566
567 nanodbc::result results;
568
569 try
570 {
571 results = nanodbc::execute( statement );
572 }
573 catch( std::exception& e )
574 {
575 m_lastError = e.what();
576 wxLogTrace( traceDatabase,
577 wxT( "Exception while executing query for selectAllAndCache: %s" ),
578 m_lastError );
579
580 // Exception may be due to a connection error; nanodbc won't auto-reconnect
581 Disconnect();
582
583 return false;
584 }
585
586 timer.Stop();
587
589
590 auto handleException =
591 [&]( std::runtime_error& aException, const std::string& aExtraContext = "" )
592 {
593 m_lastError = aException.what();
594 std::string extra = aExtraContext.empty() ? "" : ": " + aExtraContext;
595 wxLogTrace( traceDatabase,
596 wxT( "Exception while parsing result %d from selectAllAndCache: %s%s" ),
597 cacheEntry.size(), m_lastError, extra );
598 };
599
600 while( results.next() )
601 {
602 short columnCount = 0;
603 ROW result;
604
605 try
606 {
607 columnCount = results.columns();
608 }
609 catch( nanodbc::database_error& e )
610 {
611 handleException( e );
612 return false;
613 }
614
615 for( short j = 0; j < columnCount; ++j )
616 {
617 std::string column;
618 std::string columnExtraDbgInfo;
619 int datatype = SQL_UNKNOWN_TYPE;
620
621 try
622 {
623 column = toUTF8( results.column_name( j ) );
624 datatype = results.column_datatype( j );
625 columnExtraDbgInfo = fmt::format( "column index {}, name '{}', type {}",
626 j,
627 column,
628 datatype );
629 }
630 catch( nanodbc::index_range_error& e )
631 {
632 handleException( e, columnExtraDbgInfo );
633 return false;
634 }
635
636 switch( datatype )
637 {
638 case SQL_DOUBLE:
639 case SQL_FLOAT:
640 case SQL_REAL:
641 case SQL_DECIMAL:
642 case SQL_NUMERIC:
643 try
644 {
645 result[column] = fmt::format( "{:G}", results.get<double>( j ) );
646 }
647 catch( nanodbc::null_access_error& )
648 {
649 // Column was empty (null)
650 result[column] = std::string();
651 }
652 catch( std::runtime_error& e )
653 {
654 handleException( e, columnExtraDbgInfo );
655 return false;
656 }
657
658 break;
659
660 default:
661 try
662 {
663 result[column] = toUTF8( results.get<nanodbc::string>( j, NANODBC_TEXT( "" ) ) );
664 }
665 catch( std::runtime_error& e )
666 {
667 handleException( e, columnExtraDbgInfo );
668 return false;
669 }
670 }
671 }
672
673 if( !result.count( aKey ) )
674 {
675 wxLogTrace( traceDatabase,
676 wxT( "selectAllAndCache: warning: key %s not found in result set" ), aKey );
677 continue;
678 }
679
680 std::string keyStr = std::any_cast<std::string>( result.at( aKey ) );
681 cacheEntry[keyStr] = result;
682 }
683
684 wxLogTrace( traceDatabase, wxT( "selectAllAndCache from %s completed in %0.1f ms" ), aTable,
685 timer.msecs() );
686
687 m_cache->Put( aTable, cacheEntry );
688 return true;
689 }
690 catch( std::exception& e )
691 {
692 m_lastError = e.what();
693 wxLogTrace( traceDatabase, wxT( "Exception in selectAllAndCache: %s" ), m_lastError );
694
695 // Exception may be due to a connection error; nanodbc won't auto-reconnect
696 Disconnect();
697
698 return false;
699 }
700}
701
702
703bool DATABASE_CONNECTION::SelectAll( const std::string& aTable, const std::string& aKey, std::vector<ROW>& aResults )
704{
705 if( !m_conn )
706 {
707 wxLogTrace( traceDatabase, wxT( "Called SelectAll without valid connection!" ) );
708 return false;
709 }
710
711 auto tableMapIter = m_tables.find( aTable );
712
713 if( tableMapIter == m_tables.end() )
714 {
715 wxLogTrace( traceDatabase, wxT( "SelectAll: requested table %s not found in cache" ), aTable );
716 return false;
717 }
718
720
721 if( !m_cache->Get( aTable, cacheEntry ) )
722 {
723 if( !selectAllAndCache( aTable, aKey ) )
724 {
725 wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` cache fill failed" ), aTable );
726 return false;
727 }
728
729 // Now it should be filled
730 m_cache->Get( aTable, cacheEntry );
731 }
732
733 if( !m_cache->Get( aTable, cacheEntry ) )
734 {
735 wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` failed to get results from cache!" ), aTable );
736 return false;
737 }
738
739 wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` - returning cached results" ), aTable );
740
741 aResults.reserve( cacheEntry.size() );
742
743 for( auto &[ key, row ] : cacheEntry )
744 aResults.emplace_back( row );
745
746 return true;
747}
bool CacheTableInfo(const std::string &aTable, const std::set< std::string > &aColumns)
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)
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.
std::map< std::string, std::string > m_tables
A small class to help profiling.
Definition profile.h:49
void Stop()
Save the time when this function was called, and set the counter stane to stop.
Definition profile.h:88
double msecs(bool aSinceLast=false)
Definition profile.h:149
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.