This is an automated email from the ASF dual-hosted git repository.
morrySnow 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 44aaa1e87eb [fix](binlog) Align row binlog after schema with before
values (#65438)
44aaa1e87eb is described below
commit 44aaa1e87ebae9bbd8b582f97a6b4d30d075fd58
Author: yujun <[email protected]>
AuthorDate: Fri Jul 10 16:27:48 2026 +0800
[fix](binlog) Align row binlog after schema with before values (#65438)
### What problem does this PR solve?
Problem Summary:
Row binlog `MIN_DELTA` does not output separate schemas for
update-before rows and update-after rows. Instead, it materializes both
row kinds through one unified output schema, which is the generated
after/output value schema.
That means an update-before row also has to be written into the after
value slots. For MoW row binlog queries with historical values enabled,
the before values come from the nullable `__DORIS_BEFORE_*` mirror
columns. Before this change, FE generated after value columns by copying
the user column definition directly. When the user column was a complex
`NOT NULL` type such as `BITMAP`, the after/output slot stayed
non-nullable, but `MIN_DELTA` still had to write the nullable before
value into that same slot.
This schema mismatch could crash BE during `MIN_DELTA` row
materialization with a bad cast from `ColumnNullable` to
`ColumnComplexType<BITMAP>`. The fix is to make generated after value
columns follow the same nullable and default-cleared contract as before
value columns, so the unified `MIN_DELTA` output schema can safely carry
both before rows and after rows.
This PR also adds FE unit coverage for row binlog schema generation and
a normal row binlog regression case that reproduces the crash with a
`BITMAP NOT NULL` column on `MIN_DELTA`.
---
.../main/java/org/apache/doris/catalog/Column.java | 5 +
.../catalog/OlapTableRowBinlogSchemaTest.java | 30 ++-
.../row_binlog_p0/test_binlog_changes_syntax.out | 212 ++++++++++++++++++
.../test_binlog_changes_syntax.groovy | 236 +++++++--------------
4 files changed, 320 insertions(+), 163 deletions(-)
diff --git a/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Column.java
b/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Column.java
index f3acb582001..923d1954d77 100644
--- a/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Column.java
+++ b/fe/fe-catalog/src/main/java/org/apache/doris/catalog/Column.java
@@ -101,6 +101,11 @@ public class Column implements GsonPostProcessable {
Column afterValueColumn = new Column(column);
afterValueColumn.setComment("after value (" + column.getName() + ")");
afterValueColumn.setAggregationType(AggregateType.NONE, true);
+ afterValueColumn.setIsAllowNull(true);
+ // clear default value
+ afterValueColumn.defaultValue = null;
+ afterValueColumn.defaultValueExprDef = null;
+ afterValueColumn.realDefaultValue = null;
return afterValueColumn;
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableRowBinlogSchemaTest.java
b/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableRowBinlogSchemaTest.java
index f6130782d73..972f0d82267 100755
---
a/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableRowBinlogSchemaTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/catalog/OlapTableRowBinlogSchemaTest.java
@@ -33,7 +33,7 @@ public class OlapTableRowBinlogSchemaTest {
long baseIndexId = 1L;
Column key = new Column("k1", PrimitiveType.INT);
key.setIsKey(true);
- Column value = new Column("v1", PrimitiveType.INT);
+ Column value = new Column("v1", Type.INT, false, null, false, "7", "");
value.setIsKey(false);
List<Column> baseSchema = Lists.newArrayList(key, value);
@@ -62,6 +62,8 @@ public class OlapTableRowBinlogSchemaTest {
public void testRowBinlogSchemaOnEnable() {
OlapTable tableWithoutBefore =
newTestTable(BinlogTestUtils.newTestRowBinlogConfig(true, false));
Assertions.assertTrue(tableWithoutBefore.needRowBinlog());
+
Assertions.assertFalse(tableWithoutBefore.getBaseSchema(true).get(1).isAllowNull());
+ Assertions.assertEquals("7",
tableWithoutBefore.getBaseSchema(true).get(1).getDefaultValue());
List<String> tableWithoutBeforeColumns =
tableWithoutBefore.getRowBinlogMeta().getSchema(true).stream().map(Column::getName)
.collect(Collectors.toList());
@@ -70,17 +72,39 @@ public class OlapTableRowBinlogSchemaTest {
Assertions.assertEquals(tableWithoutBeforeColumns.indexOf(Column.BINLOG_OPERATION_COL),
3);
Assertions.assertEquals(tableWithoutBeforeColumns.indexOf(Column.BINLOG_TIMESTAMP_COL),
4);
Assertions.assertEquals(tableWithoutBeforeColumns.size(), 5);
+ Column afterColumnWithoutBefore =
tableWithoutBefore.getRowBinlogMeta().getSchema(true).stream()
+ .filter(column -> column.getName().equals("v1"))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertTrue(afterColumnWithoutBefore.isAllowNull());
+ Assertions.assertNull(afterColumnWithoutBefore.getDefaultValue());
+
Assertions.assertNull(afterColumnWithoutBefore.getDefaultValueExprDef());
+ Assertions.assertNull(afterColumnWithoutBefore.getRealDefaultValue());
OlapTable tableWithBefore =
newTestTable(BinlogTestUtils.newTestRowBinlogConfig(true, true));
Assertions.assertTrue(tableWithBefore.needRowBinlog());
+ List<Column> rowBinlogSchemaWithBefore =
tableWithBefore.getRowBinlogMeta().getSchema(true);
List<String> tableWithBeforeColumns =
-
tableWithBefore.getRowBinlogMeta().getSchema(true).stream().map(Column::getName)
- .collect(Collectors.toList());
+
rowBinlogSchemaWithBefore.stream().map(Column::getName).collect(Collectors.toList());
Assertions.assertTrue(tableWithBeforeColumns.contains(Column.generateBeforeColName("v1")));
Assertions.assertEquals(tableWithBeforeColumns.indexOf(Column.BINLOG_LSN_COL),
3);
Assertions.assertEquals(tableWithBeforeColumns.indexOf(Column.BINLOG_OPERATION_COL),
4);
Assertions.assertEquals(tableWithBeforeColumns.indexOf(Column.BINLOG_TIMESTAMP_COL),
5);
Assertions.assertEquals(tableWithBeforeColumns.size(), 6);
+ Column afterColumnWithBefore = rowBinlogSchemaWithBefore.stream()
+ .filter(column -> column.getName().equals("v1"))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertTrue(afterColumnWithBefore.isAllowNull());
+ Assertions.assertNull(afterColumnWithBefore.getDefaultValue());
+ Column beforeColumn = rowBinlogSchemaWithBefore.stream()
+ .filter(column ->
column.getName().equals(Column.generateBeforeColName("v1")))
+ .findFirst()
+ .orElseThrow();
+ Assertions.assertTrue(beforeColumn.isAllowNull());
+ Assertions.assertNull(beforeColumn.getDefaultValue());
+ Assertions.assertNull(beforeColumn.getDefaultValueExprDef());
+ Assertions.assertNull(beforeColumn.getRealDefaultValue());
}
@Test
diff --git a/regression-test/data/row_binlog_p0/test_binlog_changes_syntax.out
b/regression-test/data/row_binlog_p0/test_binlog_changes_syntax.out
new file mode 100644
index 00000000000..f2fa93f1918
--- /dev/null
+++ b/regression-test/data/row_binlog_p0/test_binlog_changes_syntax.out
@@ -0,0 +1,212 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !dup_append_range --
+3 30 c y 0
+3 31 c2 \N 0
+4 40 \N z 0
+
+-- !dup_detail_range --
+3 30 c y 0
+3 31 c2 \N 0
+4 40 \N z 0
+
+-- !dup_min_delta_range --
+3 30 c y 0
+3 31 c2 \N 0
+4 40 \N z 0
+
+-- !dup_detail_start --
+3 30 0
+3 31 0
+4 40 0
+5 50 0
+
+-- !dup_detail_end --
+1 10 0
+2 20 0
+3 30 0
+3 31 0
+4 40 0
+
+-- !dup_detail_all --
+1 10 0
+2 20 0
+3 30 0
+3 31 0
+4 40 0
+5 50 0
+
+-- !dup_incr_empty --
+1 10 0
+2 20 0
+3 30 0
+3 31 0
+4 40 0
+5 50 0
+
+-- !dup_reversed --
+
+-- !dup_far_future --
+
+-- !dup_full_cover --
+1 10 0
+2 20 0
+3 30 0
+3 31 0
+4 40 0
+5 50 0
+
+-- !dup_binlog_tvf --
+1 10 0
+2 20 0
+3 30 0
+3 31 0
+4 40 0
+5 50 0
+
+-- !mow_append_range --
+2 20 b 0
+4 40 d 0
+5 50 e 0
+
+-- !mow_min_delta_range --
+1 10 a 2
+1 13 a3 3
+2 21 b1 0
+3 30 c 1
+4 40 d 0
+
+-- !mow_detail_range --
+1 10 a 2
+1 11 a1 2
+1 11 a1 3
+1 12 a2 2
+1 12 a2 3
+1 13 a3 3
+2 \N \N 2
+2 20 b 0
+2 20 b 1
+2 21 b1 3
+3 30 c 1
+4 40 d 0
+5 50 e 0
+5 50 e 1
+
+-- !mow_detail_start --
+1 10 2
+1 11 2
+1 11 3
+1 12 2
+1 12 3
+1 13 3
+2 \N 2
+2 20 0
+2 20 1
+2 21 3
+3 30 1
+4 40 0
+5 50 0
+5 50 1
+6 60 0
+
+-- !mow_detail_end --
+1 10 0
+1 10 2
+1 11 2
+1 11 3
+1 12 2
+1 12 3
+1 13 3
+2 \N 2
+2 20 0
+2 20 1
+2 21 3
+3 30 0
+3 30 1
+4 40 0
+5 50 0
+5 50 1
+
+-- !mow_detail_all --
+1 10 0
+1 10 2
+1 11 2
+1 11 3
+1 12 2
+1 12 3
+1 13 3
+2 \N 2
+2 20 0
+2 20 1
+2 21 3
+3 30 0
+3 30 1
+4 40 0
+5 50 0
+5 50 1
+6 60 0
+
+-- !mow_min_delta_all --
+1 13 0
+2 21 0
+4 40 0
+6 60 0
+
+-- !mow_incr_empty --
+1 13 0
+2 21 0
+4 40 0
+6 60 0
+
+-- !mow_reversed --
+
+-- !mow_full_cover --
+1 10 0
+1 10 2
+1 11 2
+1 11 3
+1 12 2
+1 12 3
+1 13 3
+2 \N 2
+2 20 0
+2 20 1
+2 21 3
+3 30 0
+3 30 1
+4 40 0
+5 50 0
+5 50 1
+6 60 0
+
+-- !seq_detail --
+1 100 2
+1 300 3
+
+-- !seq_min_delta --
+1 100 2
+1 300 3
+
+-- !seq_visible --
+300
+
+-- !part_detail --
+1 10 a 1000 2
+1 11 a 1000 2
+1 11 a 1000 3
+1 11 b 1000 2
+1 11 b 1000 3
+1 11 b 2000 3
+
+-- !part_min_delta --
+1 10 a 1000 2
+1 11 b 2000 3
+
+-- !part_visible --
+11 b 2000
+
+-- !part_append --
+
+-- !bitmap_min_delta --
+1 1,2 2
+1 3,4 3
+
diff --git
a/regression-test/suites/row_binlog_p0/test_binlog_changes_syntax.groovy
b/regression-test/suites/row_binlog_p0/test_binlog_changes_syntax.groovy
index b2212177cc8..e9e2f758cec 100644
--- a/regression-test/suites/row_binlog_p0/test_binlog_changes_syntax.groovy
+++ b/regression-test/suites/row_binlog_p0/test_binlog_changes_syntax.groovy
@@ -29,16 +29,15 @@ suite("test_binlog_changes_syntax", "nonConcurrent") {
def mowTable = "changes_mow"
def mowSeqTable = "changes_mow_seq"
def mowPartialTable = "changes_mow_partial"
+ def mowBitmapTable = "changes_mow_bitmap"
def incrTimeFormat = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
- // Helper: stable LSN sort accessor.
- def asStr = { row, idx -> row[idx] == null ? "null" : row[idx].toString() }
-
try {
sql "DROP TABLE IF EXISTS ${dupTable}"
sql "DROP TABLE IF EXISTS ${mowTable}"
sql "DROP TABLE IF EXISTS ${mowSeqTable}"
sql "DROP TABLE IF EXISTS ${mowPartialTable}"
+ sql "DROP TABLE IF EXISTS ${mowBitmapTable}"
// ============================================================
// 1. DUPLICATE table — multi-column + nullable + multi-rowset writes.
@@ -91,128 +90,97 @@ suite("test_binlog_changes_syntax", "nonConcurrent") {
// 1.1 APPEND_ONLY [dupT0, dupT1]: 3 in-window inserts including the
// re-inserted key=3.
- def dupAppendRange = sql """
+ order_qt_dup_append_range """
SELECT id, v1, v2, v3, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr('startTimestamp' = '${dupT0}',
"endTimestamp" = "${dupT1}",
"incrementType" = "APPEND_ONLY")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(3, dupAppendRange.size())
- assertEquals("3", asStr(dupAppendRange[0], 0))
- assertEquals("30", asStr(dupAppendRange[0], 1))
- assertEquals("c", asStr(dupAppendRange[0], 2))
- assertEquals("y", asStr(dupAppendRange[0], 3))
- assertEquals("0", asStr(dupAppendRange[0], 4))
- assertEquals("4", asStr(dupAppendRange[1], 0))
- assertEquals("null", asStr(dupAppendRange[1], 2))
- assertEquals("z", asStr(dupAppendRange[1], 3))
- assertEquals("3", asStr(dupAppendRange[2], 0))
- assertEquals("31", asStr(dupAppendRange[2], 1))
- assertEquals("c2", asStr(dupAppendRange[2], 2))
- assertEquals("null", asStr(dupAppendRange[2], 3))
// 1.2 DETAIL [dupT0, dupT1] must be identical to APPEND_ONLY for dup.
- def dupDetailRange = sql """
+ order_qt_dup_detail_range """
SELECT id, v1, v2, v3, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr('startTimestamp' = '${dupT0}',
"endTimestamp" = "${dupT1}",
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(dupAppendRange, dupDetailRange)
// 1.3 MIN_DELTA on dup table should be equivalent to APPEND_ONLY:
// dup table has no DELETE / UPDATE in binlog, so per-key folding
// degenerates to "all inserts kept as APPEND".
- def dupMinDeltaRange = sql """
+ order_qt_dup_min_delta_range """
SELECT id, v1, v2, v3, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr('startTimestamp' = '${dupT0}',
"endTimestamp" = "${dupT1}",
"incrementType" = "MIN_DELTA")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(dupAppendRange, dupMinDeltaRange)
// 1.4 startTimestamp only: covers in-window + late insert.
- def dupDetailStart = sql """
+ order_qt_dup_detail_start """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr('startTimestamp' = '${dupT0}',
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(4, dupDetailStart.size())
- assertEquals("5", asStr(dupDetailStart[3], 0))
- assertEquals("50", asStr(dupDetailStart[3], 1))
// 1.5 endTimestamp only: covers seed + in-window inserts.
- def dupDetailEnd = sql """
+ order_qt_dup_detail_end """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr("endTimestamp" = "${dupT1}",
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(5, dupDetailEnd.size())
- assertEquals("1", asStr(dupDetailEnd[0], 0))
- assertEquals("2", asStr(dupDetailEnd[1], 0))
// 1.6 No timestamps: equals @incr() and equals DETAIL of full binlog.
- def dupDetailAll = sql """
+ order_qt_dup_detail_all """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr("incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- def dupIncrEmpty = sql """
+ order_qt_dup_incr_empty """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr()
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(6, dupDetailAll.size())
- assertEquals(dupDetailAll, dupIncrEmpty)
// 1.7 Degenerate windows.
// - start > end: should yield empty.
- def dupReversed = sql """
+ order_qt_dup_reversed """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr('startTimestamp' = '${dupT1}',
"endTimestamp" = "${dupT0}",
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(0, dupReversed.size())
// - far-future start: empty.
- def dupFarFuture = sql """
+ order_qt_dup_far_future """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr('startTimestamp' = '2999-01-01 00:00:00',
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(0, dupFarFuture.size())
// - far-past start + far-future end: equivalent to full binlog.
- def dupFullCover = sql """
+ order_qt_dup_full_cover """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${dupTable}@incr('startTimestamp' = '1971-01-01 00:00:00',
"endTimestamp" = "2999-01-01 00:00:00",
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(dupDetailAll, dupFullCover)
// 1.8 Cross-check against binlog() TVF — DETAIL with full window must
// return the same rowset as the underlying binlog TVF for a dup
// table (since both expose only INSERT rows).
- def dupBinlogTvf = sql """
+ order_qt_dup_binlog_tvf """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM binlog("table" = "${dupTable}")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(dupBinlogTvf.size(), dupDetailAll.size())
- for (int i = 0; i < dupBinlogTvf.size(); i++) {
- assertEquals(asStr(dupBinlogTvf[i], 0), asStr(dupDetailAll[i], 0))
- assertEquals(asStr(dupBinlogTvf[i], 1), asStr(dupDetailAll[i], 1))
- }
// ============================================================
// 2. MoW table (UNIQUE KEY + enable_unique_key_merge_on_write
@@ -294,21 +262,16 @@ suite("test_binlog_changes_syntax", "nonConcurrent") {
// key=4 INSERT(40) is APPEND.
// key=5 INSERT(50) is APPEND (the later DELETE does not undo
// APPEND in APPEND_ONLY semantics).
- def mowAppendRange = sql """
+ order_qt_mow_append_range """
SELECT id, v1, v2, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr('startTimestamp' = '${mowT0}',
"endTimestamp" = "${mowT1}",
"incrementType" = "APPEND_ONLY")
ORDER BY id, __DORIS_BINLOG_LSN__
"""
- assertEquals(3, mowAppendRange.size())
- // Sort by id is enough to locate them.
- def appendIds = mowAppendRange.collect { asStr(it, 0) }.toSet()
- assertEquals(["2", "4", "5"] as Set, appendIds)
- mowAppendRange.each { assertEquals("0", asStr(it, 3)) }
// 2.2 MIN_DELTA [mowT0, mowT1]: per-key folded result.
- def mowMinDeltaRange = sql """
+ order_qt_mow_min_delta_range """
SELECT id, v1, v2, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr('startTimestamp' = '${mowT0}',
"endTimestamp" = "${mowT1}",
@@ -324,38 +287,6 @@ suite("test_binlog_changes_syntax", "nonConcurrent") {
// (2, 21, 'b1', 0) -- key=2 did not exist before mowT0
// (3, 30, 'c', 1)
// (4, 40, 'd', 0)
- assertEquals(5, mowMinDeltaRange.size())
- def mdByKey = [:]
- mowMinDeltaRange.each {
- def k = asStr(it, 0)
- mdByKey.computeIfAbsent(k, { _ -> [] }).add(it)
- }
- // key=1
- assertEquals(2, mdByKey["1"].size())
- def k1Ops = mdByKey["1"].collect { asStr(it, 3) }.toSet()
- assertEquals(["2", "3"] as Set, k1Ops)
- def k1Before = mdByKey["1"].find { asStr(it, 3) == "2" }
- def k1After = mdByKey["1"].find { asStr(it, 3) == "3" }
- assertEquals("10", asStr(k1Before, 1))
- assertEquals("13", asStr(k1After, 1))
- assertEquals("a3", asStr(k1After, 2))
- // key=2: did not exist before mowT0; insert+delete+insert collapses to
- // a net APPEND(21).
- assertEquals(1, mdByKey["2"].size())
- assertEquals("0", asStr(mdByKey["2"][0], 3))
- assertEquals("21", asStr(mdByKey["2"][0], 1))
- assertEquals("b1", asStr(mdByKey["2"][0], 2))
- // key=3: pure DELETE
- assertEquals(1, mdByKey["3"].size())
- assertEquals("1", asStr(mdByKey["3"][0], 3))
- assertEquals("30", asStr(mdByKey["3"][0], 1))
- // key=4: pure APPEND
- assertEquals(1, mdByKey["4"].size())
- assertEquals("0", asStr(mdByKey["4"][0], 3))
- assertEquals("40", asStr(mdByKey["4"][0], 1))
- // key=5: insert-then-delete inside window collapses to nothing.
- assertEquals(null, mdByKey["5"])
-
// 2.3 DETAIL [mowT0, mowT1]: every raw binlog row in the window.
// Each "INSERT into UNIQUE KEY MoW that hits an existing key"
emits
// a UPDATE_BEFORE/UPDATE_AFTER pair instead of a plain INSERT.
After
@@ -369,53 +300,35 @@ suite("test_binlog_changes_syntax", "nonConcurrent") {
// key=4: INSERT(40) = 1 row
// key=5: INSERT(50) + DELETE(50) = 2 rows
// Total = 6 + 4 + 1 + 1 + 2 = 14.
- def mowDetailRange = sql """
+ order_qt_mow_detail_range """
SELECT id, v1, v2, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr('startTimestamp' = '${mowT0}',
"endTimestamp" = "${mowT1}",
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(14, mowDetailRange.size())
- // Detail must contain every concrete value we ever wrote on key=1.
- def k1Vals = mowDetailRange.findAll { asStr(it, 0) == "1" }.collect {
asStr(it, 1) }.toSet()
- assertTrue(k1Vals.containsAll(["10", "11", "12", "13"] as Set))
// 2.4 startTimestamp only: includes the late (6,60).
- def mowDetailStart = sql """
+ order_qt_mow_detail_start """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr('startTimestamp' = '${mowT0}',
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(15, mowDetailStart.size())
- assertEquals("6", asStr(mowDetailStart[14], 0))
- assertEquals("60", asStr(mowDetailStart[14], 1))
- assertEquals("0", asStr(mowDetailStart[14], 2))
// 2.5 endTimestamp only: includes the seed (1,10) and (3,30).
- def mowDetailEnd = sql """
+ order_qt_mow_detail_end """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr("endTimestamp" = "${mowT1}",
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(16, mowDetailEnd.size())
- assertEquals("1", asStr(mowDetailEnd[0], 0))
- assertEquals("10", asStr(mowDetailEnd[0], 1))
- assertEquals("0", asStr(mowDetailEnd[0], 2))
- assertEquals("3", asStr(mowDetailEnd[1], 0))
- assertEquals("30", asStr(mowDetailEnd[1], 1))
- assertEquals("0", asStr(mowDetailEnd[1], 2))
- def mowDetailAll = sql """
+ order_qt_mow_detail_all """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr("incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- // 2 seed + 14 in-window + 1 late = 17.
- assertEquals(17, mowDetailAll.size())
-
// 2.6 MIN_DELTA across full binlog: per-key folding from binlog start
// (every key starts non-existent) to the final visible state.
// Even the "seed" inserts before mowT0 are inside the binlog
@@ -427,57 +340,35 @@ suite("test_binlog_changes_syntax", "nonConcurrent") {
// key=4: APPEND(40)
// key=5: SKIP
// key=6: APPEND(60)
- def mowMinDeltaAll = sql """
+ order_qt_mow_min_delta_all """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr("incrementType" = "MIN_DELTA")
ORDER BY id, __DORIS_BINLOG_OP__, v1
"""
- assertEquals(4, mowMinDeltaAll.size())
- def mdAllByKey = [:]
- mowMinDeltaAll.each {
- mdAllByKey.computeIfAbsent(asStr(it, 0), { _ -> [] }).add(it)
- }
- assertEquals(1, mdAllByKey["1"].size())
- assertEquals("0", asStr(mdAllByKey["1"][0], 2))
- assertEquals("13", asStr(mdAllByKey["1"][0], 1))
- // key=2 over the full timeline: the key did not exist before mowT0,
- // so the net effect is APPEND(21). MIN_DELTA op = 0.
- assertEquals(1, mdAllByKey["2"].size())
- assertEquals("0", asStr(mdAllByKey["2"][0], 2))
- assertEquals("21", asStr(mdAllByKey["2"][0], 1))
- // key=3: full-timeline insert(30) + delete folds to SKIP.
- assertEquals(null, mdAllByKey["3"])
- assertEquals("0", asStr(mdAllByKey["4"][0], 2))
- assertEquals(null, mdAllByKey["5"])
- assertEquals("0", asStr(mdAllByKey["6"][0], 2))
- assertEquals("60", asStr(mdAllByKey["6"][0], 1))
// 2.7 Empty timestamps: min-delta binlog and equivalence with @incr().
- def mowIncrEmpty = sql """
+ order_qt_mow_incr_empty """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr()
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(mowMinDeltaAll, mowIncrEmpty)
// 2.8 Degenerate window: start > end -> empty.
- def mowReversed = sql """
+ order_qt_mow_reversed """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr('startTimestamp' = '${mowT1}',
"endTimestamp" = "${mowT0}",
"incrementType" = "MIN_DELTA")
"""
- assertEquals(0, mowReversed.size())
// 2.9 Far-past + far-future window covers everything.
- def mowFullCover = sql """
+ order_qt_mow_full_cover """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${mowTable}@incr('startTimestamp' = '1971-01-01 00:00:00',
"endTimestamp" = "2999-01-01 00:00:00",
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(mowDetailAll, mowFullCover)
// ============================================================
// 3. MoW table with sequence column.
@@ -531,33 +422,25 @@ suite("test_binlog_changes_syntax", "nonConcurrent") {
// 3.1 DETAIL only captures physically-applied writes. The out-of-order
// write (seq=3) is rejected and never lands in binlog, so only the
// accepted update (seq=10) shows up.
- def seqDetail = sql """
+ order_qt_seq_detail """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${mowSeqTable}@incr('startTimestamp' = '${seqT0}',
"endTimestamp" = "${seqT1}",
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- // Only the accepted update produces 1 BEFORE/AFTER pair = 2 rows.
- assertEquals(2, seqDetail.size())
// 3.2 MIN_DELTA collapses to BEFORE(100) -> AFTER(latest visible
value).
- def seqMinDelta = sql """
+ order_qt_seq_min_delta """
SELECT id, v1, __DORIS_BINLOG_OP__
FROM ${mowSeqTable}@incr('startTimestamp' = '${seqT0}',
"endTimestamp" = "${seqT1}",
"incrementType" = "MIN_DELTA")
ORDER BY __DORIS_BINLOG_OP__
"""
- assertEquals(2, seqMinDelta.size())
- // Op 2 is UPDATE_BEFORE, op 3 is UPDATE_AFTER.
- assertEquals("2", asStr(seqMinDelta[0], 2))
- assertEquals("100", asStr(seqMinDelta[0], 1))
- assertEquals("3", asStr(seqMinDelta[1], 2))
- // The visible AFTER value must be the latest visible row. Read it back
- // from the table to avoid hard-coding implementation details.
- def visible = sql "SELECT v1 FROM ${mowSeqTable} WHERE id = 1"
- assertEquals(asStr(visible[0], 0), asStr(seqMinDelta[1], 1))
+ order_qt_seq_visible """
+ SELECT v1 FROM ${mowSeqTable} WHERE id = 1
+ """
// ============================================================
// 4. MoW table with partial column update. Each partial update
@@ -597,44 +480,77 @@ suite("test_binlog_changes_syntax", "nonConcurrent") {
def partT1 = incrTimeFormat.format(new Date())
// 4.1 DETAIL: 3 partial updates -> 6 raw binlog rows.
- def partDetail = sql """
+ order_qt_part_detail """
SELECT id, v1, v2, v3, __DORIS_BINLOG_OP__
FROM ${mowPartialTable}@incr('startTimestamp' = '${partT0}',
"endTimestamp" = "${partT1}",
"incrementType" = "DETAIL")
ORDER BY __DORIS_BINLOG_LSN__
"""
- assertEquals(6, partDetail.size())
// 4.2 MIN_DELTA: collapses to BEFORE/AFTER pair for key=1, where AFTER
// reflects the merged final visible row.
- def partMinDelta = sql """
+ order_qt_part_min_delta """
SELECT id, v1, v2, v3, __DORIS_BINLOG_OP__
FROM ${mowPartialTable}@incr('startTimestamp' = '${partT0}',
"endTimestamp" = "${partT1}",
"incrementType" = "MIN_DELTA")
ORDER BY __DORIS_BINLOG_OP__
"""
- assertEquals(2, partMinDelta.size())
- assertEquals("2", asStr(partMinDelta[0], 4))
- assertEquals("10", asStr(partMinDelta[0], 1))
- assertEquals("a", asStr(partMinDelta[0], 2))
- assertEquals("1000", asStr(partMinDelta[0], 3))
- assertEquals("3", asStr(partMinDelta[1], 4))
- def partVisible = sql "SELECT v1, v2, v3 FROM ${mowPartialTable} WHERE
id = 1"
- assertEquals(asStr(partVisible[0], 0), asStr(partMinDelta[1], 1))
- assertEquals(asStr(partVisible[0], 1), asStr(partMinDelta[1], 2))
- assertEquals(asStr(partVisible[0], 2), asStr(partMinDelta[1], 3))
+ order_qt_part_visible """
+ SELECT v1, v2, v3 FROM ${mowPartialTable} WHERE id = 1
+ """
// 4.3 APPEND_ONLY in the same window: pure partial updates on an
// existing key produce no APPEND rows.
- def partAppend = sql """
+ order_qt_part_append """
SELECT id, __DORIS_BINLOG_OP__
FROM ${mowPartialTable}@incr('startTimestamp' = '${partT0}',
"endTimestamp" = "${partT1}",
"incrementType" = "APPEND_ONLY")
"""
- assertEquals(0, partAppend.size())
+
+ // ============================================================
+ // 5. MoW table with BITMAP NOT NULL.
+ // This specifically exercises MIN_DELTA UPDATE_BEFORE/AFTER on a
+ // complex-type value column. Before the Column.java fix, the
+ // UPDATE_BEFORE row read from nullable __BEFORE__b__ would be
+ // copied into the non-nullable after-slot and BE would crash with
+ // "Bad cast from type ColumnNullable to ColumnComplexType<BITMAP>".
+ // ============================================================
+ sql """
+ CREATE TABLE ${mowBitmapTable} (
+ id BIGINT,
+ b BITMAP NOT NULL
+ ) ENGINE=OLAP
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "light_schema_change" = "true",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW",
+ "binlog.need_historical_value" = "true"
+ )
+ """
+ sql "INSERT INTO ${mowBitmapTable} VALUES (1,
BITMAP_FROM_STRING('1,2'))"
+ sql "sync"
+ sleep(1200)
+ def bitmapT0 = incrTimeFormat.format(new Date())
+ sleep(1200)
+ sql "INSERT INTO ${mowBitmapTable} VALUES (1,
BITMAP_FROM_STRING('3,4'))"
+ sql "sync"
+ sleep(1200)
+ def bitmapT1 = incrTimeFormat.format(new Date())
+
+ order_qt_bitmap_min_delta """
+ SELECT id, BITMAP_TO_STRING(b), __DORIS_BINLOG_OP__
+ FROM ${mowBitmapTable}@incr('startTimestamp' = '${bitmapT0}',
+ "endTimestamp" = "${bitmapT1}",
+ "incrementType" = "MIN_DELTA")
+ ORDER BY __DORIS_BINLOG_OP__
+ """
} finally {
sql "DROP DATABASE IF EXISTS test_binlog_changes_syntax_db"
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]