Skip to content

[fix](paimon) Keep CHAR/VARCHAR lengths and DATETIME precision when creating Paimon tables - #69045

Merged
morningman merged 1 commit into
apache:masterfrom
morningman:b2m-t28-paimon-ddl-type-bounds
Oct 14, 2026
Merged

morningman merged 1 commit into
apache:masterfrom
morningman:b2m-t28-paimon-ddl-type-bounds

Conversation

@morningman

@morningman morningman commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: #66227, #68825

Problem Summary:

In short. When Doris creates or alters a Paimon table, master drops the declared length of CHAR(n) and VARCHAR(n) columns (they all become VARCHAR(2147483647)) and gives DATETIME a precision of 6. Paimon relies on these bounds when it reads files written under an older schema, so a table created or altered through Doris does not behave like the schema the user declared. This PR keeps the bounds, as branch-4.1 #66227 does.

Background. PaimonTypeMapping.toPaimonType (fe-connector-paimon) turns the column types of CREATE TABLE, ADD COLUMN and MODIFY COLUMN into the Paimon schema. Paimon stores that schema with every data file. When it reads a file written under an older schema, it casts each column from the file's type to the current type, so the current type decides what a reader sees: a VARCHAR(3) column truncates older longer values, and a TIMESTAMP(3) column reads a numeric string such as 42 as milliseconds. The mapping was ported from fe-core's DorisToPaimonTypeVisitor when Paimon moved into the connector plugin; branch-4.1 #66227 later changed that visitor to keep the bounds, and the change never reached master. #68825 (the master port of the rest of #66227) updated this method's Javadoc to describe the new mapping, but not the code.

The problem, and what it cost.

Doris column type Paimon type on master Paimon type with this PR What changes for users
CHAR(n) VARCHAR(2147483647) CHAR(n) The column reads back as CHAR(n) instead of STRING.
VARCHAR(n) VARCHAR(2147483647) VARCHAR(n) The column reads back as VARCHAR(n) instead of STRING. MODIFY COLUMN c VARCHAR(3) narrows the column, and Paimon truncates older values when it reads them; on master it widens the column to the maximum length instead.
STRING VARCHAR(2147483647) unchanged
DATETIME (v1, only when date conversion is disabled) TIMESTAMP(6) TIMESTAMP(0)
DATETIMEV2(p) TIMESTAMP(p) unchanged

The same applies inside ARRAY, MAP and STRUCT, and to CREATE TABLE ... AS SELECT: query columns that Doris' CTAS rules keep as VARCHAR(n) (columns of internal tables, and expressions as VARCHAR(65533) by default) keep that length instead of becoming STRING. Only Doris DDL uses this mapping: ADD COLUMN and MODIFY COLUMN on a table created by Spark are affected, reading a table is not.

How this PR fixes it. toPaimonType maps CHAR(n) to CharType(n), VARCHAR(n) to VarCharType(n) (CREATE TABLE already carries the length in ConnectorType.precision), STRING to VarCharType(MAX_LENGTH), DATETIME to TimestampType(0) and DATETIMEV2(p) to TimestampType(p). A column declared as plain varchar or char is normalized to VARCHAR(65533) or CHAR(1) by ColumnDefinition before it reaches the connector, so the length is always set. The Javadoc is the same text as in #68825.

Two existing behaviors now also apply to these tables, as they already do on master to CHAR(n) and VARCHAR(n) columns of Paimon tables created by Spark or Flink. Doris's native Parquet/ORC readers apply the declared length to older values only when truncate_char_or_varchar_columns is on (the same as for Hive and Iceberg tables); JNI reads always apply Paimon's cast. Doris's Paimon writers do not pad CHAR(n) values to their length, while Paimon's own cast does. Both are left to separate changes.

