/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2022 Jon Evans * Copyright (C) 2022 KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include #include #include // Some outdated definitions are used in sql.h // We need to define them for "recent" dev tools #define INT64 int64_t #define UINT64 uint64_t #ifdef __MINGW32__ #define BYTE uint8_t #define WORD uint16_t #define DWORD uint32_t #define HWND uint32_t /* dummy define */ #endif #ifdef WIN32 #include // for sql.h #endif #include // SQL_IDENTIFIER_QUOTE_CHAR #include #include #include const char* const traceDatabase = "KICAD_DATABASE"; /** * When Unicode support is enabled in nanodbc, string formats are used matching the appropriate * character set of the platform. KiCad uses UTF-8 encoded strings internally, but different * platforms use different encodings for SQL strings. Unicode mode must be enabled for compilation * on Windows, since Visual Studio forces the use of Unicode SQL headers if any part of the project * has Unicode enabled. */ /** * Converts a string from KiCad-native to nanodbc-native * @param aString is a UTF-8 encoded string * @return a string in nanodbc's platform-specific representation */ nanodbc::string fromUTF8( const std::string& aString ) { return boost::locale::conv::utf_to_utf( aString ); } /** * Converts a string from nanodbc-native to KiCad-native * @param aString is a string encoded in nanodbc's platform-specific way * @return a string with UTF-8 encoding */ std::string toUTF8( const nanodbc::string& aString ) { return boost::locale::conv::utf_to_utf( aString ); } DATABASE_CONNECTION::DATABASE_CONNECTION( const std::string& aDataSourceName, const std::string& aUsername, const std::string& aPassword, int aTimeoutSeconds, bool aConnectNow ) : m_quoteChar( '"' ) { m_dsn = aDataSourceName; m_user = aUsername; m_pass = aPassword; m_timeout = aTimeoutSeconds; m_cache = std::make_unique( 10, 1 ); if( aConnectNow ) Connect(); } DATABASE_CONNECTION::DATABASE_CONNECTION( const std::string& aConnectionString, int aTimeoutSeconds, bool aConnectNow ) : m_quoteChar( '"' ) { m_connectionString = aConnectionString; m_timeout = aTimeoutSeconds; m_cache = std::make_unique( 10, 1 ); if( aConnectNow ) Connect(); } DATABASE_CONNECTION::~DATABASE_CONNECTION() { Disconnect(); m_conn.reset(); } void DATABASE_CONNECTION::SetCacheParams( int aMaxSize, int aMaxAge ) { if( !m_cache ) return; if( aMaxSize < 0 ) aMaxSize = 0; if( aMaxAge < 0 ) aMaxAge = 0; m_cache->SetMaxSize( static_cast( aMaxSize ) ); m_cache->SetMaxAge( static_cast( aMaxAge ) ); } bool DATABASE_CONNECTION::Connect() { nanodbc::string dsn = fromUTF8( m_dsn ); nanodbc::string user = fromUTF8( m_user ); nanodbc::string pass = fromUTF8( m_pass ); nanodbc::string cs = fromUTF8( m_connectionString ); try { if( cs.empty() ) { wxLogTrace( traceDatabase, wxT( "Creating connection to DSN %s" ), m_dsn ); m_conn = std::make_unique( dsn, user, pass, m_timeout ); } else { wxLogTrace( traceDatabase, wxT( "Creating connection with connection string" ) ); m_conn = std::make_unique( cs, m_timeout ); } } catch( nanodbc::database_error& e ) { m_lastError = e.what(); return false; } if( IsConnected() ) { syncTables(); cacheColumns(); getQuoteChar(); } return IsConnected(); } bool DATABASE_CONNECTION::Disconnect() { if( !m_conn ) { wxLogTrace( traceDatabase, wxT( "Note: Disconnect() called without valid connection" ) ); return false; } m_conn->disconnect(); return !m_conn->connected(); } bool DATABASE_CONNECTION::IsConnected() const { if( !m_conn ) return false; return m_conn->connected(); } bool DATABASE_CONNECTION::syncTables() { if( !m_conn ) return false; m_tables.clear(); try { nanodbc::catalog catalog( *m_conn ); nanodbc::catalog::tables tables = catalog.find_tables(); while( tables.next() ) { std::string key = toUTF8( tables.table_name() ); m_tables[key] = toUTF8( tables.table_type() ); } } catch( nanodbc::database_error& e ) { m_lastError = e.what(); wxLogTrace( traceDatabase, wxT( "Exception while syncing tables: %s" ), m_lastError ); return false; } return true; } bool DATABASE_CONNECTION::cacheColumns() { if( !m_conn ) return false; m_columnCache.clear(); for( const auto& tableIter : m_tables ) { try { nanodbc::catalog catalog( *m_conn ); nanodbc::catalog::columns columns = catalog.find_columns( NANODBC_TEXT( "" ), fromUTF8( tableIter.first ) ); while( columns.next() ) { std::string columnKey = toUTF8( columns.column_name() ); m_columnCache[tableIter.first][columnKey] = columns.data_type(); } } catch( nanodbc::database_error& e ) { m_lastError = e.what(); wxLogTrace( traceDatabase, wxT( "Exception while syncing columns for table %s: %s" ), tableIter.first, m_lastError ); return false; } } return true; } bool DATABASE_CONNECTION::getQuoteChar() { if( !m_conn ) return false; try { nanodbc::string qc = m_conn->get_info( SQL_IDENTIFIER_QUOTE_CHAR ); if( qc.empty() ) return false; m_quoteChar = *toUTF8( qc ).begin(); wxLogTrace( traceDatabase, wxT( "Quote char retrieved: %c" ), m_quoteChar ); } catch( nanodbc::database_error& e ) { wxLogTrace( traceDatabase, wxT( "Exception while querying quote char: %s" ), m_lastError ); return false; } return true; } bool DATABASE_CONNECTION::SelectOne( const std::string& aTable, const std::pair& aWhere, DATABASE_CONNECTION::ROW& aResult ) { if( !m_conn ) { wxLogTrace( traceDatabase, wxT( "Called SelectOne without valid connection!" ) ); return false; } auto tableMapIter = m_tables.find( aTable ); if( tableMapIter == m_tables.end() ) { wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s not found in cache" ), aTable ); return false; } const std::string& tableName = tableMapIter->first; if( !m_columnCache.count( tableName ) ) { wxLogTrace( traceDatabase, wxT( "SelectOne: requested table %s missing from column cache" ), tableName ); return false; } auto columnCacheIter = m_columnCache.at( tableName ).find( aWhere.first ); if( columnCacheIter == m_columnCache.at( tableName ).end() ) { wxLogTrace( traceDatabase, wxT( "SelectOne: requested column %s not found in cache for %s" ), aWhere.first, tableName ); return false; } const std::string& columnName = columnCacheIter->first; std::string queryStr = fmt::format( "SELECT * FROM {}{}{} WHERE {}{}{} = ?", m_quoteChar, tableName, m_quoteChar, m_quoteChar, columnName, m_quoteChar ); nanodbc::statement statement( *m_conn ); nanodbc::string query = fromUTF8( queryStr ); if( m_cache->Get( queryStr, aResult ) ) { wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s` - cache hit" ), toUTF8( query ), aWhere.second ); return true; } try { statement.prepare( query ); statement.bind( 0, aWhere.second.c_str() ); } catch( nanodbc::database_error& e ) { m_lastError = e.what(); wxLogTrace( traceDatabase, wxT( "Exception while preparing statement for SelectOne: %s" ), m_lastError ); return false; } wxLogTrace( traceDatabase, wxT( "SelectOne: `%s` with parameter `%s`" ), toUTF8( query ), aWhere.second ); nanodbc::result results; try { results = nanodbc::execute( statement ); } catch( nanodbc::database_error& e ) { m_lastError = e.what(); wxLogTrace( traceDatabase, wxT( "Exception while executing statement for SelectOne: %s" ), m_lastError ); return false; } if( !results.first() ) { wxLogTrace( traceDatabase, wxT( "SelectOne: no results returned from query" ) ); return false; } wxLogTrace( traceDatabase, wxT( "SelectOne: %ld results returned from query" ), results.rows() ); aResult.clear(); try { for( short i = 0; i < results.columns(); ++i ) { std::string column = toUTF8( results.column_name( i ) ); aResult[ column ] = toUTF8( results.get( i, NANODBC_TEXT( "" ) ) ); } } catch( nanodbc::database_error& e ) { m_lastError = e.what(); wxLogTrace( traceDatabase, wxT( "Exception while parsing results from SelectOne: %s" ), m_lastError ); return false; } m_cache->Put( queryStr, aResult ); return true; } bool DATABASE_CONNECTION::SelectAll( const std::string& aTable, std::vector& aResults ) { if( !m_conn ) { wxLogTrace( traceDatabase, wxT( "Called SelectAll without valid connection!" ) ); return false; } auto tableMapIter = m_tables.find( aTable ); if( tableMapIter == m_tables.end() ) { wxLogTrace( traceDatabase, wxT( "SelectAll: requested table %s not found in cache" ), aTable ); return false; } nanodbc::statement statement( *m_conn ); nanodbc::string query = fromUTF8( fmt::format( "SELECT * FROM {}{}{}", m_quoteChar, tableMapIter->first, m_quoteChar ) ); wxLogTrace( traceDatabase, wxT( "SelectAll: `%s`" ), toUTF8( query ) ); try { statement.prepare( query ); } catch( nanodbc::database_error& e ) { m_lastError = e.what(); wxLogTrace( traceDatabase, wxT( "Exception while preparing query for SelectAll: %s" ), m_lastError ); return false; } nanodbc::result results; try { results = nanodbc::execute( statement ); } catch( nanodbc::database_error& e ) { m_lastError = e.what(); wxLogTrace( traceDatabase, wxT( "Exception while executing query for SelectAll: %s" ), m_lastError ); return false; } if( !results.first() ) return false; try { do { ROW result; for( short j = 0; j < results.columns(); ++j ) { std::string column = toUTF8( results.column_name( j ) ); result[column] = toUTF8( results.get( j, NANODBC_TEXT( "" ) ) ); } aResults.emplace_back( std::move( result ) ); } while( results.next() ); } catch( nanodbc::database_error& e ) { m_lastError = e.what(); wxLogTrace( traceDatabase, wxT( "Exception while parsing results from SelectAll: %s" ), m_lastError ); return false; } return true; }