From 23f7e4d45d3973852241d50dd2029ae5ecfb953c Mon Sep 17 00:00:00 2001 From: Bao Ngo <78314961+kaizenjinco@users.noreply.github.com> Date: Sun, 2 Nov 2025 02:37:52 +0700 Subject: [PATCH 1/3] Add SSL verification control for ClickHouse handler (#11367) Co-authored-by: Bao Ngo Co-authored-by: entelligence-ai-pr-reviews[bot] <174136889+entelligence-ai-pr-reviews[bot]@users.noreply.github.com> --- .../clickhouse_handler/clickhouse_handler.py | 14 ++++++++++++++ .../handlers/clickhouse_handler/connection_args.py | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py b/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py index e3fd83780ea..1dd6a06eea9 100644 --- a/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py +++ b/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py @@ -56,10 +56,24 @@ def connect(self): user = quote(self.connection_data['user']) password = quote(self.connection_data['password']) database = quote(self.connection_data['database']) + verify = self.connection_data.get('verify', False) url = f'{protocol}://{user}:{password}@{host}:{port}/{database}' # This is not redundunt. Check https://clickhouse-sqlalchemy.readthedocs.io/en/latest/connection.html#http if self.protocol == 'https': url = url + "?protocol=https" + # Add SSL verification control + if verify == False: + if "?" in url: + url = url + "&verify=false" + else: + url = url + "?verify=false" + if verify == True: + if "?" in url: + url = url + "&verify=true" + else: + url = url + "?verify=true" + logger.debug(f'Connecting to ClickHouse with URL: {url}') + try: engine = create_engine(url) connection = engine.raw_connection() diff --git a/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py b/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py index 8d64f4d0fd4..7c62c2a4b8b 100644 --- a/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py +++ b/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py @@ -40,6 +40,12 @@ 'required': True, 'label': 'Password', 'secret': True + }, + verify={ + 'type': ARG_TYPE.BOOL, + 'description': 'Controls certificate verification in https protocol. Possible choices: true/false. Default is true.', + 'required': True, + 'label': 'SSL Verification', } ) @@ -49,5 +55,6 @@ port=9000, user='root', password='password', - database='database' + database='database', + verify=True ) From 60937bf1dbd8c5cb02979b498150e8832f819890 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 1 Nov 2025 22:46:46 +0300 Subject: [PATCH 2/3] fix composing url --- .../clickhouse_handler/clickhouse_handler.py | 26 +++++++------------ .../clickhouse_handler/connection_args.py | 2 +- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py b/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py index 1dd6a06eea9..a97f48b2be0 100644 --- a/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py +++ b/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py @@ -1,4 +1,4 @@ -from urllib.parse import quote +from urllib.parse import quote, urlencode import pandas as pd from sqlalchemy import create_engine @@ -56,24 +56,18 @@ def connect(self): user = quote(self.connection_data['user']) password = quote(self.connection_data['password']) database = quote(self.connection_data['database']) - verify = self.connection_data.get('verify', False) + verify = self.connection_data.get('verify', True) url = f'{protocol}://{user}:{password}@{host}:{port}/{database}' # This is not redundunt. Check https://clickhouse-sqlalchemy.readthedocs.io/en/latest/connection.html#http + + params = {} if self.protocol == 'https': - url = url + "?protocol=https" - # Add SSL verification control - if verify == False: - if "?" in url: - url = url + "&verify=false" - else: - url = url + "?verify=false" - if verify == True: - if "?" in url: - url = url + "&verify=true" - else: - url = url + "?verify=true" - logger.debug(f'Connecting to ClickHouse with URL: {url}') - + params['protocol'] = 'https' + if verify is False: + params['verify'] = 'false' + if params: + url = f'{url}?{urlencode(params)}' + try: engine = create_engine(url) connection = engine.raw_connection() diff --git a/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py b/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py index 7c62c2a4b8b..5678b11559e 100644 --- a/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py +++ b/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py @@ -44,7 +44,7 @@ verify={ 'type': ARG_TYPE.BOOL, 'description': 'Controls certificate verification in https protocol. Possible choices: true/false. Default is true.', - 'required': True, + 'required': False, 'label': 'SSL Verification', } ) From b6b284218efe072217abc64b2f71aacba7e3a2aa Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 1 Nov 2025 22:47:51 +0300 Subject: [PATCH 3/3] ruff --- .../clickhouse_handler/clickhouse_handler.py | 55 +++++++-------- .../clickhouse_handler/connection_args.py | 68 +++++++++---------- 2 files changed, 54 insertions(+), 69 deletions(-) diff --git a/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py b/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py index a97f48b2be0..feda48c1323 100644 --- a/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py +++ b/mindsdb/integrations/handlers/clickhouse_handler/clickhouse_handler.py @@ -12,7 +12,7 @@ from mindsdb.integrations.libs.response import ( HandlerStatusResponse as StatusResponse, HandlerResponse as Response, - RESPONSE_TYPE + RESPONSE_TYPE, ) logger = log.getLogger(__name__) @@ -23,15 +23,15 @@ class ClickHouseHandler(DatabaseHandler): This handler handles connection and execution of the ClickHouse statements. """ - name = 'clickhouse' + name = "clickhouse" def __init__(self, name, connection_data, **kwargs): super().__init__(name) - self.dialect = 'clickhouse' + self.dialect = "clickhouse" self.connection_data = connection_data self.renderer = SqlalchemyRender(ClickHouseDialect) self.is_connected = False - self.protocol = connection_data.get('protocol', 'native') + self.protocol = connection_data.get("protocol", "native") def __del__(self): if self.is_connected is True: @@ -50,23 +50,23 @@ def connect(self): if self.is_connected: return self.connection - protocol = "clickhouse+native" if self.protocol == 'native' else "clickhouse+http" - host = quote(self.connection_data['host']) - port = self.connection_data['port'] - user = quote(self.connection_data['user']) - password = quote(self.connection_data['password']) - database = quote(self.connection_data['database']) - verify = self.connection_data.get('verify', True) - url = f'{protocol}://{user}:{password}@{host}:{port}/{database}' + protocol = "clickhouse+native" if self.protocol == "native" else "clickhouse+http" + host = quote(self.connection_data["host"]) + port = self.connection_data["port"] + user = quote(self.connection_data["user"]) + password = quote(self.connection_data["password"]) + database = quote(self.connection_data["database"]) + verify = self.connection_data.get("verify", True) + url = f"{protocol}://{user}:{password}@{host}:{port}/{database}" # This is not redundunt. Check https://clickhouse-sqlalchemy.readthedocs.io/en/latest/connection.html#http params = {} - if self.protocol == 'https': - params['protocol'] = 'https' + if self.protocol == "https": + params["protocol"] = "https" if verify is False: - params['verify'] = 'false' + params["verify"] = "false" if params: - url = f'{url}?{urlencode(params)}' + url = f"{url}?{urlencode(params)}" try: engine = create_engine(url) @@ -74,7 +74,7 @@ def connect(self): self.is_connected = True self.connection = connection except SQLAlchemyError as e: - logger.error(f'Error connecting to ClickHouse {self.connection_data["database"]}, {e}!') + logger.error(f"Error connecting to ClickHouse {self.connection_data['database']}, {e}!") self.is_connected = False raise @@ -94,12 +94,12 @@ def check_connection(self) -> StatusResponse: connection = self.connect() cur = connection.cursor() try: - cur.execute('select 1;') + cur.execute("select 1;") finally: cur.close() response.success = True except SQLAlchemyError as e: - logger.error(f'Error connecting to ClickHouse {self.connection_data["database"]}, {e}!') + logger.error(f"Error connecting to ClickHouse {self.connection_data['database']}, {e}!") response.error_message = str(e) self.is_connected = False @@ -125,22 +125,13 @@ def native_query(self, query: str) -> Response: cur.execute(query) result = cur.fetchall() if result: - response = Response( - RESPONSE_TYPE.TABLE, - pd.DataFrame( - result, - columns=[x[0] for x in cur.description] - ) - ) + response = Response(RESPONSE_TYPE.TABLE, pd.DataFrame(result, columns=[x[0] for x in cur.description])) else: response = Response(RESPONSE_TYPE.OK) connection.commit() except SQLAlchemyError as e: - logger.error(f'Error running query: {query} on {self.connection_data["database"]}!') - response = Response( - RESPONSE_TYPE.ERROR, - error_message=str(e) - ) + logger.error(f"Error running query: {query} on {self.connection_data['database']}!") + response = Response(RESPONSE_TYPE.ERROR, error_message=str(e)) connection.rollback() finally: cur.close() @@ -163,7 +154,7 @@ def get_tables(self) -> Response: df = result.data_frame if df is not None: - result.data_frame = df.rename(columns={df.columns[0]: 'table_name'}) + result.data_frame = df.rename(columns={df.columns[0]: "table_name"}) return result diff --git a/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py b/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py index 5678b11559e..e53e9d82366 100644 --- a/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py +++ b/mindsdb/integrations/handlers/clickhouse_handler/connection_args.py @@ -5,56 +5,50 @@ connection_args = OrderedDict( protocol={ - 'type': ARG_TYPE.STR, - 'description': 'The protocol to query clickhouse. Supported: native, http, https. Default: native', - 'required': False, - 'label': 'Protocol' + "type": ARG_TYPE.STR, + "description": "The protocol to query clickhouse. Supported: native, http, https. Default: native", + "required": False, + "label": "Protocol", }, user={ - 'type': ARG_TYPE.STR, - 'description': 'The user name used to authenticate with the ClickHouse server.', - 'required': True, - 'label': 'User' + "type": ARG_TYPE.STR, + "description": "The user name used to authenticate with the ClickHouse server.", + "required": True, + "label": "User", }, database={ - 'type': ARG_TYPE.STR, - 'description': 'The database name to use when connecting with the ClickHouse server.', - 'required': True, - 'label': 'Database name' + "type": ARG_TYPE.STR, + "description": "The database name to use when connecting with the ClickHouse server.", + "required": True, + "label": "Database name", }, host={ - 'type': ARG_TYPE.STR, - 'description': 'The host name or IP address of the ClickHouse server. NOTE: use \'127.0.0.1\' instead of \'localhost\' to connect to local server.', - 'required': True, - 'label': 'Host' + "type": ARG_TYPE.STR, + "description": "The host name or IP address of the ClickHouse server. NOTE: use '127.0.0.1' instead of 'localhost' to connect to local server.", + "required": True, + "label": "Host", }, port={ - 'type': ARG_TYPE.INT, - 'description': 'The TCP/IP port of the ClickHouse server. Must be an integer.', - 'required': True, - 'label': 'Port' + "type": ARG_TYPE.INT, + "description": "The TCP/IP port of the ClickHouse server. Must be an integer.", + "required": True, + "label": "Port", }, password={ - 'type': ARG_TYPE.PWD, - 'description': 'The password to authenticate the user with the ClickHouse server.', - 'required': True, - 'label': 'Password', - 'secret': True + "type": ARG_TYPE.PWD, + "description": "The password to authenticate the user with the ClickHouse server.", + "required": True, + "label": "Password", + "secret": True, }, verify={ - 'type': ARG_TYPE.BOOL, - 'description': 'Controls certificate verification in https protocol. Possible choices: true/false. Default is true.', - 'required': False, - 'label': 'SSL Verification', - } + "type": ARG_TYPE.BOOL, + "description": "Controls certificate verification in https protocol. Possible choices: true/false. Default is true.", + "required": False, + "label": "SSL Verification", + }, ) connection_args_example = OrderedDict( - protocol='native', - host='127.0.0.1', - port=9000, - user='root', - password='password', - database='database', - verify=True + protocol="native", host="127.0.0.1", port=9000, user="root", password="password", database="database", verify=True )