Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ public void open(String serializedTable, Map<String, String> hadoopConfig,
this.bucketMode = table.bucketMode();

CoreOptions coreOptions = CoreOptions.fromMap(table.options());
this.writeSchema = PaimonWriteSchema.create(
table.rowType(), columnNames, changelogWrite);
this.writeSchema = PaimonWriteSchema.create(table.rowType(), columnNames, changelogWrite,
PaimonWriteSchema.charPadding(table.options()), coreOptions.rowkindField().orElse(null));
this.arrowAdapter = new PaimonArrowBatchAdapter(
writeSchema.inputType(), ZoneId.of(timeZone), allocator);
validateWriteColumnsForMergeEngine(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,27 @@

package org.apache.doris.paimon;

import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.GenericArray;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalArray;
import org.apache.paimon.data.InternalMap;
import org.apache.paimon.data.InternalRow;
import org.apache.paimon.types.ArrayType;
import org.apache.paimon.types.CharType;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataType;
import org.apache.paimon.types.MapType;
import org.apache.paimon.types.RowKind;
import org.apache.paimon.types.RowType;
import org.apache.paimon.types.TinyIntType;
import org.apache.paimon.utils.BinaryStringUtils;
import org.apache.paimon.utils.DefaultValueUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
* Immutable mapping from Doris input columns to a Paimon table row.
Expand All @@ -41,8 +51,19 @@ final class PaimonWriteSchema {
static final byte INSERT_OPERATION = 0;
static final byte UPDATE_OPERATION = 1;
static final byte DELETE_OPERATION = 2;
/**
* Table option through which FE asks the writer to pad CHAR values, set from the session variable
* enable_paimon_write_char_padding. Keep in sync with PaimonWriteBinding.DORIS_WRITE_CHAR_PADDING in
* fe-connector-paimon.
*/
static final String DORIS_WRITE_CHAR_PADDING = "doris.write.char-padding";

private final InternalRow.FieldGetter[] fieldGetters;
/**
* Per input column, pads the CHAR values it holds; null when it holds none, when padding is off, or for the
* table's rowkind.field.
*/
private final CharPadder[] charPadders;
private final RowType inputType;
/** Maps Doris input-column position → Paimon table-schema position. */
private final int[] tableFieldIndexes;
Expand All @@ -52,14 +73,15 @@ final class PaimonWriteSchema {
private final int tableFieldCount;

private PaimonWriteSchema(int[] tableFieldIndexes, int[] omittedDefaultFieldIndexes,
Object[] omittedDefaultValues, int tableFieldCount, RowType inputType) {
Object[] omittedDefaultValues, int tableFieldCount, RowType inputType, CharPadder[] charPadders) {
this.fieldGetters = new InternalRow.FieldGetter[inputType.getFieldCount()];
for (int i = 0; i < fieldGetters.length; i++) {
// Getters describe the transport row, not the target table. Transport fields are
// nullable so an invalid Doris NULL survives projection and reaches Paimon's
// authoritative table-schema validation.
this.fieldGetters[i] = InternalRow.createFieldGetter(inputType.getTypeAt(i), i);
}
this.charPadders = charPadders;
this.inputType = inputType;
this.tableFieldIndexes = tableFieldIndexes;
this.omittedDefaultFieldIndexes = omittedDefaultFieldIndexes;
Expand All @@ -82,12 +104,29 @@ static PaimonWriteSchema create(RowType tableType, String[] columnNames) {

static PaimonWriteSchema create(
RowType tableType, String[] columnNames, boolean changelogWrite) {
return create(tableType, columnNames, changelogWrite, false);
}

static PaimonWriteSchema create(
RowType tableType, String[] columnNames, boolean changelogWrite, boolean charPadding) {
return create(tableType, columnNames, changelogWrite, charPadding, null);
}

/**
* @param charPadding whether to pad the CHAR values of inserted rows as Paimon's cast to CHAR does; see
* {@link #charPadder}
* @param rowKindField the table's {@code rowkind.field}, or null. Its value is not padded: Paimon takes the row
* kind from it as it is ({@code RowKind.fromShortString}), and a padded '+I' is no row kind.
*/
static PaimonWriteSchema create(RowType tableType, String[] columnNames, boolean changelogWrite,
boolean charPadding, String rowKindField) {
if (columnNames == null || columnNames.length == 0) {
throw new IllegalArgumentException(
"PaimonJniWriter requires explicit column names");
}

int[] tableFieldIndexes = new int[columnNames.length];
CharPadder[] charPadders = new CharPadder[columnNames.length];
List<DataField> inputFields = new ArrayList<>(columnNames.length);
boolean[] specifiedFields = new boolean[tableType.getFieldCount()];
for (int i = 0; i < columnNames.length; i++) {
Expand Down Expand Up @@ -118,6 +157,9 @@ static PaimonWriteSchema create(
// Paimon writer return its normal constraint diagnostic instead of reading a default
// primitive value from a non-null getter.
inputFields.add(field.newType(field.type().nullable()));
if (charPadding && !field.name().equals(rowKindField)) {
charPadders[i] = charPadder(field.type());
}
}

int[] omittedDefaultFieldIndexes = new int[tableType.getFieldCount()];
Expand All @@ -139,7 +181,16 @@ static PaimonWriteSchema create(
Arrays.copyOf(omittedDefaultFieldIndexes, omittedDefaultCount),
Arrays.copyOf(omittedDefaultValues, omittedDefaultCount),
tableType.getFieldCount(),
new RowType(inputFields));
new RowType(inputFields),
charPadders);
}

/**
* Whether FE asked for CHAR padding through {@link #DORIS_WRITE_CHAR_PADDING}. An FE that predates the option
* does not set it, and does not pad its static partition values either.
*/
static boolean charPadding(Map<String, String> tableOptions) {
return Boolean.parseBoolean(tableOptions.get(DORIS_WRITE_CHAR_PADDING));
}

/** Paimon input fields in the exact order transported by Arrow C Data. */
Expand All @@ -163,13 +214,135 @@ GenericRow tableRow(InternalRow columnValues) {
row.setRowKind(toRowKind(value));
continue;
}
// Only an inserted row is padded; its row kind, if any, is the first input column. An UPDATE or
// DELETE row carries the key Doris read back from the table, which has to stay byte-equal to the
// stored key for Paimon to match it, padded or not.
if (charPadders[i] != null && value != null && row.getRowKind() == RowKind.INSERT) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve CHAR-typed rowkind.field control tokens

Paimon permits rowkind.field to name a CHAR column: its RowKindGenerator accepts CHARACTER_STRING and parses the exact value with RowKind.fromShortString. For a table with (id INT, rk CHAR(3)), primary key id and rowkind.field=rk, an ordinary INSERT ... VALUES (1, '+I') has RowKind.INSERT here, so this branch changes '+I' into '+I '. The SDK then rejects it as an unsupported row-kind token; the same happens to '+U', '-U' and '-D'. These writes worked before default-on padding. Pass the configured control-field identity into the write schema and preserve its token while padding data fields; add a CHAR control-column regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b584341. PaimonWriteSchema now leaves the column that the table names as rowkind.field unpadded (PaimonJniWriter passes CoreOptions.rowkindField()), and pads the other CHAR columns as before. Before the fix, INSERT INTO t VALUES (3, '+I', 'c', 30) into a table with rk CHAR(3) and 'rowkind.field' = 'rk' failed with Unsupported short string '+I ' for row kind.

Tests: PaimonWriteSchemaTest.testCharPaddingLeavesTheRowKindFieldAlone; and test_paimon_write_char_padding writes two '+I' rows and then a '-D' row into such a table, and checks that the '-D' row deletes key 1 while the other CHAR column is padded.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Resolve rowkind.field before deciding whether to pad keys

An ordinary INSERT starts with GenericRow's INSERT kind, so this branch pads CHAR data keys even when rowkind.field carries -D, -U or +U. The SDK resolves that operation later in TableWriteImpl.writeAndReturn, using the already-padded key. For a Parquet table (c CHAR(3), rk VARCHAR(2), v INT) keyed by c, with rowkind.field=rk and bucket=1, seed ('k','+I',1) with padding off, then ('k','+I',2) with padding on. Still padding on, INSERT INTO t SELECT c, '-D', v FROM t WHERE c = 'k' deletes the unrelated stored 'k ' row and leaves the selected legacy 'k' row. Resolve the effective control-field operation before padding and preserve non-insert keys as for changelog rows; add a CHAR-key control-event regression. This is distinct from the existing token-padding thread: the token is preserved here, but its delete key is changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f2c45d7e38c. This reproduces: with a key 'k' written with padding off and 'k ' written with it on, INSERT INTO t SELECT c, '-D', v FROM t WHERE c = 'k' deleted 'k ' and left 'k', and the same -D for a key 'j' written unpadded, with no padded twin, deleted nothing. Doris refuses UPDATE, DELETE and MERGE on a table with a rowkind.field, so such an INSERT is how Doris deletes rows of it, with keys read back from the table.

The writer now goes by the kind Paimon will give the row: a row of such a table is padded only when its rowkind.field value is '+I', whether Doris writes that value or the field's default supplies it. A '-D', '-U' or '+U' row carries a key to match, as Doris's own UPDATE and DELETE rows do, and is written as Doris holds it, so both -D rows above now delete the row they were read from. A short key written as a literal in such a row is not padded either, and so does not reach a key that Spark or Flink wrote padded, as on master; it has to be written full length.

Tests: PaimonWriteSchemaTest.testRowKindFieldValueDecidesWhetherARowIsPadded ('+I' rows are padded; '-D', '-U', '+U' and NULL rows are not; a default '+I' or '-D' decides the same way); padding by the row's own kind alone, or looking the value up in the input only, turns it red. test_paimon_write_char_padding deletes an unpadded and a padded key of a CHAR-keyed rowkind.field table with -D rows read back from it.

value = charPadders[i].pad(value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Normalize CHAR keys before MERGE matching and cardinality checks

Padding here changes primary-key identity after ConnectorChangelogPlanBuilder.addCardinalityChecks has validated it. With an empty (c CHAR(3), v INT) primary-key target, MERGE source rows (CAST('k' AS CHAR(3)), 1) and (CAST('k ' AS CHAR(3)), 2) using ON t.c = s.c WHEN NOT MATCHED THEN INSERT (c,v) VALUES (s.c,s.v) follow this reduced plan:

JNI sink: pad(c)
  AssertTrue(count <= 1)
    Window(count PARTITION BY c)
      Project(op=INSERT, c=s.c, v=s.v)
        Join(target, source, t.c=s.c)

Doris preserves these short CHAR values and compares their bytes, so both window counts are 1. This branch then turns both keys into 'k ', and Paimon's deduplicate writer commits one surviving row instead of the required duplicate-insert error. A source 'k' can likewise overwrite an existing 'k ' target through a NOT MATCHED-only MERGE. Normalize keys before matching/checking while preserving matched legacy keys, or reject this CHAR-key MERGE combination until it is supported; add both regressions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 45e6e5d. This reproduced on a local cluster. Into an empty table keyed by c CHAR(3), MERGE INTO t USING s ON t.c = s.c WHEN NOT MATCHED THEN INSERT (c, v) VALUES (s.c, s.v) with the source rows ('k', 1) and ('k ', 2) committed and left the single row 'k '. With a row 'k ' that a padded INSERT had written, a NOT MATCHED-only MERGE of ('k', 9) replaced it, while a MATCHED-only MERGE of the same source changed nothing.

MERGE matches its source rows, and checks the keys it inserts for duplicates, by the key as Doris holds it, unpadded, and the writer padded the keys of the rows MERGE inserts afterwards. Normalizing the keys during planning would mean comparing CHAR keys padded in ON, the read-side change this PR leaves out: Doris compares CHAR values as they are, as it does for the data Spark writes. So PaimonWriteSchema now fails a row that a MERGE inserts when one of its CHAR primary-key values is shorter than the column, which is the only case where padding changes a key, and PaimonJniWriter passes it the table's primary keys. A full-length key is written as it is, so MERGE's matching and its duplicate-insert check see the stored key. A MERGE can pad the key itself, ON t.c = rpad(s.c, 3, ' ') ... INSERT (c, v) VALUES (rpad(s.c, 3, ' '), s.v): that matches the padded row, and the duplicate-insert check then rejects 'm' and 'm '. With enable_paimon_write_char_padding = false, MERGE writes the keys unpadded, as before.

Tests:

  • PaimonWriteSchemaTest.testMergeInsertOfAShortCharKeyFails ('k', 'k ' and '中中' into a CHAR(3) key), testMergeInsertPadsTheRestOfAFullLengthKeyRow (a full-length key, '中中中' included, and a longer one are written as they are, and the row's other CHAR column is padded), and testShortCharKeysThatMergeDoesNotInsertAreWrittenAsBefore (UPDATE and DELETE rows, padding off, and a plain INSERT). Removing the check, counting bytes, or checking UPDATE and DELETE rows too turns them red.
  • test_paimon_write_char_padding now runs a MERGE from a Doris table: the short key fails and nothing is written; the rpad MERGE inserts 'k ' and, run again, matches it; the rpad MERGE of 'm' and 'm ' fails the duplicate-insert check; and with padding off MERGE writes 'm' and 'm ' as two rows. With the jars from before this fix the suite fails at the MERGE that must fail.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve ORC CHAR key identity for UPDATE and DELETE

Padding inserted keys breaks subsequent row-level DML on ORC tables because both native and SDK ORC readers strip CHAR trailing spaces. For (c CHAR(3), v INT) with primary-key=c, bucket=4 and file.format=orc, INSERT INTO t VALUES ('k',1) now supplies 'k ' to Paimon's bucket extractor, putting the row in bucket 1. DELETE FROM t WHERE v=1 reads back 'k'; this method leaves DELETE keys unpadded, so the SDK writes its tombstone to bucket 2 and the selected row survives although DELETE commits. UPDATE similarly writes another bucket. Previously Doris's short INSERT and its mutations used the same unpadded key/bucket. GATHER consolidates writers but cannot reconcile these SDK buckets. Preserve stored key/bucket identity through the ORC mutation read path, or gate this combination until supported; cover ORC INSERT followed by UPDATE/DELETE with multiple buckets.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f2c45d7e38c. This reproduces, and for most short keys rather than one pair: Paimon stores CHAR(n) as ORC char(n), and ORC pads every CHAR value it writes (CharTreeWriter) and strips the trailing spaces from one it reads (CharTreeReader), as Doris's ORC reader does too. Padding in the writer therefore changed nothing that ORC stores, only the bucket a CHAR key is hashed to, and UPDATE and DELETE, which read the key back without its spaces, hashed another one. On a local cluster, in an ORC table (c CHAR(3), v INT) keyed by c with bucket = 4, DELETE ... WHERE v IN (1, 3, 4, 5) of the keys 'k', 'x', 'm' and 'zz' that Doris had just inserted returned COMMITTED and removed none of them. With padding off, and in a Parquet table with padding on, it removed all four.

The writer now pads only the partition columns of a table that writes ORC files, by file.format or at some level of file.format.per.level (PaimonWriteSchema.writesOrcFiles). A partition value is read back as it is stored, so padding it still makes Doris and Spark write one partition, and UPDATE and DELETE of a CHAR-partitioned ORC table were already correct. The ORC files hold the same bytes as before, and a key Doris writes into such a table gets the bucket it had before this PR, so UPDATE, DELETE and MERGE reach it, as they reach the keys an earlier Doris wrote; MERGE's short-key check does not apply to a key that is not padded. While padding is on, FE still writes a fixed-bucket table with a CHAR key through one writer, since it does not look at the file format; that only costs parallelism. One case stays as it is on master: a short CHAR key that Spark or Flink wrote padded into a multi-bucket ORC table sits in another bucket than the one Doris's UPDATE and DELETE hash from the key they read back, so they still miss it. Reaching it needs the bucket of the row Doris read, which is a change of its own.

Tests: PaimonWriteSchemaTest.testOrcTablePadsOnlyItsPartitionValues (an ORC table pads its CHAR partition value but not its CHAR key or map keys, and a MERGE that inserts a short partition value still fails) and testOrcFilesAreReadFromTheTableFormatOptions; padding every column of an ORC table, or ignoring file.format.per.level, turns them red. test_paimon_write_char_padding now deletes four and updates two of six keys that Doris inserted into a CHAR(3)-keyed ORC table with bucket = 4.

}
// Actual Doris input is applied last so an explicit NULL remains distinct
// from an omitted field and retains Paimon's writer-side semantics.
row.setField(tableFieldIndexes[i], value);
}
return row;
}

/**
* Returns the padder of a value of {@code type}, or null when the type holds no CHAR. A CHAR value is padded
* with spaces to the type length in code points, as Paimon's cast to CHAR pads it
* ({@code BinaryStringUtils.toCharacterString}), and so are the CHAR values nested in an ARRAY, a MAP or a
* ROW. Unlike that cast, a longer value is kept: Doris has already cut it to the length or kept it, as the
* session asks.
*/
private static CharPadder charPadder(DataType type) {
switch (type.getTypeRoot()) {
case CHAR: {
int length = ((CharType) type).getLength();
return value -> padChar((BinaryString) value, length);
}
case ARRAY: {
DataType elementType = ((ArrayType) type).getElementType();
CharPadder elementPadder = charPadder(elementType);
if (elementPadder == null) {
return null;
}
InternalArray.ElementGetter elementGetter = InternalArray.createElementGetter(elementType.nullable());
return value -> padArray((InternalArray) value, elementGetter, elementPadder);
}
case MAP: {
MapType mapType = (MapType) type;
CharPadder keyPadder = charPadder(mapType.getKeyType());
CharPadder valuePadder = charPadder(mapType.getValueType());
if (keyPadder == null && valuePadder == null) {
return null;
}
InternalArray.ElementGetter keyGetter =
InternalArray.createElementGetter(mapType.getKeyType().nullable());
InternalArray.ElementGetter valueGetter =
InternalArray.createElementGetter(mapType.getValueType().nullable());
return value -> {
InternalMap map = (InternalMap) value;
return new PaddedMap(
keyPadder == null ? map.keyArray() : padArray(map.keyArray(), keyGetter, keyPadder),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reject padded map-key collisions that Avro cannot preserve

Retaining both padded key-array entries does not preserve them through the supported Avro format. For a table with m MAP<CHAR(2), INT> and file.format=avro, inserting map('k',1,'k ',2) turns the two distinct keys into two identical 'k ' keys here. Paimon's Avro writer emits both entries, but its generic MapReader reconstructs a HashMap with put; INT values select that reader, so subsequent reads return one entry and silently discard an original value. Before this padding change both distinct keys survive. The new unit test checks duplicate preservation only in tableRow, before format serialization/readback. Reject normalized key collisions when the format cannot preserve them, or use an entry-preserving representation, and add an Avro CHAR-map round-trip regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f2c45d7e38c. This reproduces: map('k', 1, 'k ', 2) written into a MAP<CHAR(2), INT> column read back as {"k ":2} from an Avro table, and as a map holding the key 'k ' twice from a Parquet one. A map holds one entry per key, and padding is what made these two keys one, so the writer now fails such a row (PaimonWriteSchema.padMapKeys). The message names the column and the two keys, and asks for distinct keys or for padding to be turned off. Keys that were equal before padding are kept as they are, as before, and keys that stay distinct are padded. Spark, with the paimon-spark extensions, rejects the same insert with [DUPLICATED_MAP_KEY] Duplicate map key k was found. An ORC table, which is no longer padded (see the ORC thread), gets both keys as before; ORC pads them itself, as on master.

Tests: PaimonWriteSchemaTest.testMapKeysThatPaddingMakesEqualFailTheRow; removing the check, or failing keys that were equal before padding too, turns it red. test_paimon_write_char_padding writes such a map into an Avro table, which fails with padding on and keeps both keys with it off, and a map of distinct keys, which is padded. With the jars from before this commit the suite fails at that check, its first new one.

valuePadder == null
? map.valueArray() : padArray(map.valueArray(), valueGetter, valuePadder));
};
}
case ROW: {
List<DataField> fields = ((RowType) type).getFields();
CharPadder[] fieldPadders = new CharPadder[fields.size()];
InternalRow.FieldGetter[] fieldGetters = new InternalRow.FieldGetter[fields.size()];
boolean holdsChar = false;
for (int i = 0; i < fields.size(); i++) {
fieldPadders[i] = charPadder(fields.get(i).type());
fieldGetters[i] = InternalRow.createFieldGetter(fields.get(i).type().nullable(), i);
holdsChar |= fieldPadders[i] != null;
}
if (!holdsChar) {
return null;
}
return value -> {
InternalRow nested = (InternalRow) value;
GenericRow padded = new GenericRow(fieldGetters.length);
for (int i = 0; i < fieldGetters.length; i++) {
Object field = fieldGetters[i].getFieldOrNull(nested);
padded.setField(i, field == null || fieldPadders[i] == null
? field : fieldPadders[i].pad(field));
}
return padded;
};
}
default:
return null;
}
}

private static BinaryString padChar(BinaryString value, int length) {
int padLength = length - value.numChars();
return padLength > 0 ? BinaryStringUtils.concat(value, BinaryString.blankString(padLength)) : value;
}

private static InternalArray padArray(InternalArray array, InternalArray.ElementGetter elementGetter,
CharPadder elementPadder) {
Object[] elements = new Object[array.size()];
for (int i = 0; i < elements.length; i++) {
Object element = elementGetter.getElementOrNull(array, i);
elements[i] = element == null ? null : elementPadder.pad(element);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Account for expanded nested CHAR values before materializing them

This loop retains all padded values on the Java heap before writer.write can apply the managed-page budget. A supported Paimon ARRAY<CHAR(65535)> containing 4096 copies of 'a' arrives as roughly 20 KiB of Arrow data/offsets (CHAR>255 maps to Doris STRING), but the loop retains about 256 MiB of new byte arrays: blankString allocates padding and concat allocates each result. These allocations bypass both ArrowMemoryPool and PaimonJniMemoryManager, so even a writer/query with a 64 MiB budget materializes the full 256 MiB first. Increasing the width/element count or running concurrent writers can exhaust the shared JVM heap instead of failing at the query budget. Reserve and charge the expanded size before allocation, or perform padding in bounded, Doris-tracked storage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. The padded value is the value Paimon stores: an ARRAY<CHAR(65535)> with 4096 'a' elements is a 256 MiB row once padded, Paimon serializes that row when it writes it, and Spark and Flink build the same row in their JVMs. The writer pads one row at a time, and the padded values are garbage once writer.write has taken the row. The amplification comes from the CHAR length the table was declared with; Doris itself does not create a CHAR longer than 255 and reads a longer Paimon CHAR as STRING. Charging the padded bytes to the query would need a new bridge from these Java heap allocations to Doris's memory tracker, which is out of proportion to this case.

}
return new GenericArray(elements);
}

/** Pads the CHAR values held by one non-null value of an input column. */
private interface CharPadder {
Object pad(Object value);
}

/** A map over padded key and value arrays. Unlike GenericMap it keeps every entry of the input map. */
private static final class PaddedMap implements InternalMap {
private final InternalArray keys;
private final InternalArray values;

private PaddedMap(InternalArray keys, InternalArray values) {
this.keys = keys;
this.values = values;
}

@Override
public int size() {
return keys.size();
}

@Override
public InternalArray keyArray() {
return keys;
}

@Override
public InternalArray valueArray() {
return values;
}
}

private static RowKind toRowKind(Object operation) {
if (!(operation instanceof Byte)) {
throw new IllegalArgumentException("Paimon row change operation must be a TINYINT");
Expand Down
Loading
Loading