Results. PaimonTypeMappingToPaimonTest now checks the bounds and precisions, the nested types, and Paimon's own casts on the mapped types: from VARCHAR(10) to the mapped VARCHAR(3), abcdef becomes abc; from STRING to the mapped DATETIMEV2(3), 42 becomes 1970-01-01T00:00:00.042. With master's mapping all four of these cases fail. All fe-connector-paimon unit tests (726) pass. No existing regression suite creates Paimon tables with CHAR(n), VARCHAR(n) or DATETIME v1 columns through Doris; the test_paimon_rust_reader_eq_for_null suite in #68825 narrows a column to VARCHAR(3) and needs this change.

Original author: @pzhdfy (#66227)

Release note

Paimon tables created or altered by Doris keep the declared length of CHAR(n) and VARCHAR(n) columns instead of turning them into STRING.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
      • PaimonTypeMappingToPaimonTest: 4 cases fail with master's mapping and pass with this PR; all fe-connector-paimon tests (726) pass.
    • Manual test (add detailed scripts or steps below)
    • 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
  • Behavior changed:

    • No.
    • Yes.
      • CHAR(n) / VARCHAR(n) columns of Paimon tables created or altered by Doris keep their length; DATETIME v1 maps to TIMESTAMP(0).
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

…reating Paimon tables

### What problem does this PR solve?

Issue Number: None

Related PR: apache#66227

Problem Summary:

Ported from the DDL type mapping change in branch-4.1 apache#66227. When Doris
creates or alters a Paimon table, PaimonTypeMapping.toPaimonType turns the
declared column type into the Paimon schema. On master CHAR(n), VARCHAR(n)
and STRING all became VARCHAR(2147483647), and DATETIME became TIMESTAMP(6).
Paimon uses these bounds when it reads a data file written under an older
schema: it casts the file's column type to the current one, truncating to
the declared length and reading numeric timestamp strings in units of the
declared precision. With the bounds dropped, the Paimon table did not have
the schema the user declared, and Doris read the column back as STRING.

CHAR(n) now maps to CHAR(n), VARCHAR(n) to VARCHAR(n), STRING to
VARCHAR(2147483647), DATETIME to TIMESTAMP(0), and DATETIMEV2(p) to
TIMESTAMP(p) as before, also inside ARRAY, MAP and STRUCT, as on
branch-4.1 and branch-4.2.

### Release note

Paimon tables created by Doris keep the declared CHAR(n) / VARCHAR(n) length; DATETIME maps to TIMESTAMP(0).

### Check List (For Author)

- Test: Unit Test
    - PaimonTypeMappingToPaimonTest: bounds, precision, nested types, and Paimon's own casts on the mapped types
- Behavior changed: Yes. CHAR(n) / VARCHAR(n) columns of Paimon tables created by Doris keep their length instead of becoming STRING.
- Does this need documentation: No

Co-authored-by: Fangyuan Deng <982092332@qq.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

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.

Request changes: two verified character-handling defects remain. Persisting bounded VARCHAR now exposes a native/JNI mismatch for historical narrowing, and persisting true CHAR needs write-side blank padding before key extraction and storage. Both are inline below; there are no existing blocking comments to carry forward.

Static review of both changed files and their CREATE/CTAS/ADD/MODIFY, recursive type, schema evolution, and read/write consumers. No builds or tests were run, as required by this review environment. The author's reported test count is not reviewer-executed evidence.

Critical checkpoint conclusions:

  1. Goal and correctness: declared bounds and DATETIME-v1 seconds precision are mapped correctly locally; end-to-end character behavior still needs the two inline fixes. The new unit assertions verify descriptors and library casts, not the affected Doris read/write paths.
  2. Scope, clarity, and reuse: focused two-file change; CREATE/ADD/MODIFY and nested types reuse the same mapper.
  3. Concurrency and locking: stateless conversion; no new threads, mutable shared state, locks, lock-held I/O, or lock-order changes.
  4. Lifecycle and initialization: only small schema descriptors are constructed; no new resource ownership, caches, worker lifetimes, or static initialization dependency.
  5. Configuration: no configuration or dynamic-update mechanism added.
  6. Compatibility: existing remote schemas remain authoritative, and an unchanged MODIFY retains the remote type. No SPI/function symbol or wire-format change. Newly created/explicitly altered character schemas expose the inline interoperability problems.
  7. Thrift/protobuf IDs: no definitions changed; branch field-ID comparison is inapplicable.
  8. Parallel paths: CREATE/CTAS and ADD/MODIFY share this mapping; ARRAY/MAP/STRUCT recurse and preserve existing nullability/comments. Native/JNI read parity and canonical CHAR writes require fixes. STRING, DATETIMEV2, and TIMESTAMPTZ behavior is unchanged.
  9. Conditions and errors: uppercase normalization makes the v1/v2 timestamp discriminator correct; valid DDL validates and recursively defaults character lengths before encoding them as precision. Unsupported types still fail; no new swallowed error or speculative fallback.
  10. Test coverage: scalar bounds/kinds, timestamp precisions, historical library casts, ARRAY/MAP bounds, and existing STRUCT/nullability/unsupported cases inspected. End-to-end ALTER/read parity and short CHAR write/key cases are missing and should cover the inline issues.
  11. Test results: new expected values agree with Paimon 1.4.2 source; no golden outputs changed. Assertions were inspected, not executed.
  12. Observability: no new distributed operation; existing DDL error/log context remains sufficient. The silent value/key defects need correctness fixes and regression coverage.
  13. Transactions and persistence: unchanged Paimon catalog create/alter persists the schema. Doris EditLog/replay/failover and transaction commit protocols are untouched.
  14. Writes, atomicity, and crashes: atomic schema/insert commit and cleanup paths are unchanged, but CHAR values need normalization before key/bucket extraction; no new leak or deadlock mechanism identified.
  15. FE/BE propagation: no new variable or protocol field; existing remote-schema transport carries bounds to JNI, while native character conversion loses their semantic effect.
  16. Performance: constant-time scalar construction and existing linear schema recursion; no additional I/O or row-path work introduced.
  17. Other concerns and user focus: no additional user focus was supplied. Invalid/missing-length and timestamp-compatibility risks were dismissed with upstream/downstream evidence. No remaining unresolved candidate or static coverage gap after the final sweep; runtime execution remains outside this review's scope.

Review converged after 2 rounds: both assigned agents returned NO_NEW_VALUABLE_FINDINGS in the latest round, both changed files and applicable checkpoints are covered, and all candidates/risk focuses have explicit verified or dismissed dispositions.

Review converged after 2 round(s).

// DDL must retain bounds: widening to STRING changes historical narrowing casts.
return new CharType(type.getPrecision());
case "VARCHAR":
return new VarCharType(type.getPrecision());

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] Apply narrowing casts on the native Paimon read path

