You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

697 lines
19 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2022 Jon Evans <jon@craftyjon.com>
  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. #include <boost/algorithm/string.hpp>
  21. #include <boost/locale.hpp>
  22. #include <fmt/core.h>
  23. #include <nanodbc/nanodbc.h>
  24. // Some outdated definitions are used in sql.h
  25. // We need to define them for "recent" dev tools
  26. #define INT64 int64_t
  27. #define UINT64 uint64_t
  28. #ifdef __MINGW32__
  29. #define BYTE uint8_t
  30. #define WORD uint16_t
  31. #define DWORD uint32_t
  32. #define HWND uint32_t /* dummy define */
  33. #endif
  34. #ifdef WIN32
  35. #include <windows.h> // for sql.h
  36. #endif
  37. #include <sql.h> // SQL_IDENTIFIER_QUOTE_CHAR
  38. #include <wx/log.h>
  39. #include <database/database_connection.h>
  40. #include <database/database_cache.h>
  41. #include <core/profile.h>
  42. const char* const traceDatabase = "KICAD_DATABASE";
  43. /**
  44. * When Unicode support is enabled in nanodbc, string formats are used matching the appropriate
  45. * character set of the platform. KiCad uses UTF-8 encoded strings internally, but different
  46. * platforms use different encodings for SQL strings. Unicode mode must be enabled for compilation
  47. * on Windows, since Visual Studio forces the use of Unicode SQL headers if any part of the project
  48. * has Unicode enabled.
  49. */
  50. /**
  51. * Converts a string from KiCad-native to nanodbc-native
  52. * @param aString is a UTF-8 encoded string
  53. * @return a string in nanodbc's platform-specific representation
  54. */
  55. nanodbc::string fromUTF8( const std::string& aString )
  56. {
  57. return boost::locale::conv::utf_to_utf<nanodbc::string::value_type>( aString );
  58. }
  59. /**
  60. * Converts a string from nanodbc-native to KiCad-native
  61. * @param aString is a string encoded in nanodbc's platform-specific way
  62. * @return a string with UTF-8 encoding
  63. */
  64. std::string toUTF8( const nanodbc::string& aString )
  65. {
  66. return boost::locale::conv::utf_to_utf<char>( aString );
  67. }
  68. DATABASE_CONNECTION::DATABASE_CONNECTION( const std::string& aDataSourceName,
  69. const std::string& aUsername,
  70. const std::string& aPassword, int aTimeoutSeconds,
  71. bool aConnectNow ) :
  72. m_quoteChar( '"' )
  73. {
  74. m_dsn = aDataSourceName;
  75. m_user = aUsername;
  76. m_pass = aPassword;
  77. m_timeout = aTimeoutSeconds;
  78. init();
  79. if( aConnectNow )
  80. Connect();
  81. }
  82. DATABASE_CONNECTION::DATABASE_CONNECTION( const std::string& aConnectionString,
  83. int aTimeoutSeconds, bool aConnectNow ) :
  84. m_quoteChar( '"' )
  85. {
  86. m_connectionString = aConnectionString;
  87. m_timeout = aTimeoutSeconds;
  88. init();
  89. if( aConnectNow )
  90. Connect();
  91. }
  92. DATABASE_CONNECTION::~DATABASE_CONNECTION()
  93. {
  94. Disconnect();
  95. m_conn.reset();
  96. }
  97. void DATABASE_CONNECTION::init()
  98. {
  99. m_cache = std::make_unique<DB_CACHE_TYPE>( 10, 1 );
  100. }
  101. void DATABASE_CONNECTION::SetCacheParams( int aMaxSize, int aMaxAge )
  102. {
  103. if( !m_cache )
  104. return;
  105. if( aMaxSize < 0 )
  106. aMaxSize = 0;
  107. if( aMaxAge < 0 )
  108. aMaxAge = 0;
  109. m_cache->SetMaxSize( static_cast<size_t>( aMaxSize ) );
  110. m_cache->SetMaxAge( static_cast<time_t>( aMaxAge ) );
  111. }
  112. bool DATABASE_CONNECTION::Connect()
  113. {
  114. nanodbc::string dsn = fromUTF8( m_dsn );
  115. nanodbc::string user = fromUTF8( m_user );
  116. nanodbc::string pass = fromUTF8( m_pass );
  117. nanodbc::string cs = fromUTF8( m_connectionString );
  118. try
  119. {
  120. if( cs.empty() )
  121. {
  122. wxLogTrace( traceDatabase, wxT( "Creating connection to DSN %s" ), m_dsn );
  123. m_conn = std::make_unique<nanodbc::connection>( dsn, user, pass, m_timeout );
  124. }
  125. else
  126. {
  127. wxLogTrace( traceDatabase, wxT( "Creating connection with connection string" ) );
  128. m_conn = std::make_unique<nanodbc::connection>( cs, m_timeout );
  129. }
  130. }
  131. catch( nanodbc::database_error& e )
  132. {
  133. m_lastError = e.what();
  134. return false;
  135. }
  136. m_tables.clear();
  137. if( IsConnected() )
  138. getQuoteChar();
  139. return IsConnected();
  140. }
  141. bool DATABASE_CONNECTION::Disconnect()
  142. {
  143. if( !m_conn )
  144. {
  145. wxLogTrace( traceDatabase, wxT( "Note: Disconnect() called without valid connection" ) );
  146. return false;
  147. }
  148. try
  149. {
  150. m_conn->disconnect();
  151. }
  152. catch( boost::locale::conv::conversion_error& exc )
  153. {
  154. wxLogTrace( traceDatabase, wxT( "Disconnect() error \"%s\" occured." ), exc.what() );
  155. return false;
  156. }
  157. return !m_conn->connected();
  158. }
  159. bool DATABASE_CONNECTION::IsConnected() const
  160. {
  161. if( !m_conn )
  162. return false;
  163. return m_conn->connected();
  164. }
  165. bool DATABASE_CONNECTION::CacheTableInfo( const std::string& aTable,
  166. const std::set<std::string>& aColumns )
  167. {
  168. if( !m_conn )
  169. return false;
  170. try
  171. {
  172. nanodbc::catalog catalog( *m_conn );
  173. nanodbc::catalog::tables tables = catalog.find_tables( fromUTF8( aTable ) );
  174. if( !tables.next() )
  175. {
  176. wxLogTrace( traceDatabase, wxT( "CacheTableInfo: table '%s' not found in catalog" ),
  177. aTable );
  178. return false;
  179. }
  180. std::string key = toUTF8( tables.table_name() );
  181. m_tables[key] = toUTF8( tables.table_type() );
  182. try
  183. {
  184. nanodbc::catalog::columns columns =
  185. catalog.find_columns( NANODBC_TEXT( "" ), tables.table_name() );
  186. while( columns.next() )
  187. {
  188. std::string columnKey = toUTF8( columns.column_name() );
  189. if( aColumns.count( boost::to_lower_copy( columnKey ) ) )
  190. m_columnCache[key][columnKey] = columns.data_type();
  191. }
  192. }
  193. catch( nanodbc::database_error& e )
  194. {
  195. m_lastError = e.what();
  196. wxLogTrace( traceDatabase, wxT( "Exception while syncing columns for table '%s': %s" ),
  197. key, m_lastError );
  198. return false;
  199. }
  200. }
  201. catch( nanodbc::database_error& e )
  202. {
  203. m_lastError = e.what();
  204. wxLogTrace( traceDatabase, wxT( "Exception while caching table info: %s" ), m_lastError );
  205. return false;
  206. }
  207. return true;
  208. }
  209. bool DATABASE_CONNECTION::getQuoteChar()
  210. {
  211. if( !m_conn )
  212. return false;
  213. try
  214. {
  215. nanodbc::string qc = m_conn->get_info<nanodbc::string>( SQL_IDENTIFIER_QUOTE_CHAR );
  216. if( qc.empty() )
  217. return false;
  218. m_quoteChar = *toUTF8( qc ).begin();
  219. wxLogTrace( traceDatabase, wxT( "Quote char retrieved: %c" ), m_quoteChar );
  220. }
  221. catch( nanodbc::database_error& )
  222. {
  223. wxLogTrace( traceDatabase, wxT( "Exception while querying quote char: %s" ), m_lastError );
  224. return false;
  225. }
  226. return true;
  227. }
  228. std::string DATABASE_CONNECTION::columnsFor( const std::string& aTable )
  229. {
  230. if( !m_columnCache.count( aTable ) )
  231. {
  232. wxLogTrace( traceDatabase, wxT( "columnsFor: requested table %s missing from cache!" ),
  233. aTable );
  234. return "*";
  235. }
  236. if( m_columnCache[aTable].empty() )
  237. {
  238. wxLogTrace( traceDatabase, wxT( "columnsFor: requested table %s has no columns mapped!" ),
  239. aTable );
  240. return "*";
  241. }
  242. std::string ret;
  243. for( const auto& [ columnName, columnType ] : m_columnCache[aTable] )
  244. ret += fmt::format( "{}{}{}, ", m_quoteChar, columnName, m_quoteChar );
  245. // strip tailing ', '
  246. ret.resize( ret.length() - 2 );
  247. return ret;
  248. }
  249. //next step, make SelectOne take from the SelectAll cache if the SelectOne cache is missing.
  250. //To do this, need to build a map of PK->ROW for the cache result.
  251. bool DATABASE_CONNECTION::SelectOne( const std::string& aTable,
  252. const std::pair<std::string, std::string>& aWhere,
  253. DATABASE_CONNECTION::ROW& aResult )
  254. {
  255. if( !m_conn )
  256. {
  257. wxLogTrace( traceDatabase, wxT( "Called SelectOne without valid connection!" ) );
  258. return false;
  259. }
  260. auto tableMapIter = m_tables.find( aTable );
  261. if( tableMapIter == m_tables.end() )
  262. {
  263. wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s not found in cache" ),
  264. aTable );
  265. return false;
  266. }
  267. const std::string& tableName = tableMapIter->first;
  268. DB_CACHE_TYPE::CACHE_VALUE cacheEntry;
  269. if( m_cache->Get( tableName, cacheEntry ) )
  270. {
  271. if( cacheEntry.count( aWhere.second ) )
  272. {
  273. wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s` - cache hit" ),
  274. tableName, aWhere.second );
  275. aResult = cacheEntry.at( aWhere.second );
  276. return true;
  277. }
  278. }
  279. else
  280. {
  281. wxLogTrace( traceDatabase, wxT( "SelectOne: table `%s` not in row cache; will SelectAll" ),
  282. tableName, aWhere.second );
  283. selectAllAndCache( tableName, aWhere.first );
  284. if( m_cache->Get( tableName, cacheEntry ) )
  285. {
  286. if( cacheEntry.count( aWhere.second ) )
  287. {
  288. wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s` - cache hit" ),
  289. tableName, aWhere.second );
  290. aResult = cacheEntry.at( aWhere.second );
  291. return true;
  292. }
  293. }
  294. }
  295. if( !m_columnCache.count( tableName ) )
  296. {
  297. wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s missing from column cache" ),
  298. tableName );
  299. return false;
  300. }
  301. auto columnCacheIter = m_columnCache.at( tableName ).find( aWhere.first );
  302. if( columnCacheIter == m_columnCache.at( tableName ).end() )
  303. {
  304. wxLogTrace( traceDatabase, wxT( "SelectOne: requested column %s not found in cache for %s" ),
  305. aWhere.first, tableName );
  306. return false;
  307. }
  308. const std::string& columnName = columnCacheIter->first;
  309. std::string cacheKey = fmt::format( "{}{}{}", tableName, columnName, aWhere.second );
  310. std::string queryStr = fmt::format( "SELECT {} FROM {}{}{} WHERE {}{}{} = ?",
  311. columnsFor( tableName ),
  312. m_quoteChar, tableName, m_quoteChar,
  313. m_quoteChar, columnName, m_quoteChar );
  314. nanodbc::string query = fromUTF8( queryStr );
  315. PROF_TIMER timer;
  316. nanodbc::statement statement;
  317. try
  318. {
  319. statement.prepare( *m_conn, query );
  320. statement.bind( 0, aWhere.second.c_str() );
  321. }
  322. catch( nanodbc::database_error& e )
  323. {
  324. m_lastError = e.what();
  325. wxLogTrace( traceDatabase, wxT( "Exception while preparing statement for SelectOne: %s" ),
  326. m_lastError );
  327. // Exception may be due to a connection error; nanodbc won't auto-reconnect
  328. m_conn->disconnect();
  329. return false;
  330. }
  331. wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s`" ), toUTF8( query ),
  332. aWhere.second );
  333. nanodbc::result results;
  334. try
  335. {
  336. results = nanodbc::execute( statement );
  337. }
  338. catch( nanodbc::database_error& e )
  339. {
  340. m_lastError = e.what();
  341. wxLogTrace( traceDatabase, wxT( "Exception while executing statement for SelectOne: %s" ),
  342. m_lastError );
  343. // Exception may be due to a connection error; nanodbc won't auto-reconnect
  344. m_conn->disconnect();
  345. return false;
  346. }
  347. timer.Stop();
  348. if( !results.first() )
  349. {
  350. wxLogTrace( traceDatabase, wxT( "SelectOne: no results returned from query" ) );
  351. return false;
  352. }
  353. wxLogTrace( traceDatabase, wxT( "SelectOne: %ld results returned from query in %0.1f ms" ),
  354. results.rows(), timer.msecs() );
  355. aResult.clear();
  356. try
  357. {
  358. for( short i = 0; i < results.columns(); ++i )
  359. {
  360. std::string column = toUTF8( results.column_name( i ) );
  361. switch( results.column_datatype( i ) )
  362. {
  363. case SQL_DOUBLE:
  364. case SQL_FLOAT:
  365. case SQL_REAL:
  366. case SQL_DECIMAL:
  367. case SQL_NUMERIC:
  368. {
  369. try
  370. {
  371. aResult[column] = fmt::format( "{:G}", results.get<double>( i ) );
  372. }
  373. catch( nanodbc::null_access_error& )
  374. {
  375. // Column was empty (null)
  376. aResult[column] = std::string();
  377. }
  378. break;
  379. }
  380. default:
  381. aResult[column] = toUTF8( results.get<nanodbc::string>( i, NANODBC_TEXT( "" ) ) );
  382. }
  383. }
  384. }
  385. catch( nanodbc::database_error& e )
  386. {
  387. m_lastError = e.what();
  388. wxLogTrace( traceDatabase, wxT( "Exception while parsing results from SelectOne: %s" ),
  389. m_lastError );
  390. return false;
  391. }
  392. return true;
  393. }
  394. bool DATABASE_CONNECTION::selectAllAndCache( const std::string& aTable, const std::string& aKey )
  395. {
  396. nanodbc::statement statement( *m_conn );
  397. nanodbc::string query = fromUTF8( fmt::format( "SELECT {} FROM {}{}{}", columnsFor( aTable ),
  398. m_quoteChar, aTable, m_quoteChar ) );
  399. wxLogTrace( traceDatabase, wxT( "selectAllAndCache: `%s`" ), toUTF8( query ) );
  400. PROF_TIMER timer;
  401. try
  402. {
  403. statement.prepare( query );
  404. }
  405. catch( nanodbc::database_error& e )
  406. {
  407. m_lastError = e.what();
  408. wxLogTrace( traceDatabase,
  409. wxT( "Exception while preparing query for selectAllAndCache: %s" ),
  410. m_lastError );
  411. // Exception may be due to a connection error; nanodbc won't auto-reconnect
  412. m_conn->disconnect();
  413. return false;
  414. }
  415. nanodbc::result results;
  416. try
  417. {
  418. results = nanodbc::execute( statement );
  419. }
  420. catch( nanodbc::database_error& e )
  421. {
  422. m_lastError = e.what();
  423. wxLogTrace( traceDatabase,
  424. wxT( "Exception while executing query for selectAllAndCache: %s" ),
  425. m_lastError );
  426. // Exception may be due to a connection error; nanodbc won't auto-reconnect
  427. m_conn->disconnect();
  428. return false;
  429. }
  430. timer.Stop();
  431. DB_CACHE_TYPE::CACHE_VALUE cacheEntry;
  432. auto handleException =
  433. [&]( std::runtime_error& aException, const std::string& aExtraContext = "" )
  434. {
  435. m_lastError = aException.what();
  436. std::string extra = aExtraContext.empty() ? "" : ": " + aExtraContext;
  437. wxLogTrace( traceDatabase,
  438. wxT( "Exception while parsing result %d from selectAllAndCache: %s%s" ),
  439. cacheEntry.size(), m_lastError, extra );
  440. };
  441. while( results.next() )
  442. {
  443. short columnCount = 0;
  444. ROW result;
  445. try
  446. {
  447. columnCount = results.columns();
  448. }
  449. catch( nanodbc::database_error& e )
  450. {
  451. handleException( e );
  452. return false;
  453. }
  454. for( short j = 0; j < columnCount; ++j )
  455. {
  456. std::string column;
  457. std::string columnExtraDbgInfo;
  458. int datatype = SQL_UNKNOWN_TYPE;
  459. try
  460. {
  461. column = toUTF8( results.column_name( j ) );
  462. datatype = results.column_datatype( j );
  463. columnExtraDbgInfo = fmt::format( "column index {}, name '{}', type {}", j, column,
  464. datatype );
  465. }
  466. catch( nanodbc::index_range_error& e )
  467. {
  468. handleException( e, columnExtraDbgInfo );
  469. return false;
  470. }
  471. switch( datatype )
  472. {
  473. case SQL_DOUBLE:
  474. case SQL_FLOAT:
  475. case SQL_REAL:
  476. case SQL_DECIMAL:
  477. case SQL_NUMERIC:
  478. {
  479. try
  480. {
  481. result[column] = fmt::format( "{:G}", results.get<double>( j ) );
  482. }
  483. catch( nanodbc::null_access_error& )
  484. {
  485. // Column was empty (null)
  486. result[column] = std::string();
  487. }
  488. catch( std::runtime_error& e )
  489. {
  490. handleException( e, columnExtraDbgInfo );
  491. return false;
  492. }
  493. break;
  494. }
  495. default:
  496. {
  497. try
  498. {
  499. result[column] = toUTF8( results.get<nanodbc::string>( j,
  500. NANODBC_TEXT( "" ) ) );
  501. }
  502. catch( std::runtime_error& e )
  503. {
  504. handleException( e, columnExtraDbgInfo );
  505. return false;
  506. }
  507. }
  508. }
  509. }
  510. if( !result.count( aKey ) )
  511. {
  512. wxLogTrace( traceDatabase,
  513. wxT( "selectAllAndCache: warning: key %s not found in result set" ), aKey );
  514. continue;
  515. }
  516. std::string keyStr = std::any_cast<std::string>( result.at( aKey ) );
  517. cacheEntry[keyStr] = result;
  518. }
  519. wxLogTrace( traceDatabase, wxT( "selectAllAndCache from %s completed in %0.1f ms" ), aTable,
  520. timer.msecs() );
  521. m_cache->Put( aTable, cacheEntry );
  522. return true;
  523. }
  524. bool DATABASE_CONNECTION::SelectAll( const std::string& aTable, const std::string& aKey,
  525. std::vector<ROW>& aResults )
  526. {
  527. if( !m_conn )
  528. {
  529. wxLogTrace( traceDatabase, wxT( "Called SelectAll without valid connection!" ) );
  530. return false;
  531. }
  532. auto tableMapIter = m_tables.find( aTable );
  533. if( tableMapIter == m_tables.end() )
  534. {
  535. wxLogTrace( traceDatabase, wxT( "SelectAll: requested table %s not found in cache" ),
  536. aTable );
  537. return false;
  538. }
  539. DB_CACHE_TYPE::CACHE_VALUE cacheEntry;
  540. if( !m_cache->Get( aTable, cacheEntry ) )
  541. {
  542. if( !selectAllAndCache( aTable, aKey ) )
  543. {
  544. wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` cache fill failed" ), aTable );
  545. return false;
  546. }
  547. // Now it should be filled
  548. m_cache->Get( aTable, cacheEntry );
  549. }
  550. if( !m_cache->Get( aTable, cacheEntry ) )
  551. {
  552. wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` failed to get results from cache!" ),
  553. aTable );
  554. return false;
  555. }
  556. wxLogTrace( traceDatabase, wxT( "SelectAll: `%s` - returning cached results" ), aTable );
  557. aResults.reserve( cacheEntry.size() );
  558. for( auto &[ key, row ] : cacheEntry )
  559. aResults.emplace_back( row );
  560. return true;
  561. }