This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new 53c4a24cf7a [fix](schema-change) Reject values exceeding the
CHAR/VARCHAR length in bytes (#69061)
53c4a24cf7a is described below
commit 53c4a24cf7a28bfd95b5dd6e33fe5b1a8ace8b21
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Oct 15 14:11:42 2026 +0800
[fix](schema-change) Reject values exceeding the CHAR/VARCHAR length in
bytes (#69061)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
`ALTER TABLE ... MODIFY COLUMN j VARCHAR(n)` on a JSON column runs a
full
schema change that rewrites historical rows with `CAST(j AS
VARCHAR(n))`.
FE cannot bound the text length of JSON, and unlike the load path
(`OlapTableBlockConvertor`), the BE schema change path never validated
the
length of the converted strings. Historical JSON text longer than `n`
was
written into the new VARCHAR(n) column unchanged, while rows inserted
after
the schema change were limited to `n` bytes, so the same column obeyed
different length constraints for old and new rows.
Reproduce:
```sql
CREATE TABLE t (k INT, j JSON) DUPLICATE KEY(k)
DISTRIBUTED BY HASH(k) BUCKETS 1 PROPERTIES ("replication_num" = "1");
INSERT INTO t VALUES (1, '{"a": 1}');
ALTER TABLE t MODIFY COLUMN j VARCHAR(1);
-- before: the job FINISHED and LENGTH(j) of the old row was 7
```
`BlockChanger::change_block` now checks, right after the existing
null-map
validation, that every non-null converted value fits the declared
CHAR/VARCHAR length of the new tablet column and fails the alter task
with DATA_QUALITY_ERROR otherwise. The limit comes from the new tablet
column instead of the expression result type, because a string-to-string
change (e.g. widening VARCHAR without light schema change) is sent as a
slot ref whose runtime type carries no length; the 2-byte length prefix
included in the VARCHAR tablet column length is excluded. The job is
cancelled with a message naming the column, the declared length, the
actual length and a value prefix, and the table keeps its original
schema
and data. Users can retry with a sufficient length or STRING. The check
is
shared by local and cloud schema change since both use `BlockChanger`.
Default values of new CHAR/VARCHAR columns had the same gap: FE compared
the default value with the declared length by characters
(`String.length()`), while the length is in bytes. For example
`ADD COLUMN c VARCHAR(1) DEFAULT '中'` finished and every old row read a
3-byte value, and a new CHAR key column rewrote old rows with a value
wider than the CHAR length. FE now checks CHAR/VARCHAR default values by
their UTF-8 byte length (CREATE TABLE and ADD/MODIFY COLUMN), and
`change_block` also checks the materialized default value of a new
column
so a rewrite requested by an FE without the check is cancelled as well.
A schema change that only links the old rowsets (e.g. adding a value
column) never rewrites data, so an over-length default value sent by an
FE without the check during a rolling upgrade is not caught by BE.
The rollup job that builds a synchronous materialized view runs the same
`change_block` and had the same gap. For example
`MAX(CAST(v AS CHAR(1)))` is stored as `substring(CAST(v AS CHAR(1)), 1,
1)`,
which keeps the first character, so '中文' wrote the 3-byte '中' into the
CHAR(1) column, while loading the same value into the materialized view
was rejected. The length check now applies to the expression results of
both schema change and rollup, and such a rollup job is cancelled.
### Release note
Schema changes that convert a column to CHAR/VARCHAR(n) (e.g. JSON to
VARCHAR(n)) are now cancelled when an existing value is longer than n
bytes, instead of silently keeping values that violate the new length.
Creating a synchronous materialized view now fails the same way when an
expression result is longer than its CHAR/VARCHAR column in bytes.
A CHAR/VARCHAR default value longer than the column length in bytes is
now rejected (e.g. `VARCHAR(2) DEFAULT '中'`).
### Check List (For Author)
- Test:
- Unit Test: BlockChangerStringLengthTest, StringDefaultValueLengthTest
- Regression test:
schema_change_p0/test_schema_change_string_length_limit,
plus existing test_type_length_change, test_alter_table_column_nullable,
test_varchar_schema_change, test_dup_schema_value_modify,
test_dup_schema_value_modify4, test_unique_schema_value_modify3,
test_agg_schema_value_modify, all suites of rollup_p0 and mv_p0
- Behavior changed: Yes (such alter jobs are cancelled with an explicit
error message; CHAR/VARCHAR
default values longer than the column in bytes are rejected; such
synchronous materialized
view creation is cancelled)
- Does this need documentation: No
---
be/src/storage/schema_change/schema_change.cpp | 51 +++++++
be/src/storage/schema_change/schema_change.h | 6 +
.../block_changer_string_length_test.cpp | 131 ++++++++++++++++
.../java/org/apache/doris/analysis/ColumnDef.java | 6 +
.../analysis/StringDefaultValueLengthTest.java | 48 ++++++
.../test_schema_change_string_length_limit.out | 63 ++++++++
.../test_schema_change_string_length_limit.groovy | 165 +++++++++++++++++++++
7 files changed, 470 insertions(+)
diff --git a/be/src/storage/schema_change/schema_change.cpp
b/be/src/storage/schema_change/schema_change.cpp
index 6295091eb94..f57aad8ef6f 100644
--- a/be/src/storage/schema_change/schema_change.cpp
+++ b/be/src/storage/schema_change/schema_change.cpp
@@ -44,6 +44,7 @@
#include "core/block/column_with_type_and_name.h"
#include "core/column/column.h"
#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
#include "exec/common/util.hpp"
#include "exec/common/variant_util.h"
#include "exprs/aggregate/aggregate_function.h"
@@ -421,6 +422,11 @@ Status BlockChanger::change_block(Block* ref_block, Block*
new_block) const {
RETURN_IF_ERROR(
_check_cast_valid(ref_column_def.column,
result_tmp_column_def.column));
}
+ // Both the schema change and the rollup (e.g. a synchronous
materialized view on
+ // MAX(CAST(v AS CHAR(1)))) write the expression result into the
new column directly.
+ RETURN_IF_ERROR(check_string_length_within_schema(
+ result_tmp_column_def.column,
*_schema_mapping[idx].new_column,
+ _schema_mapping[idx].new_column->name()));
swap_idx_list.emplace_back(result_tmp_column_idx, idx);
} else if (_schema_mapping[idx].ref_column_idx < 0) {
// new column, write default value
@@ -434,6 +440,11 @@ Status BlockChanger::change_block(Block* ref_block, Block*
new_block) const {
column->insert_duplicate_fields(value, row_num);
}
new_block->get_by_position(idx).column = std::move(column);
+ // An FE before the byte length check of default values may send a
default value
+ // longer than the CHAR/VARCHAR length in bytes.
+ RETURN_IF_ERROR(check_string_length_within_schema(
+ new_block->get_by_position(idx).column,
*_schema_mapping[idx].new_column,
+ _schema_mapping[idx].new_column->name()));
} else {
// same type, just swap column
swap_idx_list.emplace_back(_schema_mapping[idx].ref_column_idx,
idx);
@@ -531,6 +542,46 @@ Status BlockChanger::_check_cast_valid(ColumnPtr
input_column, ColumnPtr output_
return Status::OK();
}
+Status BlockChanger::check_string_length_within_schema(const ColumnPtr& column,
+ const TabletColumn&
new_column,
+ const std::string&
column_name) {
+ // The converted values (e.g. JSON to VARCHAR(n)), the rollup expression
results and the default
+ // values of new columns are not truncated to the declared CHAR/VARCHAR
length, so they must be
+ // checked here like the load path does.
+ // STRING has no declared length.
+ int32_t max_length = 0;
+ if (new_column.type() == FieldType::OLAP_FIELD_TYPE_CHAR) {
+ max_length = new_column.length();
+ } else if (new_column.type() == FieldType::OLAP_FIELD_TYPE_VARCHAR) {
+ // The VARCHAR length of a tablet column includes the 2-byte length
prefix, see
+ // TabletColumn::get_field_length_by_type.
+ max_length = new_column.length() -
cast_set<int32_t>(sizeof(OLAP_VARCHAR_MAX_LENGTH));
+ } else {
+ return Status::OK();
+ }
+ DORIS_CHECK(max_length >= 0) << "invalid length of column " << column_name
<< ": "
+ << new_column.length();
+
+ const auto* nullable = check_and_get_column<ColumnNullable>(column.get());
+ const auto& string_column = assert_cast<const ColumnString&>(
+ nullable == nullptr ? *column : nullable->get_nested_column());
+ const auto* null_map = nullable == nullptr ? nullptr :
nullable->get_null_map_data().data();
+ for (size_t i = 0; i < string_column.size(); ++i) {
+ // NULL has no length, same as the load path.
+ if (null_map != nullptr && null_map[i]) {
+ continue;
+ }
+ const auto value = string_column.get_data_at(i);
+ if (value.size > cast_set<size_t>(max_length)) {
+ return Status::DataQualityError(
+ "the length of value exceeds the length of column {} after
schema change, "
+ "schema length: {}; actual length: {}; first 32 bytes of
value: [{}]",
+ column_name, max_length, value.size, value.to_prefix(32));
+ }
+ }
+ return Status::OK();
+}
+
Status LinkedSchemaChange::process(RowsetReaderSharedPtr rowset_reader,
RowsetWriter* rowset_writer,
BaseTabletSPtr new_tablet, BaseTabletSPtr
base_tablet,
TabletSchemaSPtr base_tablet_schema,
diff --git a/be/src/storage/schema_change/schema_change.h
b/be/src/storage/schema_change/schema_change.h
index 1220e05d818..ac2b0052027 100644
--- a/be/src/storage/schema_change/schema_change.h
+++ b/be/src/storage/schema_change/schema_change.h
@@ -83,6 +83,12 @@ public:
bool has_where() const { return _where_expr != nullptr; }
+ // Returns DataQualityError if any non-null value in `column` is longer
than the declared
+ // length in bytes of `new_column` when it is CHAR/VARCHAR.
+ static Status check_string_length_within_schema(const ColumnPtr& column,
+ const TabletColumn&
new_column,
+ const std::string&
column_name);
+
private:
static Status _check_cast_valid(ColumnPtr ref_column, ColumnPtr
new_column);
diff --git a/be/test/storage/schema_change/block_changer_string_length_test.cpp
b/be/test/storage/schema_change/block_changer_string_length_test.cpp
new file mode 100644
index 00000000000..b78e4e9eab5
--- /dev/null
+++ b/be/test/storage/schema_change/block_changer_string_length_test.cpp
@@ -0,0 +1,131 @@
+// 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.
+
+#include <gtest/gtest.h>
+
+#include <cstring>
+#include <string>
+
+#include "common/status.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/column/column_vector.h"
+#include "storage/olap_define.h"
+#include "storage/schema_change/schema_change.h"
+#include "storage/tablet/tablet_schema.h"
+
+namespace doris {
+
+namespace {
+
+// Builds the new tablet column the same way as the tablet meta of a created
tablet, whose
+// VARCHAR length includes the 2-byte length prefix.
+TabletColumn varchar_column(int32_t declared_length) {
+ return {FieldAggregationMethod::OLAP_FIELD_AGGREGATION_NONE,
FieldType::OLAP_FIELD_TYPE_VARCHAR,
+ true, 1, declared_length + sizeof(OLAP_VARCHAR_MAX_LENGTH)};
+}
+
+TabletColumn char_column(int32_t declared_length) {
+ return {FieldAggregationMethod::OLAP_FIELD_AGGREGATION_NONE,
FieldType::OLAP_FIELD_TYPE_CHAR,
+ true, 1, static_cast<size_t>(declared_length)};
+}
+
+} // namespace
+
+TEST(BlockChangerStringLengthTest, AcceptsValuesWithinSchemaLength) {
+ auto column = ColumnString::create();
+ column->insert_data("abc", 3);
+ column->insert_data("abcdef", 6);
+ column->insert_data("", 0);
+
EXPECT_TRUE(BlockChanger::check_string_length_within_schema(std::move(column),
+
varchar_column(6), "c")
+ .ok());
+}
+
+TEST(BlockChangerStringLengthTest, RejectsValueLongerThanSchemaLength) {
+ auto column = ColumnString::create();
+ column->insert_data("abc", 3);
+ column->insert_data(R"({"a":1})", 7);
+ Status st =
BlockChanger::check_string_length_within_schema(std::move(column),
+
varchar_column(5), "j");
+ EXPECT_EQ(st.code(), ErrorCode::DATA_QUALITY_ERROR);
+ EXPECT_NE(st.to_string().find("exceeds the length of column j"),
std::string::npos);
+ EXPECT_NE(st.to_string().find("schema length: 5"), std::string::npos);
+ EXPECT_NE(st.to_string().find("actual length: 7"), std::string::npos);
+ EXPECT_NE(st.to_string().find(R"([{"a":1}])"), std::string::npos);
+}
+
+TEST(BlockChangerStringLengthTest, ExcludesVarcharLengthPrefix) {
+ // VARCHAR(5) has tablet column length 7; values of 6 and 7 bytes must
still be rejected.
+ for (const auto* value : {"abcdef", "abcdefg"}) {
+ auto column = ColumnString::create();
+ column->insert_data(value, strlen(value));
+
EXPECT_EQ(BlockChanger::check_string_length_within_schema(std::move(column),
+
varchar_column(5), "c")
+ .code(),
+ ErrorCode::DATA_QUALITY_ERROR)
+ << value;
+ }
+}
+
+TEST(BlockChangerStringLengthTest, ChecksCharLength) {
+ auto column = ColumnString::create();
+ column->insert_data("abcde", 5);
+ ColumnPtr ptr = std::move(column);
+ EXPECT_TRUE(BlockChanger::check_string_length_within_schema(ptr,
char_column(5), "c").ok());
+ EXPECT_EQ(BlockChanger::check_string_length_within_schema(ptr,
char_column(4), "c").code(),
+ ErrorCode::DATA_QUALITY_ERROR);
+}
+
+TEST(BlockChangerStringLengthTest, IgnoresStringColumn) {
+ // STRING has no declared length.
+ auto column = ColumnString::create();
+ column->insert_data("abcdefgh", 8);
+ TabletColumn
string_column(FieldAggregationMethod::OLAP_FIELD_AGGREGATION_NONE,
+ FieldType::OLAP_FIELD_TYPE_STRING, true, 1, 1);
+ EXPECT_TRUE(
+ BlockChanger::check_string_length_within_schema(std::move(column),
string_column, "c")
+ .ok());
+}
+
+TEST(BlockChangerStringLengthTest, ComparesByteLength) {
+ // "中文" is 6 bytes in UTF-8, VARCHAR length is in bytes.
+ auto column = ColumnString::create();
+ column->insert_data("中文", 6);
+ ColumnPtr ptr = std::move(column);
+ EXPECT_TRUE(BlockChanger::check_string_length_within_schema(ptr,
varchar_column(6), "c").ok());
+ EXPECT_EQ(BlockChanger::check_string_length_within_schema(ptr,
varchar_column(5), "c").code(),
+ ErrorCode::DATA_QUALITY_ERROR);
+}
+
+TEST(BlockChangerStringLengthTest, SkipsNullRowsButChecksNonNullRows) {
+ // The nested payload of a NULL row is ignored even if it is long.
+ auto nested = ColumnString::create();
+ nested->insert_data("zzzzzzzzzz", 10);
+ nested->insert_data("ab", 2);
+ auto null_map = ColumnUInt8::create();
+ null_map->insert_value(1);
+ null_map->insert_value(0);
+ ColumnPtr nullable = ColumnNullable::create(std::move(nested),
std::move(null_map));
+ EXPECT_TRUE(
+ BlockChanger::check_string_length_within_schema(nullable,
varchar_column(2), "c").ok());
+ EXPECT_EQ(BlockChanger::check_string_length_within_schema(nullable,
varchar_column(1), "c")
+ .code(),
+ ErrorCode::DATA_QUALITY_ERROR);
+}
+
+} // namespace doris
diff --git a/fe/fe-core/src/main/java/org/apache/doris/analysis/ColumnDef.java
b/fe/fe-core/src/main/java/org/apache/doris/analysis/ColumnDef.java
index fa2b593e5d4..df8046dc33c 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/ColumnDef.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/ColumnDef.java
@@ -34,6 +34,7 @@ import org.apache.doris.nereids.types.DataType;
import com.google.common.base.Preconditions;
+import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
@@ -440,6 +441,11 @@ public class ColumnDef {
break;
case CHAR:
case VARCHAR:
+ // CHAR/VARCHAR length is in bytes, the same as the load path
checks.
+ if (defaultValue.getBytes(StandardCharsets.UTF_8).length >
scalarType.getLength()) {
+ throw new AnalysisException("Default value is too long: "
+ defaultValue);
+ }
+ break;
case HLL:
case STRING:
case JSONB:
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/analysis/StringDefaultValueLengthTest.java
b/fe/fe-core/src/test/java/org/apache/doris/analysis/StringDefaultValueLengthTest.java
new file mode 100644
index 00000000000..a4a99c32ff0
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/analysis/StringDefaultValueLengthTest.java
@@ -0,0 +1,48 @@
+// 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.
+
+package org.apache.doris.analysis;
+
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.common.AnalysisException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class StringDefaultValueLengthTest {
+ // CHAR/VARCHAR length is in bytes, "中" is 3 bytes in UTF-8.
+ @Test
+ void testVarcharDefaultValueLengthInBytes() {
+ Assertions.assertDoesNotThrow(
+ () ->
ColumnDef.validateDefaultValue(ScalarType.createVarcharType(3), "中", null));
+ AnalysisException e = Assertions.assertThrows(AnalysisException.class,
+ () ->
ColumnDef.validateDefaultValue(ScalarType.createVarcharType(2), "中", null));
+ Assertions.assertTrue(e.getMessage().contains("Default value is too
long"), e.getMessage());
+ }
+
+ @Test
+ void testCharDefaultValueLengthInBytes() {
+ Assertions.assertDoesNotThrow(
+ () ->
ColumnDef.validateDefaultValue(ScalarType.createCharType(3), "中", null));
+ Assertions.assertDoesNotThrow(
+ () ->
ColumnDef.validateDefaultValue(ScalarType.createCharType(3), "abc", null));
+ Assertions.assertThrows(AnalysisException.class,
+ () ->
ColumnDef.validateDefaultValue(ScalarType.createCharType(1), "中", null));
+ Assertions.assertThrows(AnalysisException.class,
+ () ->
ColumnDef.validateDefaultValue(ScalarType.createCharType(3), "abcd", null));
+ }
+}
diff --git
a/regression-test/data/schema_change_p0/test_schema_change_string_length_limit.out
b/regression-test/data/schema_change_p0/test_schema_change_string_length_limit.out
new file mode 100644
index 00000000000..97f64162cd0
--- /dev/null
+++
b/regression-test/data/schema_change_p0/test_schema_change_string_length_limit.out
@@ -0,0 +1,63 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !before --
+1 {"a":1}
+2 [1,2,3]
+3 \N
+
+-- !desc_after_cancel --
+k int Yes true \N
+j json Yes false \N NONE
+
+-- !after_cancel --
+1 {"a":1}
+2 [1,2,3]
+3 \N
+
+-- !desc_after_finish --
+k int Yes true \N
+j varchar(7) Yes false \N NONE
+
+-- !after_finish --
+1 {"a":1} 7
+2 [1,2,3] 7
+3 \N \N
+4 abcdefg 7
+
+-- !desc_after_widen --
+k int Yes true \N
+v varchar(6) Yes false \N NONE
+
+-- !after_widen --
+1 abcde 5
+2 abc 3
+3 \N \N
+4 abcdef 6
+
+-- !desc_after_default --
+k int Yes true \N
+ck char(3) Yes true 中
+v int Yes false \N NONE
+c varchar(3) Yes false 中 NONE
+
+-- !after_default --
+1 中 3 1 中 3
+2 中 3 2 中 3
+
+-- !mv_char --
+1 x 1
+2 d 1
+3 \N \N
+
+-- !mv_varchar --
+1 {" 2
+2 \N \N
+3 \N \N
+
+-- !desc_after_mv_cancel --
+test_schema_change_string_length_limit_mv DUP_KEYS k int
int Yes true \N true
+ v varchar(8192) varchar(8192) Yes false \N
NONE true
+ j json json Yes false \N NONE true
+
+test_schema_change_string_length_limit_mv_varchar AGG_KEYS kv
int int Yes true \N true `k`
+ v2 varchar(2) varchar(2) Yes false \N
MAX true substring(CAST(`j` AS varchar(2)), 1, 2)
+
diff --git
a/regression-test/suites/schema_change_p0/test_schema_change_string_length_limit.groovy
b/regression-test/suites/schema_change_p0/test_schema_change_string_length_limit.groovy
new file mode 100644
index 00000000000..ea75ad8e295
--- /dev/null
+++
b/regression-test/suites/schema_change_p0/test_schema_change_string_length_limit.groovy
@@ -0,0 +1,165 @@
+// 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.
+
+suite("test_schema_change_string_length_limit", "p0") {
+ def waitForJob = { tableName, expectedState ->
+ def row = null
+ int maxTrySecs = 120
+ while (maxTrySecs-- > 0) {
+ def res = sql """ SHOW ALTER TABLE COLUMN WHERE
IndexName='${tableName}' ORDER BY createtime DESC LIMIT 1 """
+ row = res[0]
+ if (row[9] == "FINISHED" || row[9] == "CANCELLED") {
+ break
+ }
+ sleep(1000)
+ }
+ assertEquals(expectedState, row[9])
+ return row
+ }
+
+ sql "DROP TABLE IF EXISTS test_schema_change_string_length_limit"
+ sql """
+ CREATE TABLE test_schema_change_string_length_limit (
+ k INT,
+ j JSON
+ ) DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ // Both JSON values are 7 bytes after being converted to text, the NULL
row has no length.
+ sql """INSERT INTO test_schema_change_string_length_limit VALUES (1,
'{"a": 1}'), (2, '[1,2,3]'), (3, NULL)"""
+ order_qt_before """SELECT k, j FROM
test_schema_change_string_length_limit"""
+
+ // The new length is too short for the old data: the job is cancelled and
the table is unchanged.
+ sql """ALTER TABLE test_schema_change_string_length_limit MODIFY COLUMN j
VARCHAR(5)"""
+ def cancelled = waitForJob("test_schema_change_string_length_limit",
"CANCELLED")
+ assertTrue(cancelled[10].toString().contains("exceeds the length of column
j"), cancelled[10].toString())
+ qt_desc_after_cancel """DESC test_schema_change_string_length_limit"""
+ order_qt_after_cancel """SELECT k, j FROM
test_schema_change_string_length_limit"""
+
+ // The new length is enough: the job finishes and old and new rows obey
VARCHAR(7).
+ sql """ALTER TABLE test_schema_change_string_length_limit MODIFY COLUMN j
VARCHAR(7)"""
+ waitForJob("test_schema_change_string_length_limit", "FINISHED")
+ qt_desc_after_finish """DESC test_schema_change_string_length_limit"""
+ sql """INSERT INTO test_schema_change_string_length_limit VALUES (4,
'abcdefg')"""
+ order_qt_after_finish """SELECT k, j, LENGTH(j) FROM
test_schema_change_string_length_limit"""
+
+ // Widening VARCHAR on a table without light schema change rewrites old
data through a slot
+ // ref, whose values already fit the new length.
+ sql "DROP TABLE IF EXISTS test_schema_change_string_length_limit_widen"
+ sql """
+ CREATE TABLE test_schema_change_string_length_limit_widen (
+ k INT,
+ v VARCHAR(5)
+ ) DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES ("replication_num" = "1", "light_schema_change" = "false")
+ """
+ sql """INSERT INTO test_schema_change_string_length_limit_widen VALUES (1,
'abcde'), (2, 'abc'), (3, NULL)"""
+ sql """ALTER TABLE test_schema_change_string_length_limit_widen MODIFY
COLUMN v VARCHAR(6)"""
+ waitForJob("test_schema_change_string_length_limit_widen", "FINISHED")
+ qt_desc_after_widen """DESC test_schema_change_string_length_limit_widen"""
+ sql """INSERT INTO test_schema_change_string_length_limit_widen VALUES (4,
'abcdef')"""
+ order_qt_after_widen """SELECT k, v, LENGTH(v) FROM
test_schema_change_string_length_limit_widen"""
+
+ // CHAR/VARCHAR length is in bytes, so a default value longer than the
column in bytes is
+ // rejected. '中' is 3 bytes in UTF-8.
+ sql "DROP TABLE IF EXISTS test_schema_change_string_length_limit_default"
+ test {
+ sql """
+ CREATE TABLE test_schema_change_string_length_limit_default (
+ k INT,
+ c VARCHAR(2) DEFAULT '中'
+ ) DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ exception "Default value is too long"
+ }
+ sql """
+ CREATE TABLE test_schema_change_string_length_limit_default (
+ k INT,
+ v INT
+ ) DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES ("replication_num" = "1", "light_schema_change" = "false")
+ """
+ sql """INSERT INTO test_schema_change_string_length_limit_default VALUES
(1, 1), (2, 2)"""
+ // A new value column is linked and filled with the default value when it
is read.
+ test {
+ sql """ALTER TABLE test_schema_change_string_length_limit_default ADD
COLUMN c VARCHAR(2) DEFAULT '中'"""
+ exception "Default value is too long"
+ }
+ // A new CHAR key column changes the short key and rewrites old data with
the default value.
+ test {
+ sql """ALTER TABLE test_schema_change_string_length_limit_default ADD
COLUMN ck CHAR(1) KEY DEFAULT '中' AFTER k"""
+ exception "Default value is too long"
+ }
+ sql """ALTER TABLE test_schema_change_string_length_limit_default ADD
COLUMN c VARCHAR(3) DEFAULT '中'"""
+ waitForJob("test_schema_change_string_length_limit_default", "FINISHED")
+ sql """ALTER TABLE test_schema_change_string_length_limit_default ADD
COLUMN ck CHAR(3) KEY DEFAULT '中' AFTER k"""
+ waitForJob("test_schema_change_string_length_limit_default", "FINISHED")
+ qt_desc_after_default """DESC
test_schema_change_string_length_limit_default"""
+ order_qt_after_default """SELECT k, ck, LENGTH(ck), v, c, LENGTH(c) FROM
test_schema_change_string_length_limit_default"""
+
+ // A synchronous materialized view is built by the rollup job, which also
checks the length of
+ // the expression results.
+ sql "DROP TABLE IF EXISTS test_schema_change_string_length_limit_mv"
+ sql """
+ CREATE TABLE test_schema_change_string_length_limit_mv (
+ k INT,
+ v VARCHAR(8192),
+ j JSON
+ ) DUPLICATE KEY(k)
+ DISTRIBUTED BY HASH(k) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """INSERT INTO test_schema_change_string_length_limit_mv VALUES
+ (1, repeat('x', 8192), '{"a": 12345}'), (1, 'abc', '[1,2,3]'), (2,
'de', NULL), (3, NULL, NULL)"""
+ // Values cast to a narrow CHAR/VARCHAR fit the materialized view column.
+ create_sync_mv(context.dbName, "test_schema_change_string_length_limit_mv",
+ "test_schema_change_string_length_limit_mv_char",
+ """SELECT k AS kc, MAX(CAST(v AS CHAR(1))) AS c1 FROM
test_schema_change_string_length_limit_mv GROUP BY k""")
+ create_sync_mv(context.dbName, "test_schema_change_string_length_limit_mv",
+ "test_schema_change_string_length_limit_mv_varchar",
+ """SELECT k AS kv, MAX(CAST(j AS VARCHAR(2))) AS v2 FROM
test_schema_change_string_length_limit_mv GROUP BY k""")
+ order_qt_mv_char """SELECT kc, c1, LENGTH(c1) FROM
test_schema_change_string_length_limit_mv INDEX
test_schema_change_string_length_limit_mv_char"""
+ order_qt_mv_varchar """SELECT kv, v2, LENGTH(v2) FROM
test_schema_change_string_length_limit_mv INDEX
test_schema_change_string_length_limit_mv_varchar"""
+
+ // CAST(v AS CHAR(1)) keeps the first character, which is 3 bytes for '中文'
and longer than the
+ // CHAR(1) column in bytes, so the rollup job is cancelled like a load of
the same value into the
+ // materialized view fails.
+ sql """DROP MATERIALIZED VIEW IF EXISTS
test_schema_change_string_length_limit_mv_char ON
test_schema_change_string_length_limit_mv"""
+ sql """INSERT INTO test_schema_change_string_length_limit_mv VALUES (4,
'中文', NULL)"""
+ sql """CREATE MATERIALIZED VIEW
test_schema_change_string_length_limit_mv_char AS
+ SELECT k AS kc, MAX(CAST(v AS CHAR(1))) AS c1 FROM
test_schema_change_string_length_limit_mv GROUP BY k"""
+ def mvRow = null
+ for (int i = 0; i < 120; i++) {
+ // The latest job of the materialized view, an earlier job of the same
name has finished.
+ mvRow = sql(""" SHOW ALTER TABLE MATERIALIZED VIEW WHERE
TableName='test_schema_change_string_length_limit_mv' """)
+ .findAll { it[5] ==
"test_schema_change_string_length_limit_mv_char" }
+ .max { it[0] as Long }
+ if (mvRow[8] == "FINISHED" || mvRow[8] == "CANCELLED") {
+ break
+ }
+ sleep(1000)
+ }
+ assertEquals("CANCELLED", mvRow[8])
+ assertTrue(mvRow[9].toString().contains("exceeds the length of column"),
mvRow[9].toString())
+ // Only the VARCHAR materialized view is left.
+ qt_desc_after_mv_cancel """DESC test_schema_change_string_length_limit_mv
ALL"""
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]