23#include <boost/algorithm/string.hpp>
24#include <boost/locale.hpp>
25#include <fmt/format.h>
26#include <nanodbc/nanodbc.h>
31#define UINT64 uint64_t
68nanodbc::string
fromUTF8(
const std::string& aString )
70 return boost::locale::conv::utf_to_utf<nanodbc::string::value_type>( aString );
79std::string
toUTF8(
const nanodbc::string& aString )
81 return boost::locale::conv::utf_to_utf<char>( aString );
93static std::vector<RESULT_COLUMN>
resultColumns( nanodbc::result& aResults )
95 std::vector<RESULT_COLUMN> columns;
97 for(
short i = 0; i < aResults.columns(); ++i )
98 columns.push_back( { toUTF8( aResults.column_name( i ) ), aResults.column_datatype( i ) } );
108static void decodeRow( nanodbc::result& aResults,
const std::vector<RESULT_COLUMN>& aColumns,
111 for(
short i = 0; i < static_cast<short>( aColumns.size() ); ++i )
115 switch( column.
type )
124 aRow[column.
name] = fmt::format(
"{:G}", aResults.get<
double>( i ) );
126 catch( nanodbc::null_access_error& )
128 aRow[column.
name] = std::string();
134 aRow[column.
name] =
toUTF8( aResults.get<nanodbc::string>( i, NANODBC_TEXT(
"" ) ) );
141 const std::string& aUsername,
142 const std::string& aPassword,
int aTimeoutSeconds,
146 m_dsn = aDataSourceName;
159 int aTimeoutSeconds,
bool aConnectNow ) :
181 m_cache = std::make_unique<DB_CACHE_TYPE>( 10, 1 );
196 m_cache->SetMaxSize(
static_cast<size_t>( aMaxSize ) );
197 m_cache->SetMaxAge(
static_cast<time_t
>( aMaxAge ) );
213 m_conn = std::make_unique<nanodbc::connection>( dsn, user, pass,
m_timeout );
217 wxLogTrace(
traceDatabase, wxT(
"Creating connection with connection string" ) );
221 catch( nanodbc::database_error& e )
227 catch( std::exception& e )
244 catch( std::exception& e )
247 wxLogTrace(
traceDatabase, wxT(
"Pattern escape unavailable: %s" ), e.what() );
259 wxLogTrace(
traceDatabase, wxT(
"Note: Disconnect() called without valid connection" ) );
267 catch( std::exception& exc )
269 wxLogTrace(
traceDatabase, wxT(
"Disconnect() error \"%s\" occured." ), exc.what() );
273 return !
m_conn->connected();
282 return m_conn->connected();
287 const std::set<std::string>& aRequiredColumns,
288 const std::set<std::string>& aOptionalColumns )
296 std::set<std::string> requiredLower;
297 std::set<std::string> optionalLower;
299 for(
const std::string& col : aRequiredColumns )
300 requiredLower.insert( boost::to_lower_copy( col ) );
302 for(
const std::string& col : aOptionalColumns )
303 optionalLower.insert( boost::to_lower_copy( col ) );
307 nanodbc::catalog catalog( *
m_conn );
312 wxLogTrace(
traceDatabase, wxT(
"CacheTableInfo: table '%s' not found in catalog" ),
317 std::string key =
toUTF8( tables.table_name() );
322 nanodbc::catalog::columns columns =
325 std::set<std::string> columnsInCatalog;
327 while( columns.next() )
329 std::string columnKey =
toUTF8( columns.column_name() );
330 std::string columnKeyLower = boost::to_lower_copy( columnKey );
332 if( requiredLower.count( columnKeyLower )
333 || optionalLower.count( columnKeyLower ) )
336 columnsInCatalog.insert( columnKeyLower );
342 for(
const std::string& requestedCol : aRequiredColumns )
344 std::string requestedColLower = boost::to_lower_copy( requestedCol );
346 if( !columnsInCatalog.count( requestedColLower ) && !requestedCol.empty() )
349 wxT(
"CacheTableInfo: column '%s' not found in catalog for table "
350 "'%s', adding anyway" ),
359 for(
const std::string& requestedCol : aOptionalColumns )
361 std::string requestedColLower = boost::to_lower_copy( requestedCol );
363 if( !columnsInCatalog.count( requestedColLower ) && !requestedCol.empty() )
365 wxLogWarning( wxT(
"Database table '%s' has no column '%s'; ignoring the "
366 "misconfigured properties mapping." ),
371 catch( nanodbc::database_error& e )
374 wxLogTrace(
traceDatabase, wxT(
"Exception while syncing columns for table '%s': %s" ),
379 catch( std::exception& e )
397 nanodbc::string qc =
m_conn->get_info<nanodbc::string>( SQL_IDENTIFIER_QUOTE_CHAR );
406 catch( std::exception& e )
420 std::string escaped = aIdentifier;
422 boost::replace_all( escaped, quote, quote + quote );
424 return quote + escaped + quote;
436 for(
char c : aName )
452 wxLogTrace(
traceDatabase, wxT(
"columnsFor: requested table %s missing from cache!" ),
459 wxLogTrace(
traceDatabase, wxT(
"columnsFor: requested table %s has no columns mapped!" ),
466 for(
const auto& [ columnName, columnType ] :
m_columnCache[aTable] )
467 ret +=
quoted( columnName ) +
", ";
470 ret.resize( ret.length() - 2 );
478 const std::pair<std::string, std::string>& aWhere,
485 wxLogTrace(
traceDatabase, wxT(
"Called SelectOne without valid connection!" ) );
489 auto tableMapIter =
m_tables.find( aTable );
491 if( tableMapIter ==
m_tables.end() )
493 wxLogTrace(
traceDatabase, wxT(
"SelectOne: requested table %s not found in cache" ),
498 const std::string& tableName = tableMapIter->first;
501 if(
m_cache->Get( tableName, cacheEntry ) )
503 if( cacheEntry.count( aWhere.second ) )
505 wxLogTrace(
traceDatabase, wxT(
"SelectOne: `%s` with parameter `%s` - cache hit" ),
506 tableName, aWhere.second );
507 aResult = cacheEntry.at( aWhere.second );
513 wxLogTrace(
traceDatabase, wxT(
"SelectOne: table `%s` not in row cache; will SelectAll" ),
514 tableName, aWhere.second );
518 if(
m_cache->Get( tableName, cacheEntry ) )
520 if( cacheEntry.count( aWhere.second ) )
522 wxLogTrace(
traceDatabase, wxT(
"SelectOne: `%s` with parameter `%s` - cache hit" ),
523 tableName, aWhere.second );
524 aResult = cacheEntry.at( aWhere.second );
532 wxLogTrace(
traceDatabase, wxT(
"SelectOne: requested table %s missing from column cache" ),
537 auto columnCacheIter =
m_columnCache.at( tableName ).find( aWhere.first );
541 wxLogTrace(
traceDatabase, wxT(
"SelectOne: requested column %s not found in cache for %s" ),
542 aWhere.first, tableName );
546 const std::string& columnName = columnCacheIter->first;
548 std::string cacheKey = fmt::format(
"{}{}{}", tableName, columnName, aWhere.second );
550 std::string queryStr = fmt::format(
"SELECT {} FROM {} WHERE {} = ?",
columnsFor( tableName ),
552 nanodbc::string query =
fromUTF8( queryStr );
555 nanodbc::statement statement;
559 statement.prepare( *
m_conn, query );
561 catch( std::exception& e )
564 wxLogTrace(
traceDatabase, wxT(
"Exception while preparing statement for SelectOne: %s" ),
577 statement.describe_parameters( { 0 }, { SQL_VARCHAR }, { 255 }, { 0 } );
578 statement.bind( 0, aWhere.second.c_str() );
580 catch( std::exception& e )
583 wxLogTrace(
traceDatabase, wxT(
"Exception while binding parameter for SelectOne: %s" ),
594 nanodbc::result results;
598 results = nanodbc::execute( statement );
600 catch( std::exception& e )
603 wxLogTrace(
traceDatabase, wxT(
"Exception while executing statement for SelectOne: %s" ),
615 if( !results.first() )
617 wxLogTrace(
traceDatabase, wxT(
"SelectOne: no results returned from query" ) );
621 wxLogTrace(
traceDatabase, wxT(
"SelectOne: %ld results returned from query in %0.1f ms" ),
622 results.rows(), timer.
msecs() );
630 catch( std::exception& e )
633 wxLogTrace(
traceDatabase, wxT(
"Exception while parsing results from SelectOne: %s" ),
646 nanodbc::statement statement( *
m_conn );
648 nanodbc::string query =
fromUTF8( fmt::format(
"SELECT {} FROM {}",
columnsFor( aTable ),
655 statement.prepare( query );
657 catch( std::exception& e )
661 wxT(
"Exception while preparing query for selectAllAndCache: %s" ),
670 nanodbc::result results;
674 results = nanodbc::execute( statement );
676 catch( std::exception& e )
680 wxT(
"Exception while executing query for selectAllAndCache: %s" ),
692 std::vector<RESULT_COLUMN> columns =
resultColumns( results );
694 while( results.next() )
702 catch( std::runtime_error& e )
705 wxLogTrace(
traceDatabase, wxT(
"Exception while parsing result %d from selectAllAndCache: %s" ),
710 if( !
result.count( aKey ) )
713 wxT(
"selectAllAndCache: warning: key %s not found in result set" ), aKey );
717 std::string keyStr = std::any_cast<std::string>(
result.at( aKey ) );
718 cacheEntry[keyStr] =
result;
721 wxLogTrace(
traceDatabase, wxT(
"selectAllAndCache from %s completed in %0.1f ms" ), aTable,
724 m_cache->Put( aTable, cacheEntry );
727 catch( std::exception& e )
746 wxLogTrace(
traceDatabase, wxT(
"Called SelectAll without valid connection!" ) );
750 auto tableMapIter =
m_tables.find( aTable );
752 if( tableMapIter ==
m_tables.end() )
754 wxLogTrace(
traceDatabase, wxT(
"SelectAll: requested table %s not found in cache" ), aTable );
760 if( !
m_cache->Get( aTable, cacheEntry ) )
764 wxLogTrace(
traceDatabase, wxT(
"SelectAll: `%s` cache fill failed" ), aTable );
769 m_cache->Get( aTable, cacheEntry );
773 wxLogTrace(
traceDatabase, wxT(
"SelectAll: `%s` - returning cached results" ), aTable );
776 if( !
m_cache->Get( aTable, cacheEntry ) )
778 wxLogTrace(
traceDatabase, wxT(
"SelectAll: `%s` failed to get results from cache!" ), aTable );
782 aResults.reserve( cacheEntry.size() );
784 for(
auto &[ key, row ] : cacheEntry )
785 aResults.emplace_back( row );
792 std::vector<std::string>& aColumns, std::vector<ROW>& aRows )
804 std::string select = fmt::format(
"SELECT * FROM {}",
quoted( aTable ) );
805 nanodbc::result results;
814 catch(
const nanodbc::database_error& e )
817 wxLogTrace(
traceDatabase, wxT(
"SelectSample: ordered query on %s failed (%s); retrying unordered" ),
822 std::vector<RESULT_COLUMN> columns =
resultColumns( results );
825 aColumns.push_back( column.name );
827 while( aRows.size() < aMaxRows && results.next() )
831 aRows.emplace_back( std::move( row ) );
834 catch( std::exception& e )
856 nanodbc::catalog catalog( *
m_conn );
857 nanodbc::string current;
861 current =
m_conn->catalog_name();
863 catch(
const std::exception& )
869 [&](
const nanodbc::string& aCatalog )
871 nanodbc::catalog::tables tables = catalog.find_tables( NANODBC_TEXT(
"" ),
872 NANODBC_TEXT(
"TABLE,VIEW" ),
873 NANODBC_TEXT(
"" ), aCatalog );
875 while( tables.next() )
876 aTables.emplace_back(
toUTF8( tables.table_name() ) );
884 if( aTables.empty() && !current.empty() )
885 listTables( NANODBC_TEXT(
"" ) );
887 catch( std::exception& e )
894 std::sort( aTables.begin(), aTables.end() );
910 nanodbc::catalog catalog( *
m_conn );
911 nanodbc::catalog::columns columns = catalog.find_columns( NANODBC_TEXT(
"" ),
914 while( columns.next() )
915 aColumns.emplace_back(
toUTF8( columns.column_name() ) );
917 catch( std::exception& e )
930 std::vector<DATA_SOURCE> sources;
934 for(
const nanodbc::datasource&
source : nanodbc::list_datasources() )
937 catch( std::exception& e )
939 wxLogTrace(
traceDatabase, wxT(
"ListDataSourceDetails failed: %s" ), e.what() );
948 std::vector<std::string> names;
952 for(
const nanodbc::driver& driver : nanodbc::list_drivers() )
953 names.emplace_back(
toUTF8( driver.name ) );
955 catch( std::exception& e )
957 wxLogTrace(
traceDatabase, wxT(
"ListDrivers failed: %s" ), e.what() );
965 const std::string& aMessage )
967 std::string
message = boost::to_lower_copy( aMessage );
970 [&]( std::initializer_list<const char*> aPhrases )
972 for(
const char* phrase : aPhrases )
974 if(
message.find( phrase ) != std::string::npos )
983 if( aState.starts_with(
"28" )
984 || mentions( {
"password authentication failed",
"(using password:",
"login failed",
"authentication failed",
985 "no password supplied" } ) )
990 if( aState ==
"IM002" || mentions( {
"data source name not found" } ) )
993 if( aState ==
"IM003" || mentions( {
"can't open lib",
"could not be loaded" } ) )
996 if( aState.starts_with(
"HYT" ) || mentions( {
"timeout expired",
"timed out" } ) )
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" } ) )
std::map< std::string, ROW > CACHE_VALUE
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.
std::string m_connectionString
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::string m_patternEscape
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.
void Stop()
Save the time when this function was called, and set the counter stane to stop.
double msecs(bool aSinceLast=false)
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)
wxString result
Test unit parsing edge cases and error handling.