This mapping is also used by MODIFY COLUMN. For an append-only Parquet table t(s STRING) containing abcdef, ALTER TABLE t MODIFY COLUMN s VARCHAR(3) now persists the bound, but the default native read still returns abcdef: PaimonScanPlanProvider routes raw files to native without checking character casts, the v1 string converter is ConsistentConverter, and v2 treats string types as equal regardless of length. Paimon's historical cast instead returns abc (also with force_jni_scanner=true). Previously this ALTER mapped to VARCHAR(MAX), so the change exposes inconsistent results after Doris DDL. Please apply Paimon character casts in native reads or route affected splits through JNI, and cover ALTER/read parity with an end-to-end test; the new direct CastExecutors assertion bypasses this path.

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.

The difference is real, but it is how Doris's native readers already treat the CHAR/VARCHAR bounds of external tables, not something this PR adds. They apply the table's length only when truncate_char_or_varchar_columns is on: the format_v2 TableReader handles exactly this case (a table VARCHAR(n) over a wider or STRING file column, _should_truncate_char_or_varchar_column), and Hive and Iceberg tables behave the same. Master already reads Paimon VARCHAR(n) as VARCHAR(n), so a column narrowed by Spark or Flink gives the same native/JNI difference today, and with the session variable on the native read returns abc. This PR only makes Doris DDL store the declared type, as branch-4.1 and branch-4.2 do. Making native reads follow Paimon's casts by default would change those existing tables as well, so I'd keep it out of this PR. I updated the description, which said older values are truncated without mentioning the native default.

