From 74559d6eec7a0eb6271ae044f3462167da34c1ff Mon Sep 17 00:00:00 2001 From: lewis <32348572+wang-zhengxin@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:07:38 +0800 Subject: [PATCH] fix(clickhouse): handle system.settings pagination --- crates/dbx-core/src/query_result_sql.rs | 72 +++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/crates/dbx-core/src/query_result_sql.rs b/crates/dbx-core/src/query_result_sql.rs index fe553a14d..5dcc3006a 100644 --- a/crates/dbx-core/src/query_result_sql.rs +++ b/crates/dbx-core/src/query_result_sql.rs @@ -8,7 +8,7 @@ use crate::sql_dialect::{ firebird_rows_clause, pagination_strategy, quote_table_identifier, PaginationContext, TablePaginationStrategy, }; use sqlparser::ast::{Expr, GroupByExpr, SelectItem, SetExpr, Statement}; -use sqlparser::dialect::{GenericDialect, MsSqlDialect}; +use sqlparser::dialect::{ClickHouseDialect, GenericDialect, MsSqlDialect}; use sqlparser::parser::Parser; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -1015,8 +1015,7 @@ fn add_outer_standard_limit( fn add_clickhouse_limit(statement: &str, limit_sql: &str) -> String { let limit_sql = limit_sql.trim(); - let settings_index = - top_level_sql_tokens(statement).iter().find(|token| token.text == "SETTINGS").map(|token| token.start); + let settings_index = clickhouse_settings_clause_index(statement); if let Some(index) = settings_index { let statement_before_settings = statement[..index].trim_end(); @@ -1027,6 +1026,31 @@ fn add_clickhouse_limit(statement: &str, limit_sql: &str) -> String { append_sql_suffix(statement, &format!("{limit_sql};")) } +fn clickhouse_settings_clause_index(statement: &str) -> Option { + let parsed_settings = Parser::parse_sql(&ClickHouseDialect {}, statement).ok().and_then(|statements| { + let [Statement::Query(query)] = statements.as_slice() else { + return None; + }; + Some(query.settings.is_some()) + }); + if parsed_settings == Some(false) { + return None; + } + + // Keep the lexical fallback for valid ClickHouse syntax that sqlparser does not yet support. + top_level_sql_tokens(statement) + .iter() + .rev() + .find(|token| token.text == "SETTINGS" && !is_qualified_identifier_part(statement, token)) + .map(|token| token.start) +} + +fn is_qualified_identifier_part(sql: &str, token: &SqlToken) -> bool { + let token_end = token.start + token.text.len(); + sql[..token.start].chars().rev().find(|ch| !ch.is_whitespace()) == Some('.') + || sql[token_end..].chars().find(|ch| !ch.is_whitespace()) == Some('.') +} + fn derived_table_sql(prefix: &str, statement: &str, suffix: &str) -> String { format!("{prefix} ({}) {suffix}", statement_for_sql_suffix(statement)) } @@ -1818,6 +1842,48 @@ WHERE u.id = picked.id; ); } + #[test] + fn clickhouse_settings_table_is_not_treated_as_settings_clause() { + let result = build_paginated_query_sql(PaginatedQuerySqlOptions { + original_sql: "SELECT * FROM system.settings".to_string(), + database_type: Some(DatabaseType::ClickHouse), + limit: 100, + offset: 0, + }); + + assert!(result.ok); + assert_eq!(result.sql.unwrap(), "SELECT * FROM system.settings LIMIT 100;"); + } + + #[test] + fn clickhouse_settings_identifier_is_not_treated_as_settings_clause() { + let result = build_paginated_query_sql(PaginatedQuerySqlOptions { + original_sql: "SELECT settings FROM (SELECT 1 AS settings)".to_string(), + database_type: Some(DatabaseType::ClickHouse), + limit: 100, + offset: 0, + }); + + assert!(result.ok); + assert_eq!(result.sql.unwrap(), "SELECT settings FROM (SELECT 1 AS settings) LIMIT 100;"); + } + + #[test] + fn clickhouse_settings_identifier_keeps_trailing_settings_clause() { + let result = build_paginated_query_sql(PaginatedQuerySqlOptions { + original_sql: "SELECT settings FROM (SELECT 1 AS settings) SETTINGS max_threads = 1".to_string(), + database_type: Some(DatabaseType::ClickHouse), + limit: 100, + offset: 0, + }); + + assert!(result.ok); + assert_eq!( + result.sql.unwrap(), + "SELECT settings FROM (SELECT 1 AS settings) LIMIT 100 SETTINGS max_threads = 1;" + ); + } + #[test] fn clickhouse_query_plan_places_limit_before_settings() { let sql = "SELECT * FROM system.clusters SETTINGS max_execution_time = 0";