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.

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