return new DoubleType();
case "CHAR":
// DDL must retain bounds: widening to STRING changes historical narrowing casts.
return new CharType(type.getPrecision());

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] Normalize CHAR values before writing the new CHAR schema

Returning CharType(n) also changes newly Doris-created tables from VARCHAR(MAX) to real Paimon CHAR, but the Doris sink does not implement Paimon's CHAR padding. With k CHAR(3) NOT NULL as a primary key, inserting 'a' passes the one-character string through BindSink, Arrow, and PaimonWriteSchema.tableRow; the SDK extracts/stores that key without casting. Paimon's CHAR cast produces 'a ', and its CHAR comparator compares the raw strings, so another writer using the typed CHAR value creates a separate key instead of updating the Doris-written record. Please normalize CHAR values before key/bucket extraction and storage (including nested CHAR), and add a write/cast parity test with a short value. The former VARCHAR(MAX) schema did not require this padding.

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.

Confirmed: neither write backend pads CHAR. PaimonWriteSchema.tableRow (JNI) and the FFI backend pass the string through as Doris holds it. That is already the case on master for CHAR(n) columns of tables created by Spark or Flink, which Doris reads as CHAR(n) and writes to unpadded, and branch-4.1/4.2 behave the same with this mapping. The fix belongs in the write path (both backends, nested fields included), where it also covers those existing tables; mapping CHAR(n) to a wider type here would only hide it for tables created by Doris. I'll do the padding as a separate change, and the description now states the limitation.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29128 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit bd8676232a50c3d6b1c60fb57274f81e2d9d176f, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17614	5633	5544	5544
q2	2129	308	273	273
q3	10203	1616	858	858
q4	4690	481	353	353
q5	7453	805	530	530
q6	191	185	157	157
q7	774	808	605	605
q8	9246	1384	1186	1186
q9	5861	4495	4481	4481
q10	6835	1287	1045	1045
q11	427	258	241	241
q12	635	406	296	296
q13	18044	3049	2411	2411
q14	284	276	254	254
q15	q16	736	732	666	666
q17	1207	742	583	583
q18	7169	6280	6241	6241
q19	1267	966	633	633
q20	391	344	233	233
q21	5105	2271	2425	2271
q22	374	328	267	267
Total cold run time: 100635 ms
Total hot run time: 29128 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6206	6146	6112	6112
q2	697	588	569	569
q3	5224	5393	4986	4986
q4	2045	2144	1573	1573
q5	5245	5477	5942	5477
q6	263	204	160	160
q7	2376	2098	1888	1888
q8	3357	3020	2867	2867
q9	8315	8228	8363	8228
q10	3757	3796	3337	3337
q11	656	485	431	431
q12	715	743	539	539
q13	2870	3210	2520	2520
q14	313	320	294	294
q15	q16	711	860	670	670
q17	8383	7807	7456	7456
q18	13148	12338	13097	12338
q19	969	893	865	865
q20	2255	2217	2004	2004
q21	5925	4892	4965	4892
q22	497	463	431	431
Total cold run time: 73927 ms
Total hot run time: 67637 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152623 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit bd8676232a50c3d6b1c60fb57274f81e2d9d176f, data reload: false

