blob: a234f44bad123920e196e6d0aaa711511cf63930 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import org.awaitility.Awaitility
import static java.util.concurrent.TimeUnit.SECONDS
suite("test_analyze_long_string", "nonConcurrent") {
// `analyze ... with sync` does not persist the job in the in-memory map,
// so we use async analyze and poll SHOW ANALYZE / SHOW ANALYZE TASK STATUS.
def findJobId = { String ctl, String db, String tbl ->
def rows = sql """show analyze"""
def match = -1L
for (row in rows) {
// columns: job_id, catalog_name, db_name, tbl_name, ...
if (row[1].toString() == ctl
&& row[2].toString() == db
&& row[3].toString() == tbl) {
long id = Long.parseLong(row[0].toString())
if (id > match) {
match = id
}
}
}
return match == -1L ? null : match
}
def waitJobFinished = { long jobId ->
Awaitility.await().atMost(120, SECONDS).pollInterval(2, SECONDS).until {
def rows = sql """show analyze ${jobId}"""
if (rows.isEmpty()) {
return false
}
def state = rows[0][9].toString()
logger.info("job ${jobId} state=${state}")
return state == "FINISHED" || state == "FAILED"
}
}
def collectTaskStatuses = { long jobId ->
def rows = sql """show analyze task status ${jobId}"""
def result = [:]
for (row in rows) {
// columns: task_id, col_name, index_name, message,
// last_state_change_time, time_cost_in_ms, state
def colName = row[1].toString()
def msg = row[3] == null ? "" : row[3].toString()
def state = row[6].toString()
result[colName] = [state: state, message: msg]
}
return result
}
def assertTaskSkipped = { long jobId, List<String> expectedSkipped, List<String> expectedOk ->
def statuses = collectTaskStatuses(jobId)
logger.info("job ${jobId} task statuses=${statuses}")
for (c in expectedSkipped) {
assertTrue(statuses.containsKey(c), "missing task for skipped col ${c}")
assertEquals("FINISHED", statuses[c].state,
"expected FINISHED for skipped col ${c}, got ${statuses[c]}")
assertTrue(statuses[c].message.contains("statistics_max_string_column_length")
|| statuses[c].message.contains("exceeds"),
"expected skip reason visible for col ${c}, got msg=${statuses[c].message}")
}
for (c in expectedOk) {
assertTrue(statuses.containsKey(c), "missing task for col ${c}")
assertEquals("FINISHED", statuses[c].state,
"expected FINISHED for col ${c}, got ${statuses[c]}")
assertEquals("", statuses[c].message,
"expected empty message for col ${c}, got ${statuses[c].message}")
}
}
def collectedColumns = { String tbl ->
def rows = sql """show column stats ${tbl}"""
def set = [] as Set
for (r in rows) {
set.add(r[0].toString())
}
return set
}
sql """drop database if exists test_analyze_long_string"""
sql """create database test_analyze_long_string"""
sql """use test_analyze_long_string"""
sql """set global enable_auto_analyze=false"""
// ---------- Case 1: full analyze on a non-partitioned table ----------
sql """drop table if exists test_analyze_long_string_full"""
sql """
CREATE TABLE test_analyze_long_string_full (
`id` bigint,
`name` varchar(100),
`small_str` varchar(100),
`fixed` char(16),
`big_str` string
) ENGINE=OLAP
DISTRIBUTED BY HASH(`id`) BUCKETS 4
PROPERTIES ("replication_allocation" = "tag.location.default: 1");
"""
sql """insert into test_analyze_long_string_full values(1, 'alice', 'aaa', 'abc', NULL)"""
sql """insert into test_analyze_long_string_full values(2, NULL, 'bbb', 'defg', repeat('y', 100))"""
sql """insert into test_analyze_long_string_full values(3, 'charlie', 'ccc', 'hij', repeat('z', 2048))"""
setFeConfigTemporary([statistics_max_string_column_length: 1024]) {
sql """analyze table test_analyze_long_string_full"""
def jobId = findJobId("internal", "test_analyze_long_string", "test_analyze_long_string_full")
assertNotNull(jobId, "must find analyze job for test_analyze_long_string_full")
waitJobFinished(jobId)
assertTaskSkipped(jobId, ["big_str"], ["id", "name", "small_str", "fixed"])
def collected = collectedColumns("test_analyze_long_string_full")
logger.info("test_analyze_long_string_full collected=${collected}")
assertFalse(collected.contains("big_str"),
"big_str should be skipped, got ${collected}")
assertTrue(collected.contains("id"))
assertTrue(collected.contains("name"))
assertTrue(collected.contains("small_str"))
assertTrue(collected.contains("fixed"))
def bigRows = sql """show column stats test_analyze_long_string_full (big_str)"""
assertEquals(0, bigRows.size(),
"expected no stats row for skipped column big_str, got ${bigRows}")
}
// ---------- Case 2: disabled (limit <= 0) must collect everything ----------
setFeConfigTemporary([statistics_max_string_column_length: 0]) {
sql """analyze table test_analyze_long_string_full with sync"""
def bigRows = sql """show column stats test_analyze_long_string_full (big_str)"""
assertEquals(1, bigRows.size(),
"protection disabled: big_str should be collected, got ${bigRows}")
assertEquals("3.0", bigRows[0][2].toString())
}
// ---------- Case 3: sample analyze (LINEAR / DUJ1 paths) ----------
sql """drop table if exists test_analyze_long_string_sample"""
sql """
CREATE TABLE test_analyze_long_string_sample (
`id` bigint,
`small_str` varchar(100),
`big_str` string
) ENGINE=OLAP
DISTRIBUTED BY HASH(`id`) BUCKETS 2
PROPERTIES ("replication_allocation" = "tag.location.default: 1");
"""
sql """insert into test_analyze_long_string_sample values(1, 'aa', repeat('z', 2048))"""
sql """insert into test_analyze_long_string_sample values(2, 'bb', 'short1')"""
sql """insert into test_analyze_long_string_sample values(3, 'cc', 'short2')"""
sql """insert into test_analyze_long_string_sample values(4, 'dd', 'short3')"""
sql """insert into test_analyze_long_string_sample values(5, 'ee', 'short4')"""
setFeConfigTemporary([statistics_max_string_column_length: 1024]) {
sql """analyze table test_analyze_long_string_sample with sample percent 100"""
def jobId = findJobId("internal", "test_analyze_long_string", "test_analyze_long_string_sample")
assertNotNull(jobId, "must find sample analyze job for test_analyze_long_string_sample")
waitJobFinished(jobId)
assertTaskSkipped(jobId, ["big_str"], ["id", "small_str"])
def collected = collectedColumns("test_analyze_long_string_sample")
assertFalse(collected.contains("big_str"), "sample analyze: big_str should be skipped")
assertTrue(collected.contains("small_str"))
}
// ---------- Case 4: partitioned table — long-string guard does NOT apply ----------
// Partition-granularity analyze uses PARTITION_ANALYZE_TEMPLATE and commits per-batch
// inserts into __partition_stats incrementally. To avoid leaving a mix of fresh and
// stale partition rows on mid-loop abort, the long-string skip guard is intentionally
// NOT wired into this path. Analyze should FINISH normally and produce statistics
// for big_str even when rows exceed statistics_max_string_column_length.
sql """drop table if exists test_analyze_long_string_part"""
sql """
CREATE TABLE test_analyze_long_string_part (
`d` date NOT NULL,
`id` bigint,
`big_str` string
) ENGINE=OLAP
DUPLICATE KEY(`d`)
PARTITION BY RANGE(`d`) (
PARTITION p1 VALUES LESS THAN ('2024-01-02'),
PARTITION p2 VALUES LESS THAN ('2024-01-03')
)
DISTRIBUTED BY HASH(`id`) BUCKETS 2
PROPERTIES ("replication_allocation" = "tag.location.default: 1");
"""
sql """insert into test_analyze_long_string_part values('2024-01-01', 1, 'short_a'),
('2024-01-01', 2, repeat('x', 2048)),
('2024-01-02', 3, 'short_b')"""
setFeConfigTemporary([statistics_max_string_column_length: 1024]) {
sql """set global enable_partition_analyze = true"""
try {
sql """analyze table test_analyze_long_string_part"""
def jobId = findJobId("internal", "test_analyze_long_string", "test_analyze_long_string_part")
assertNotNull(jobId, "must find analyze job for test_analyze_long_string_part")
waitJobFinished(jobId)
// No task should be skipped: partition path bypasses the long-string guard.
assertTaskSkipped(jobId, [], ["d", "id", "big_str"])
def collected = collectedColumns("test_analyze_long_string_part")
assertTrue(collected.contains("big_str"),
"partition analyze should still collect big_str (guard does not apply)")
assertTrue(collected.contains("d"))
assertTrue(collected.contains("id"))
} finally {
sql """set global enable_partition_analyze = false"""
}
}
// ---------- Case 5: sample analyze forced onto DUJ1 template ----------
// useLinearAnalyzeTemplate() normally returns true on small tables (scanFullTable),
// so LINEAR covers the sample path. Use a FE debug point to force DUJ1 so its
// ${lengthAssert} injection is exercised end-to-end.
sql """drop table if exists test_analyze_long_string_duj1"""
sql """
CREATE TABLE test_analyze_long_string_duj1 (
`id` bigint,
`small_str` varchar(100),
`big_str` string
) ENGINE=OLAP
DISTRIBUTED BY HASH(`id`) BUCKETS 2
PROPERTIES ("replication_allocation" = "tag.location.default: 1");
"""
sql """insert into test_analyze_long_string_duj1 values(1, 'aa', repeat('z', 2048))"""
sql """insert into test_analyze_long_string_duj1 values(2, 'bb', 'short1')"""
sql """insert into test_analyze_long_string_duj1 values(3, 'cc', 'short2')"""
sql """insert into test_analyze_long_string_duj1 values(4, 'dd', 'short3')"""
sql """insert into test_analyze_long_string_duj1 values(5, 'ee', 'short4')"""
setFeConfigTemporary([statistics_max_string_column_length: 1024]) {
GetDebugPoint().enableDebugPointForAllFEs('OlapAnalysisTask.useDUJ1Template')
try {
sql """analyze table test_analyze_long_string_duj1 with sample rows 3"""
def jobId = findJobId("internal", "test_analyze_long_string", "test_analyze_long_string_duj1")
assertNotNull(jobId, "must find analyze job for test_analyze_long_string_duj1")
waitJobFinished(jobId)
assertTaskSkipped(jobId, ["big_str"], ["id", "small_str"])
def collected = collectedColumns("test_analyze_long_string_duj1")
assertFalse(collected.contains("big_str"),
"DUJ1 analyze: big_str should be skipped")
assertTrue(collected.contains("small_str"))
} finally {
GetDebugPoint().disableDebugPointForAllFEs('OlapAnalysisTask.useDUJ1Template')
}
}
// ---------- Case 6: WITH SYNC — skipped column must have no stats ----------
// Sync analyze never populates analysisJobIdToTaskMap, so SHOW ANALYZE cannot see
// the skip reason. AnalysisManager.buildAndAssignJob surfaces skip messages via
// ConnectContext OK-packet info for interactive visibility. We cannot easily read
// that info string from JDBC in regression, so we verify the functional outcome:
// the skipped column produces no row in column_statistics, while other columns do.
sql """drop table if exists test_analyze_long_string_full"""
sql """
CREATE TABLE test_analyze_long_string_full (
`id` bigint,
`name` varchar(100),
`big_str` string
) ENGINE=OLAP
DISTRIBUTED BY HASH(`id`) BUCKETS 4
PROPERTIES ("replication_allocation" = "tag.location.default: 1");
"""
sql """insert into test_analyze_long_string_full values(1, 'alice', NULL)"""
sql """insert into test_analyze_long_string_full values(2, 'bob', repeat('y', 100))"""
sql """insert into test_analyze_long_string_full values(3, 'chris', repeat('z', 2048))"""
setFeConfigTemporary([statistics_max_string_column_length: 1024]) {
sql """analyze table test_analyze_long_string_full with sync"""
def bigRows = sql """show column stats test_analyze_long_string_full (big_str)"""
assertEquals(0, bigRows.size(),
"WITH SYNC: big_str should be skipped, got ${bigRows}")
def nameRows = sql """show column stats test_analyze_long_string_full (name)"""
assertEquals(1, nameRows.size(),
"WITH SYNC: name should be collected, got ${nameRows}")
}
// ---------- Cleanup ----------
sql """drop database if exists test_analyze_long_string"""
}