morningman opened a new pull request, #69083:
URL: https://github.com/apache/doris/pull/69083

   ### What problem does this PR solve?
   
   Issue Number: None
   
   Related PR: #69045
   
   Problem Summary:
   
   **In short.** Doris wrote CHAR(n) values into Paimon tables without padding 
them to n characters, while Spark and Flink pad them, so a primary key that 
Doris wrote and the same key written by Spark or Flink were two rows. This PR 
pads the CHAR values of inserted rows, nested values and static partition 
values included, as Paimon's own cast pads them, behind a new session variable 
`enable_paimon_write_char_padding` that is on by default. It came up in the 
review of #69045; branch-4.1 and branch-4.2 write CHAR values unpadded too.
   
   **Background.** Paimon's write path stores a value as it is given: it 
neither pads nor checks the length of a CHAR value, and it compares primary 
keys and hashes buckets by bytes. The engines pad before they write: Spark with 
the paimon-spark extensions (`PaimonAnalysis` → `CharVarcharUtils`, `rpad`) and 
Flink with an implicit cast (`CharVarCharTrimPadCastRule`), nested CHAR values 
included. Paimon's own cast to CHAR (`BinaryStringUtils.toCharacterString`, 
used for schema evolution and default values) pads the same way, counting code 
points. Doris writes Paimon through the JNI writer (`PaimonJniWriter`, rows 
built by `PaimonWriteSchema.tableRow`); the FFI writer backend is a stub that 
FE never selects.
   
   **The problem, and what it cost.** On a local cluster with a file:// Paimon 
catalog, Parquet tables created through the Paimon API:
   
   | Case | master | with this PR |
   |---|---|---|
   | Doris inserts `'k'` into a CHAR(3) primary key, then the Paimon API writes 
`'k  '`, which is what Spark and Flink write for `'k'` | 2 rows: `[k]`, `[k  ]` 
| 1 row: `[k  ]` |
   | Doris inserts `['a']`, `map('k', 'v')` and `named_struct('code', 'a', 'n', 
1)` into `ARRAY<CHAR(3)>`, `MAP<CHAR(2), CHAR(3)>` and `ROW<code CHAR(3), n 
INT>` | `[a]`, `[k] -> [v]`, `[a]` | `[a  ]`, `[k ] -> [v  ]`, `[a  ]` |
   | `INSERT OVERWRITE ... PARTITION (p = 'x')` and `INSERT ... VALUES (2, 
'y')` into a CHAR(3) partition column | partitions `[x]`, `[y]` | partitions 
`[x  ]`, `[y  ]` |
   
   A table that Doris and Spark or Flink both wrote got duplicate primary keys, 
and rows of one key could land in different partitions or buckets.
   
   **How this PR fixes it.**
   
   - **Rows.** `PaimonWriteSchema` pads every CHAR value of an inserted row 
with spaces to the column length in code points, as Paimon's cast pads it, and 
also the CHAR values nested in ARRAY, MAP and ROW. It builds a padder once per 
column whose type holds a CHAR. Unlike Paimon's cast it never cuts a longer 
value, because Doris has already decided that: BindSink cuts it when the 
session truncates strings on insert and keeps it otherwise. A map keeps every 
entry, as before.
   - **UPDATE and DELETE.** Their rows are not padded. They carry the key Doris 
read back from the table, which has to stay byte-equal to the stored key for 
Paimon to match it: padding it would leave a row that an earlier Doris wrote 
unpadded neither updated nor deleted. The values an UPDATE sets are written as 
before.
   - **Static partition values.** `PaimonWriteBinding.resolveStaticPartition` 
pads the static partition value of a CHAR column the same way. Paimon parses 
that value without padding it (`TypeUtils.castFromString`), so with only the 
rows padded the overwrite would name another partition than the rows reach, and 
the commit would fail.
   - **Switch.** The new session variable `enable_paimon_write_char_padding` 
(default true, forwarded with the statement) turns padding off, for a table 
whose CHAR keys an earlier Doris wrote unpadded. FE passes it to the BE writer 
as the option `doris.write.char-padding` on the table it serializes for the 
writer, the table that already carries the write options FE sets, such as 
`dynamic-partition-overwrite`; the table the FE committer keeps does not get 
the option. So the change needs no thrift field and no BE C++ change. A writer 
that finds no such option, from an FE without this change, does not pad, which 
matches that FE's unpadded static partition values. BEs are upgraded before 
FEs, so a new FE meets new BEs only; a BE without this change would ignore the 
option, and a static overwrite of a CHAR partition from a new FE would then 
fail its commit rather than write a wrong partition.
   
   Reading is unchanged. Doris reads a padded value with its trailing spaces, 
so `WHERE c = 'a'` does not match `'a  '`, as with the data that Spark and 
Flink write today; with ORC, orc-core strips the trailing spaces on read.
   
   **Results.**
   
   - **FE UT.**
     - `PaimonWriteSchemaTest`: top-level CHAR values padded by code points 
(`'中'` → `'中  '`), a longer value kept, VARCHAR left alone; CHAR values nested 
in ARRAY, MAP and ROW, where a map keeps two keys that only padding makes 
equal; padding off; UPDATE and DELETE rows left alone; reading the option.
     - `PaimonWriteBindingTest.staticCharPartitionIsPaddedAsTheRowsAre`, 
`PaimonBackendBoundTableTest.writeBindingTellsOnlyTheBackendWhetherToPadCharValues`
 and 
`PaimonWritePlanProviderTest.charPaddingFollowsTheSessionVariableAndDefaultsToOn`.
     - Each of these mutations fails its test: no padding, padding every row 
kind, no nested padding, ignoring the option, counting bytes or UTF-16 units 
instead of code points, not setting the option, and ignoring the session 
variable.
     - All 737 tests of fe-connector-paimon and 128 of paimon-scanner pass.
   - **End to end.** The table above was produced on a local cluster. In the 
same run, with the switch off Doris writes `'u'`, `'w'` and the static 
partition `'z'` unpadded; with it on again, `DELETE ... WHERE v = 3` removes 
`'u'` and `UPDATE ... SET v = 40 WHERE v = 4` updates `'w'` in place, with no 
padded duplicate.
   - **Regression.** The new `test_paimon_write_char_padding` 
(external_table_p0/paimon/write) writes through Spark and Doris: a key both 
write is one row, nested and partition values are padded, a second static 
overwrite of a CHAR partition replaces the padded partition, the switch off 
writes values as they are, and DELETE and UPDATE match the unpadded keys. Its 
`.out` was generated on a local cluster with the iceberg docker environment; 
with the FE and BE jars from before this change it fails at its first check 
(`[k] 1 1` instead of `[k  ] 3 1`). The other suites under 
`external_table_p0/paimon/write` pass on the same cluster, except 
`test_paimon_write_thread_lifecycle`, which reads BE's 
`/api/be_process_thread_num` and that endpoint needs Linux `/proc`.
   
   ### Release note
   
   Doris now pads CHAR(n) values written into Paimon tables with spaces to n 
characters, as Spark and Flink do, including values nested in ARRAY, MAP and 
ROW and static partition values; UPDATE and DELETE keep keys as stored. Doris 
reads a padded value back with its trailing spaces, so `WHERE c = 'a'` does not 
match it, as with data that Spark and Flink write. A CHAR key that an earlier 
Doris wrote unpadded is a different key from its padded form; set the session 
variable `enable_paimon_write_char_padding` to false to keep writing CHAR 
values unpadded.
   
   ### Check List (For Author)
   
   - Test <!-- At least one of them must be included. -->
       - [x] Regression test
           - `test_paimon_write_char_padding`, as above.
       - [x] Unit Test
           - `PaimonWriteSchemaTest`, `PaimonWriteBindingTest`, 
`PaimonBackendBoundTableTest` and `PaimonWritePlanProviderTest`, as above; all 
tests of fe-connector-paimon and paimon-scanner pass.
       - [x] Manual test (add detailed scripts or steps below)
           - Local single-BE cluster with a file:// Paimon catalog: the cases 
in the table above and the switch-off, DELETE and UPDATE cases, before and 
after this change.
       - [ ] No need to test or manual test. Explain why:
           - [ ] This is a refactor/code format and no logic has been changed.
           - [ ] Previous test can cover this change.
           - [ ] No code files have been changed.
           - [ ] Other reason <!-- Add your reason?  -->
   
   - Behavior changed:
       - [ ] No.
       - [x] Yes. <!-- Explain the behavior change -->
           - CHAR values written into Paimon tables are padded to the column 
length unless `enable_paimon_write_char_padding` is false.
   
   - Does this need documentation?
       - [ ] No.
       - [x] Yes. <!-- Add document PR link here. eg: 
https://github.com/apache/doris-website/pull/1214 -->
           - The new session variable `enable_paimon_write_char_padding`.
   
   ### Check List (For Reviewer who merge this PR)
   
   - [ ] Confirm the release note
   - [ ] Confirm test cases
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to