query5	4300	606	456	456
query6	431	225	197	197
query7	4814	446	240	240
query8	333	186	174	174
query9	8806	4048	4009	4009
query10	438	315	250	250
query11	5982	3419	3215	3215
query12	136	85	83	83
query13	1240	445	319	319
query14	6539	4790	4488	4488
query14_1	4233	4275	4231	4231
query15	206	198	180	180
query16	936	445	397	397
query17	848	655	551	551
query18	2424	444	321	321
query19	205	163	136	136
query20	83	79	82	79
query21	214	138	110	110
query22	12981	12969	12756	12756
query23	13300	12623	12226	12226
query23_1	12296	12212	12293	12212
query24	7115	820	476	476
query24_1	481	471	473	471
query25	533	428	367	367
query26	1250	256	138	138
query27	2792	442	274	274
query28	4659	1958	1948	1948
query29	1584	610	476	476
query30	305	222	187	187
query31	886	776	654	654
query32	140	94	90	90
query33	516	310	253	253
query34	1018	870	491	491
query35	741	762	646	646
query36	829	838	729	729
query37	132	102	94	94
query38	1826	1785	1713	1713
query39	720	744	709	709
query39_1	706	675	693	675
query40	226	125	107	107
query41	75	69	71	69
query42	87	88	88	88
query43	363	385	327	327
query44	1285	707	705	705
query45	186	174	167	167
query46	840	995	577	577
query47	3032	2951	2987	2951
query48	303	312	228	228
query49	577	398	302	302
query50	682	278	216	216
query51	10371	10393	10688	10393
query52	77	83	82	82
query53	189	208	163	163
query54	238	208	197	197
query55	74	67	73	67
query56	240	219	216	216
query57	1613	1615	1560	1560
query58	289	268	263	263
query59	2362	2401	2143	2143
query60	286	257	240	240
query61	175	174	166	166
query62	425	354	287	287
query63	194	162	164	162
query64	2721	927	751	751
query65	3468	3374	3406	3374
query66	1773	420	295	295
query67	20382	20197	20079	20079
query68	3241	997	617	617
query69	399	296	247	247
query70	910	854	848	848
query71	295	234	214	214
query72	2747	2555	2319	2319
query73	528	524	296	296
query74	4578	4484	4317	4317
query75	2360	2332	1944	1944
query76	2383	1036	614	614
query77	355	383	297	297
query78	9036	9090	8529	8529
query79	962	891	524	524
query80	536	423	348	348
query81	536	328	272	272
query82	578	139	109	109
query83	302	200	173	173
query84	310	123	101	101
query85	811	427	370	370
query86	330	234	226	226
query87	2054	1963	1845	1845
query88	3648	2709	2675	2675
query89	327	294	255	255
query90	1916	184	182	182
query91	160	151	125	125
query92	98	80	88	80
query93	1022	993	579	579
query94	447	319	279	279
query95	594	347	326	326
query96	675	531	237	237
query97	2429	2467	2325	2325
query98	155	151	145	145
query99	773	764	648	648
Total cold run time: 234633 ms
Total hot run time: 152623 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.71 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit bd8676232a50c3d6b1c60fb57274f81e2d9d176f, data reload: false

query1	0.01	0.00	0.01
query2	0.10	0.04	0.05
query3	0.27	0.12	0.12
query4	1.60	0.12	0.12
query5	0.29	0.26	0.26
query6	1.18	0.68	0.68
query7	0.05	0.01	0.01
query8	0.05	0.04	0.04
query9	0.44	0.37	0.37
query10	0.58	0.63	0.59
query11	0.21	0.14	0.14
query12	0.18	0.14	0.14
query13	0.49	0.50	0.51
query14	0.98	0.97	0.97
query15	0.66	0.61	0.60
query16	0.36	0.36	0.35
query17	1.14	1.10	1.12
query18	0.24	0.22	0.22
query19	2.15	2.06	1.97
query20	0.02	0.01	0.02
query21	15.49	0.25	0.14
query22	4.45	0.06	0.04
query23	16.29	0.32	0.12
query24	3.17	0.59	0.50
query25	0.11	0.06	0.04
query26	1.40	0.26	0.15
query27	0.04	0.05	0.04
query28	2.30	0.66	0.43
query29	12.54	4.17	3.28
query30	0.28	0.16	0.14
query31	2.77	0.59	0.35
query32	3.22	0.63	0.52
query33	3.21	3.20	3.24
query34	15.54	4.05	3.41
query35	3.37	3.35	3.37
query36	0.56	0.44	0.42
query37	0.10	0.07	0.06
query38	0.05	0.04	0.04
query39	0.04	0.03	0.02
query40	0.17	0.15	0.14
query41	0.09	0.04	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 96.27 s
Total hot run time: 24.71 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100% (0/0) 🎉
Increment coverage report
Complete coverage report

@morningman
morningman merged commit e771daa into apache:master Oct 14, 2026
48 of 49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants