2018-08-19 17:09:54 +00:00
# include "ColumnInfoHandler.h"
2018-09-14 19:48:51 +00:00
2020-05-08 14:11:19 +00:00
# if USE_ODBC
2021-03-22 11:40:29 +00:00
# include <DataTypes/DataTypeFactory.h>
# include <DataTypes/DataTypeNullable.h>
# include <Server/HTTP/WriteBufferFromHTTPServerResponse.h>
2022-08-10 07:39:32 +00:00
# include <IO/ReadHelpers.h>
2021-03-22 11:40:29 +00:00
# include <IO/WriteHelpers.h>
# include <Parsers/ParserQueryWithOutput.h>
# include <Server/HTTP/HTMLForm.h>
# include <Poco/Net/HTTPServerRequest.h>
# include <Poco/Net/HTTPServerResponse.h>
# include <Poco/NumberParser.h>
2022-04-27 15:05:45 +00:00
# include <Common/logger_useful.h>
2022-08-08 14:40:19 +00:00
# include <Common/BridgeProtocolVersion.h>
2021-03-22 11:40:29 +00:00
# include <Common/quoteString.h>
# include "getIdentifierQuote.h"
# include "validateODBCConnectionString.h"
2022-06-01 09:00:39 +00:00
# include "ODBCPooledConnectionFactory.h"
2021-03-22 11:40:29 +00:00
# include <sql.h>
# include <sqlext.h>
2018-08-19 17:09:54 +00:00
namespace DB
{
2021-03-22 11:40:29 +00:00
namespace ErrorCodes
{
2023-03-14 13:24:11 +00:00
extern const int UNKNOWN_TABLE ;
2021-03-24 12:27:46 +00:00
extern const int BAD_ARGUMENTS ;
2021-03-22 11:40:29 +00:00
}
2018-08-19 17:09:54 +00:00
namespace
{
DataTypePtr getDataType ( SQLSMALLINT type )
{
const auto & factory = DataTypeFactory : : instance ( ) ;
switch ( type )
{
2019-10-25 17:49:49 +00:00
case SQL_TINYINT :
return factory . get ( " Int8 " ) ;
2018-08-19 17:09:54 +00:00
case SQL_INTEGER :
return factory . get ( " Int32 " ) ;
case SQL_SMALLINT :
return factory . get ( " Int16 " ) ;
2019-10-25 17:49:49 +00:00
case SQL_BIGINT :
return factory . get ( " Int64 " ) ;
2018-08-19 17:09:54 +00:00
case SQL_FLOAT :
2019-10-25 17:49:49 +00:00
return factory . get ( " Float64 " ) ;
2018-08-19 17:09:54 +00:00
case SQL_REAL :
return factory . get ( " Float32 " ) ;
case SQL_DOUBLE :
return factory . get ( " Float64 " ) ;
case SQL_DATETIME :
return factory . get ( " DateTime " ) ;
case SQL_TYPE_TIMESTAMP :
return factory . get ( " DateTime " ) ;
case SQL_TYPE_DATE :
return factory . get ( " Date " ) ;
default :
return factory . get ( " String " ) ;
}
}
}
2021-03-22 11:40:29 +00:00
2024-01-03 16:47:15 +00:00
void ODBCColumnsInfoHandler : : handleRequest ( HTTPServerRequest & request , HTTPServerResponse & response , const ProfileEvents : : Event & /*write_event*/ )
2018-08-19 17:09:54 +00:00
{
2021-06-16 14:33:14 +00:00
HTMLForm params ( getContext ( ) - > getSettingsRef ( ) , request , request . getStream ( ) ) ;
2020-05-23 22:24:01 +00:00
LOG_TRACE ( log , " Request URI: {} " , request . getURI ( ) ) ;
2018-08-19 17:09:54 +00:00
2018-08-24 00:07:25 +00:00
auto process_error = [ & response , this ] ( const std : : string & message )
{
2018-08-19 17:09:54 +00:00
response . setStatusAndReason ( Poco : : Net : : HTTPResponse : : HTTP_INTERNAL_SERVER_ERROR ) ;
if ( ! response . sent ( ) )
2024-01-03 16:47:15 +00:00
* response . send ( ) < < message < < ' \n ' ;
Use fmt::runtime() for LOG_* for non constexpr
Here is oneliner:
$ gg 'LOG_\(DEBUG\|TRACE\|INFO\|TEST\|WARNING\|ERROR\|FATAL\)([^,]*, [a-zA-Z]' -- :*.cpp :*.h | cut -d: -f1 | sort -u | xargs -r sed -E -i 's#(LOG_[A-Z]*)\(([^,]*), ([A-Za-z][^,)]*)#\1(\2, fmt::runtime(\3)#'
Note, that I tried to do this with coccinelle (tool for semantic
patchin), but it cannot parse C++:
$ cat fmt.cocci
@@
expression log;
expression var;
@@
-LOG_DEBUG(log, var)
+LOG_DEBUG(log, fmt::runtime(var))
I've also tried to use some macros/templates magic to do this implicitly
in logger_useful.h, but I failed to do so, and apparently it is not
possible for now.
Signed-off-by: Azat Khuzhin <a.khuzhin@semrush.com>
v2: manual fixes
Signed-off-by: Azat Khuzhin <a.khuzhin@semrush.com>
2022-02-01 09:10:27 +00:00
LOG_WARNING ( log , fmt : : runtime ( message ) ) ;
2018-08-19 17:09:54 +00:00
} ;
2022-08-10 07:39:32 +00:00
size_t version ;
2022-08-08 14:40:19 +00:00
if ( ! params . has ( " version " ) )
2022-08-10 07:39:32 +00:00
version = 0 ; /// assumed version for too old servers which do not send a version
2022-08-08 14:40:19 +00:00
else
{
String version_str = params . get ( " version " ) ;
2022-08-10 07:39:32 +00:00
if ( ! tryParse ( version , version_str ) )
2022-08-08 14:40:19 +00:00
{
process_error ( " Unable to parse 'version' string in request URL: ' " + version_str + " ' Check if the server and library-bridge have the same version. " ) ;
return ;
}
2022-08-10 07:39:32 +00:00
}
if ( version ! = XDBC_BRIDGE_PROTOCOL_VERSION )
{
/// backwards compatibility is considered unnecessary for now, just let the user know that the server and the bridge must be upgraded together
process_error ( " Server and library-bridge have different versions: ' " + std : : to_string ( version ) + " ' vs. ' " + std : : to_string ( LIBRARY_BRIDGE_PROTOCOL_VERSION ) + " ' " ) ;
return ;
2022-08-08 14:40:19 +00:00
}
2018-08-19 17:09:54 +00:00
if ( ! params . has ( " table " ) )
{
process_error ( " No 'table' param in request URL " ) ;
return ;
}
2021-03-24 12:27:46 +00:00
2018-08-19 17:09:54 +00:00
if ( ! params . has ( " connection_string " ) )
{
process_error ( " No 'connection_string' in request URL " ) ;
return ;
}
2021-03-24 12:27:46 +00:00
2020-03-09 03:38:43 +00:00
std : : string schema_name ;
2018-08-19 17:09:54 +00:00
std : : string table_name = params . get ( " table " ) ;
std : : string connection_string = params . get ( " connection_string " ) ;
2019-10-28 11:01:09 +00:00
2018-08-17 09:07:39 +00:00
if ( params . has ( " schema " ) )
schema_name = params . get ( " schema " ) ;
2021-03-22 11:40:29 +00:00
2020-05-23 22:24:01 +00:00
LOG_TRACE ( log , " Got connection str '{}' " , connection_string ) ;
2018-08-19 17:09:54 +00:00
try
{
2019-11-07 06:08:59 +00:00
const bool external_table_functions_use_nulls = Poco : : NumberParser : : parseBool ( params . get ( " external_table_functions_use_nulls " , " false " ) ) ;
2022-06-01 09:00:39 +00:00
auto connection_holder = ODBCPooledConnectionFactory : : instance ( ) . get (
2021-04-06 18:59:34 +00:00
validateODBCConnectionString ( connection_string ) ,
2021-04-11 06:13:11 +00:00
getContext ( ) - > getSettingsRef ( ) . odbc_bridge_connection_pool_size ) ;
2021-04-06 18:59:34 +00:00
2021-03-24 12:27:46 +00:00
/// In XDBC tables it is allowed to pass either database_name or schema_name in table definion, but not both of them.
/// They both are passed as 'schema' parameter in request URL, so it is not clear whether it is database_name or schema_name passed.
2021-03-24 18:23:12 +00:00
/// If it is schema_name then we know that database is added in odbc.ini. But if we have database_name as 'schema',
/// it is not guaranteed. For nanodbc database_name must be either in odbc.ini or passed as catalog_name.
2021-06-07 18:09:16 +00:00
auto get_columns = [ & ] ( nanodbc : : connection & connection )
2021-03-24 12:27:46 +00:00
{
2021-06-07 18:09:16 +00:00
nanodbc : : catalog catalog ( connection ) ;
std : : string catalog_name ;
2021-04-12 22:53:00 +00:00
nanodbc : : catalog : : tables tables = catalog . find_tables ( table_name , /* type = */ " " , /* schema = */ " " , /* catalog = */ schema_name ) ;
2021-03-24 12:27:46 +00:00
if ( tables . next ( ) )
{
catalog_name = tables . table_catalog ( ) ;
2023-10-13 10:50:36 +00:00
/// `tables.next()` call is mandatory to drain the iterator before next operation and avoid "Invalid cursor state"
if ( tables . next ( ) )
throw Exception ( ErrorCodes : : UNKNOWN_TABLE , " Driver returned more than one table for '{}': '{}' and '{}' " ,
table_name , catalog_name , tables . table_schema ( ) ) ;
2021-04-12 22:55:14 +00:00
LOG_TRACE ( log , " Will fetch info for table '{}.{}' " , catalog_name , table_name ) ;
2021-03-24 18:23:12 +00:00
return catalog . find_columns ( /* column = */ " " , table_name , /* schema = */ " " , catalog_name ) ;
2021-03-24 12:27:46 +00:00
}
2021-04-12 22:53:00 +00:00
tables = catalog . find_tables ( table_name , /* type = */ " " , /* schema = */ schema_name ) ;
2021-03-24 12:27:46 +00:00
if ( tables . next ( ) )
{
catalog_name = tables . table_catalog ( ) ;
2023-10-13 10:50:36 +00:00
/// `tables.next()` call is mandatory to drain the iterator before next operation and avoid "Invalid cursor state"
if ( tables . next ( ) )
throw Exception ( ErrorCodes : : UNKNOWN_TABLE , " Driver returned more than one table for '{}': '{}' and '{}' " ,
table_name , catalog_name , tables . table_schema ( ) ) ;
2021-04-12 22:55:14 +00:00
LOG_TRACE ( log , " Will fetch info for table '{}.{}.{}' " , catalog_name , schema_name , table_name ) ;
2021-03-24 12:27:46 +00:00
return catalog . find_columns ( /* column = */ " " , table_name , schema_name , catalog_name ) ;
}
throw Exception ( ErrorCodes : : BAD_ARGUMENTS , " Table {} not found " , schema_name . empty ( ) ? table_name : schema_name + ' . ' + table_name ) ;
} ;
2021-06-07 18:09:16 +00:00
nanodbc : : catalog : : columns columns_definition = execute < nanodbc : : catalog : : columns > (
std : : move ( connection_holder ) ,
[ & ] ( nanodbc : : connection & connection ) { return get_columns ( connection ) ; } ) ;
2018-08-19 17:09:54 +00:00
NamesAndTypesList columns ;
2021-03-22 11:40:29 +00:00
while ( columns_definition . next ( ) )
2018-08-19 17:09:54 +00:00
{
2021-03-22 11:40:29 +00:00
SQLSMALLINT type = columns_definition . sql_data_type ( ) ;
std : : string column_name = columns_definition . column_name ( ) ;
2019-10-21 09:13:33 +00:00
2021-03-22 11:40:29 +00:00
bool is_nullable = columns_definition . nullable ( ) = = SQL_NULLABLE ;
2019-10-21 09:13:33 +00:00
auto column_type = getDataType ( type ) ;
2021-03-22 11:40:29 +00:00
2019-10-28 11:01:09 +00:00
if ( external_table_functions_use_nulls & & is_nullable = = SQL_NULLABLE )
2019-10-21 09:13:33 +00:00
column_type = std : : make_shared < DataTypeNullable > ( column_type ) ;
2021-03-22 11:40:29 +00:00
columns . emplace_back ( column_name , std : : move ( column_type ) ) ;
2018-08-19 17:09:54 +00:00
}
2023-03-14 13:24:11 +00:00
/// Usually this should not happen, since in case of table does not
/// exists, the call should be succeeded.
/// However it is possible sometimes because internally there are two
/// queries in ClickHouse ODBC bridge:
/// - system.tables
/// - system.columns
/// And if between this two queries the table will be removed, them
/// there will be no columns
///
/// Also sometimes system.columns can return empty result because of
/// the cached value of total tables to scan.
2021-03-22 11:40:29 +00:00
if ( columns . empty ( ) )
2023-03-14 13:24:11 +00:00
throw Exception ( ErrorCodes : : UNKNOWN_TABLE , " Columns definition was not returned " ) ;
2021-03-22 11:40:29 +00:00
2022-10-07 10:46:45 +00:00
WriteBufferFromHTTPServerResponse out (
response ,
request . getMethod ( ) = = Poco : : Net : : HTTPRequest : : HTTP_HEAD ,
2022-10-21 19:16:13 +00:00
keep_alive_timeout ) ;
2021-02-20 05:31:05 +00:00
try
{
writeStringBinary ( columns . toString ( ) , out ) ;
out . finalize ( ) ;
}
catch ( . . . )
{
out . finalize ( ) ;
}
2018-08-19 17:09:54 +00:00
}
catch ( . . . )
{
process_error ( " Error getting columns from ODBC ' " + getCurrentExceptionMessage ( false ) + " ' " ) ;
tryLogCurrentException ( log ) ;
}
}
2020-05-08 14:11:19 +00:00
2018-08-19 17:09:54 +00:00
}
2020-05-08 14:11:19 +00:00
2018-08-19 17:09:54 +00:00
# endif