test(query): lock GROUP BY survival in MySQL pagination and count SQL

Refs: #7832
This commit is contained in:
t8y2
2026-09-06 13:01:37 +08:00
parent 450d9fb9ee
commit c0b4fa09cc
2 changed files with 87 additions and 0 deletions
@@ -353,6 +353,38 @@ describe("splitSqlStatementRanges", () => {
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT 1", "SELECT 2", "SELECT 3"]);
});
// Issue #7832 shape: MySQL GROUP BY over a LEFT JOIN with an aggregated
// derived table, COUNT(DISTINCT IF(...)) in the projection, and inline
// `-- 中文` line comments. The splitter must keep it a single statement and
// cursor-statement extraction must always include the trailing GROUP BY, so
// the executed SQL keeps the query's row cardinality.
it("keeps a commented MySQL GROUP BY query as one statement including the GROUP BY (issue #7832)", () => {
const sql = [
"SELECT",
" base.brand_name",
" ,base.stall_id",
" ,base.floor",
" ,COUNT(1) total_invite -- 邀约数量",
" ,SUM(IFNULL(base.ver_status, 0)) sign_num -- 签到数量",
" ,SUM(IFNULL(dr.draw_count, 0)) draw_num -- 抽奖次数",
" ,SUM(IFNULL(dr.draw_user_num, 0)) draw_user_num -- 抽奖人数",
" ,COUNT(distinct IF(base.ver_status = 1, base.mobile, null)) sign_and_draw_user_num",
"FROM v_form_data_1786326962 base",
"LEFT JOIN (",
" SELECT id, SUM(IFNULL(hx_status, 0)) draw_count, COUNT(distinct IF(hx_status = 1, mobile, null)) draw_user_num",
" FROM v_form_data_1786326962_coupon",
" GROUP BY id",
") dr ON base.id = dr.id",
"GROUP BY base.brand_name, base.stall_id, base.floor",
].join("\n");
const ranges = splitSqlStatementRanges(sql, "mysql");
expect(rangeSqlTexts(ranges)).toEqual([sql.trim()]);
for (const position of [0, indexOf(sql, "total_invite"), indexOf(sql, "sign_and_draw_user_num"), sql.indexOf("GROUP BY") + 5, sql.length - 1]) {
const range = statementRangeAtCursor(sql, position, "mysql");
expect(range?.sql).toContain("GROUP BY base.brand_name, base.stall_id, base.floor");
}
});
it("keeps a trailing statement without a semicolon", () => {
const sql = "SELECT 1;\nSELECT 2";
const ranges = splitSqlStatementRanges(sql);
+55
View File
@@ -2335,6 +2335,61 @@ fn fallback_alias(index: usize) -> String {
mod tests {
use super::*;
/// Query shape from issue #7832: a MySQL GROUP BY over a LEFT JOIN with an
/// aggregated derived table, COUNT(DISTINCT IF(...)) in the projection, and
/// inline `-- 中文` line comments. Locks in the invariants that keep DBX's
/// derived page/count SQL row-count-identical to the user's statement:
/// the statement splitter must keep it a single statement, the page SQL
/// must preserve the GROUP BY while injecting deterministic pagination,
/// and the count wrap must count the grouped result, not the raw join.
#[test]
fn mysql_group_by_with_distinct_if_and_comments_keeps_grouping_in_page_and_count_sql() {
let sql = "SELECT\n base.brand_name\n ,base.stall_id\n ,base.floor\n ,COUNT(1) total_invite -- 邀约数量\n ,SUM(IFNULL(base.ver_status, 0)) sign_num -- 签到数量\n ,SUM(IFNULL(dr.draw_count, 0)) draw_num -- 抽奖次数\n ,SUM(IFNULL(dr.draw_user_num, 0)) draw_user_num -- 抽奖人数\n ,COUNT(distinct IF(base.ver_status = 1, base.mobile, null)) sign_and_draw_user_num\nFROM v_form_data_1786326962 base\nLEFT JOIN (\n SELECT id, SUM(IFNULL(hx_status, 0)) draw_count, COUNT(distinct IF(hx_status = 1, mobile, null)) draw_user_num\n FROM v_form_data_1786326962_coupon\n GROUP BY id\n) dr ON base.id = dr.id\nGROUP BY base.brand_name, base.stall_id, base.floor";
let statements = crate::sql::split_sql_statements_for_database(sql, DatabaseType::Mysql);
assert_eq!(statements.len(), 1, "inline comments must not split the statement");
let plan = build_query_pagination_execution_plan(QueryPaginationExecutionPlanOptions {
sql: sql.to_string(),
query_base_sql: sql.to_string(),
database_type: Some(DatabaseType::Mysql),
pagination: QueryPagination { limit: 500, offset: 0, session_id: None },
use_agent_cursor: false,
first_page_uses_actual_sql: false,
});
let page_sql = plan.page_sql.expect("pagination plan provides page SQL");
assert!(page_sql.to_uppercase().contains("GROUP BY"));
assert!(page_sql.contains("ORDER BY 1, 2, 3 LIMIT 500;"), "dedup pagination must be appended after the GROUP BY, got: {page_sql}");
let count_sql = plan.count_sql.expect("pagination plan provides count SQL");
assert!(count_sql.starts_with("SELECT COUNT(*) AS dbx_total_rows FROM ("));
assert_eq!(
count_sql.matches("GROUP BY").count(),
2,
"the count wrap must keep both the outer and derived-table GROUP BY, got: {count_sql}"
);
let mut variants = vec![
("crlf", sql.replace('\n', "\r\n")),
("trailing semicolon", format!("{sql};")),
("trailing GROUP BY comment", format!("{sql} -- 分组")),
];
if let Some(stripped) = sql.strip_prefix("SELECT") {
variants.push(("leading comment", format!("-- header\nSELECT{stripped}")));
}
for (name, variant) in variants {
let counted = build_count_query_sql(CountQuerySqlOptions {
original_sql: variant.clone(),
database_type: Some(DatabaseType::Mysql),
});
let counted = counted.sql.unwrap_or_default();
assert_eq!(counted.matches("GROUP BY").count(), 2, "{name}: count wrap kept both GROUP BYs");
let statements = crate::sql::split_sql_statements_for_database(&variant, DatabaseType::Mysql);
assert_eq!(statements.len(), 1, "{name}: still a single statement");
}
}
#[test]
fn easysearch_uses_elasticsearch_sql_pagination_rules() {
let paginated = build_paginated_query_sql(PaginatedQuerySqlOptions {