Skip to content

[fix](arrow) correct the Flight SQL GetTables schema and the TIMESTAMPTZ arrow reader - #66344

Merged
morningman merged 2 commits into
apache:masterfrom
morningman:fix-flightsql-arrow-type-fidelity
Aug 2, 2026
Merged

[fix](arrow) correct the Flight SQL GetTables schema and the TIMESTAMPTZ arrow reader#66344
morningman merged 2 commits into
apache:masterfrom
morningman:fix-flightsql-arrow-type-fidelity

Conversation

@morningman

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Related Issue: #65615

Problem Summary:

Two places where Doris says one thing about an Arrow value and does another. Both were found
while reading Doris over Arrow Flight SQL, and both reproduce on master on their own.

1. CommandGetTables describes types the batches do not carry

FlightSqlSchemaHelper.getArrowType is documented as mirroring convert_to_arrow_type in the
backend -- the schema a client reads from GetTables is what it types its columns by. Two of the
mappings had drifted from what BE actually writes:

  • A DATEV2 column was described as Date(MILLISECOND) -- Arrow date64 -- while BE writes
    arrow::Date32Type, a day number. A client that trusts the schema renders and compares the
    column as a datetime and then fails on the first batch with
    not support convert to datetimev2 from arrow type: 16.
  • A complex column was described with a placeholder child: an array's element type was the Null
    type, a map's pair was a bare list, a struct had no fields at all. An Arrow ARRAY/MAP/STRUCT
    carries its element types in its children and nowhere else, so this said "array of nothing"
    about every array in the catalog while BE emitted ListType(item), MapType(key, value) and
    StructType(fields) in the data.

describeTables already reports the whole tree -- Column.createChildrenColumn even names an
array's element item and a map's pair key/value, which is what Arrow calls them -- so the
children are now built from it, recursively. A descriptor that reports no children keeps the old
placeholders: a source that cannot describe its nested types is no worse off than before.

2. A TIMESTAMPTZ column read from Arrow was decoded by copying its bits

DataTypeTimeStampTzSerDe never overrode read_column_from_arrow, so the one it inherited from
DataTypeNumberSerDe<TYPE_TIMESTAMPTZ> ran: its fixed-width path memcpys the array's buffer
straight into the column. Arrow hands it int64 EPOCH values; the column stores PACKED date/time
values. Both are eight bytes wide, so the width check passes, the scan succeeds, and every row is
silently wrong -- a Doris datetime read back over Arrow Flight SQL rendered as +08:05 with no
date in it.

This is reachable today through the remote_doris catalog. Its schema is the remote table's
Column objects deserialized verbatim (RemoteDorisRestClient.parseColumns), so a remote
TIMESTAMPTZ column stays TIMESTAMPTZ locally; RemoteDorisScanNode plans the scan as
FORMAT_ARROW; and remote_doris_reader materializes it through this serde. The remote side
writes the epochs with the serde's own write_column_to_arrow, so both halves of that round trip
are Doris code and the mismatch is entirely internal.

The other Arrow readers -- arrow_stream_reader for the arrow file format,
python_udtf_function, paimon_cpp_reader -- share the same serde and are covered by the same
change; whether each of them can present a TIMESTAMPTZ column today is not something this PR
claims either way.

The reader converts by unit into the microseconds the column stores. NANO is divided rather than
refused, since no Doris datetime type keeps sub-microsecond digits, and the division floors so a
pre-1970 instant does not move forward by one microsecond. The value is read as an instant on the
UTC line, the inverse of what write_column_to_arrow emits, so the round trip is exact.

Two cases are deliberate. A null slot is not converted: the bytes under it are whatever the source
left there, and running a garbage epoch through the range check would fail a batch whose rows are
all well-formed. An Arrow type this serde cannot decode is an error rather than a fallback --
accepting anything eight bytes wide is how the corruption above stayed invisible.

Release note

Fix two Arrow type-fidelity bugs.

GetTables over Arrow Flight SQL reported DATEV2 as date64 while the data is date32, and
described ARRAY/MAP/STRUCT columns with placeholder children instead of their real element
types; a client that types its columns from that schema failed on the first batch.

A TIMESTAMPTZ column materialized from Arrow was decoded by copying the epoch integers over the
column's packed values -- same width, no error, every row wrong. This affects any reader that
builds a TIMESTAMPTZ column from Arrow, including the native Paimon reader.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • 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.
      • An Arrow Flight SQL client now receives, for DATEV2 and for nested types, the Arrow
        types the batches actually carry. A client that had worked around the old schema by
        ignoring it is unaffected; one that trusted it stops failing.
      • A TIMESTAMPTZ column read from Arrow now holds the instant the source sent instead of
        a reinterpreted epoch. Values previously returned on those paths were wrong, so results
        change -- to the correct ones.
  • 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

The schema GetTables reports and the data that follows it disagreed in
two places, and getArrowType's own comment says they must not: it is
documented as mirroring convert_to_arrow_type in the backend.

A DATEV2 column was described as Date(MILLISECOND) -- date64 -- while BE
writes arrow::Date32Type. A client that types its columns from this
schema sees a datetime where a date arrives, so it renders the column as
one, compares it as one, and its read fails with "not support convert to
datetimev2 from arrow type: 16" on the first batch.

A complex column was described with a placeholder child: an array's
element type was the Null type, a map's pair was a bare list, a struct
had no fields at all. An Arrow ARRAY/MAP/STRUCT carries its element
types in its children and nowhere else, so this said "array of nothing"
about every array in the catalog while BE emitted ListType(item),
MapType(key, value) and StructType(fields) in the batches.

describeTables already reports the whole tree -- Column's children even
name an array's element "item" and a map's pair "key"/"value", which is
what Arrow calls them -- so the children are built from it. A descriptor
that reports none keeps the old placeholders: a source that cannot
describe its nested types is no worse off than before.

The test pins each mapping against what BE emits rather than against
itself: the date unit, an array's element type, a map's key/value pair
under the entries struct, a struct's fields, a nested tree walked to its
leaves, and the placeholders a descriptor without children still gets.
The last case asserts after a round trip through the serialized schema,
because that byte string -- not the Field objects -- is what reaches the
client in the table_schema column.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1z6M8Gj9F9pErafGdfdjx
@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?

DataTypeTimeStampTzSerDe never overrode read_column_from_arrow, so the
one it inherited from DataTypeNumberSerDe<TYPE_TIMESTAMPTZ> ran: its
fixed-width path memcpy's the array's buffer straight into the column.
Arrow hands it int64 EPOCH values; the column stores PACKED date/time
values. Both are eight bytes wide, so the width check passes, the scan
succeeds, and every row is silently wrong -- a Doris datetime read back
over Arrow Flight SQL rendered as "+08:05" with no date in it.

The reader converts by unit into the microseconds the column stores.
NANO is divided rather than refused, since no Doris datetime type keeps
sub-microsecond digits, and the division floors so a pre-1970 instant
does not move forward by one microsecond. The value is read as an
instant on the UTC line, the inverse of what write_column_to_arrow
emits, so the round trip is exact.

Two cases are deliberate. A null slot is not converted: the bytes under
it are whatever the source left there, and running a garbage epoch
through the range check would fail a batch whose rows are all
well-formed. An arrow type this serde cannot decode is an error rather
than a fallback -- accepting anything eight bytes wide is how the
corruption above stayed invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1z6M8Gj9F9pErafGdfdjx
@morningman
morningman force-pushed the fix-flightsql-arrow-type-fidelity branch from 038d93c to 6ec4202 Compare August 1, 2026 04:06
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17716	3988	3962	3962
q2	1998	336	212	212
q3	10307	1430	821	821
q4	4680	475	334	334
q5	7515	833	557	557
q6	183	175	141	141
q7	765	789	596	596
q8	9306	1587	1631	1587
q9	5444	4090	4074	4074
q10	6738	1628	1372	1372
q11	520	355	316	316
q12	742	582	446	446
q13	18109	3352	2711	2711
q14	263	255	235	235
q15	q16	732	740	670	670
q17	958	899	999	899
q18	7310	5851	5577	5577
q19	1303	1213	1116	1116
q20	812	687	574	574
q21	5831	2612	2454	2454
q22	442	360	299	299
Total cold run time: 101674 ms
Total hot run time: 28953 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4284	4180	4184	4180
q2	284	321	208	208
q3	4557	4896	4391	4391
q4	2170	2219	1426	1426
q5	4229	4076	4138	4076
q6	224	171	126	126
q7	1702	1556	1427	1427
q8	2988	2213	2099	2099
q9	7503	7440	7402	7402
q10	4328	4279	3880	3880
q11	643	438	369	369
q12	705	739	537	537
q13	3290	3483	2874	2874
q14	286	303	257	257
q15	q16	685	713	634	634
q17	1311	1289	1279	1279
q18	8110	7193	7526	7193
q19	1167	1107	1065	1065
q20	2181	2193	1908	1908
q21	5263	4608	4433	4433
q22	543	470	392	392
Total cold run time: 56453 ms
Total hot run time: 50156 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 169641 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 6ec4202e5ba2e22268bfac252f5d92d97a19af86, data reload: false

query5	4322	618	465	465
query6	462	230	199	199
query7	4842	627	339	339
query8	331	179	164	164
query9	8711	4059	4011	4011
query10	492	330	282	282
query11	5833	2201	1992	1992
query12	151	98	96	96
query13	1252	602	473	473
query14	6135	4708	4405	4405
query14_1	3814	3811	3797	3797
query15	214	203	173	173
query16	995	507	529	507
query17	896	671	552	552
query18	2418	461	340	340
query19	197	178	148	148
query20	102	99	101	99
query21	227	152	136	136
query22	12999	12980	12745	12745
query23	17357	16413	15991	15991
query23_1	16150	16101	16222	16101
query24	7552	1698	1240	1240
query24_1	1234	1207	1226	1207
query25	546	422	354	354
query26	1309	369	211	211
query27	2593	602	388	388
query28	4476	2039	2000	2000
query29	1076	606	460	460
query30	337	265	219	219
query31	1104	1070	967	967
query32	114	62	61	61
query33	529	308	242	242
query34	1154	1140	634	634
query35	734	738	640	640
query36	785	765	692	692
query37	149	102	97	97
query38	1830	1654	1608	1608
query39	836	837	806	806
query39_1	786	798	767	767
query40	252	165	150	150
query41	65	62	63	62
query42	96	90	92	90
query43	310	327	271	271
query44	1449	769	745	745
query45	186	173	165	165
query46	1020	1141	714	714
query47	1577	1543	1440	1440
query48	408	383	281	281
query49	598	409	303	303
query50	1068	439	343	343
query51	10439	10569	10359	10359
query52	88	95	74	74
query53	267	268	201	201
query54	273	233	224	224
query55	76	71	65	65
query56	303	308	301	301
query57	1032	1019	929	929
query58	290	258	274	258
query59	1521	1607	1363	1363
query60	340	283	267	267
query61	188	147	147	147
query62	394	315	266	266
query63	230	197	195	195
query64	2824	1036	808	808
query65	3913	3842	3857	3842
query66	1840	479	358	358
query67	28230	28246	28155	28155
query68	3148	1634	1046	1046
query69	398	303	265	265
query70	857	789	799	789
query71	378	331	328	328
query72	3055	2799	2499	2499
query73	816	834	456	456
query74	4653	4514	4321	4321
query75	2383	2358	2004	2004
query76	2350	1137	770	770
query77	351	373	291	291
query78	11260	11082	10617	10617
query79	1398	1184	743	743
query80	895	564	499	499
query81	510	339	284	284
query82	557	149	114	114
query83	420	331	304	304
query84	335	171	138	138
query85	1077	607	518	518
query86	363	234	223	223
query87	1793	1810	1695	1695
query88	3666	2778	2782	2778
query89	390	324	278	278
query90	1797	198	198	198
query91	198	183	157	157
query92	61	59	53	53
query93	1579	1512	967	967
query94	579	360	308	308
query95	783	494	544	494
query96	1036	778	341	341
query97	2480	2434	2327	2327
query98	199	195	190	190
query99	717	724	620	620
Total cold run time: 254852 ms
Total hot run time: 169641 ms

@hello-stephen

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

query1	0.00	0.00	0.01
query2	0.09	0.05	0.05
query3	0.25	0.13	0.13
query4	1.61	0.13	0.14
query5	0.24	0.21	0.22
query6	1.16	0.84	0.82
query7	0.03	0.01	0.01
query8	0.05	0.04	0.03
query9	0.38	0.30	0.30
query10	0.59	0.55	0.53
query11	0.18	0.13	0.13
query12	0.17	0.14	0.14
query13	0.45	0.46	0.46
query14	0.99	0.98	1.00
query15	0.60	0.57	0.59
query16	0.31	0.32	0.31
query17	1.10	1.11	1.14
query18	0.22	0.20	0.19
query19	2.05	1.95	1.91
query20	0.02	0.02	0.01
query21	15.43	0.22	0.12
query22	4.78	0.05	0.05
query23	16.12	0.31	0.12
query24	2.90	0.43	0.31
query25	0.12	0.05	0.05
query26	0.73	0.22	0.15
query27	0.05	0.03	0.04
query28	3.53	0.74	0.35
query29	12.47	4.08	3.23
query30	0.28	0.16	0.15
query31	2.77	0.56	0.31
query32	3.21	0.59	0.48
query33	3.09	3.18	3.15
query34	15.48	3.91	3.25
query35	3.21	3.23	3.23
query36	0.56	0.43	0.41
query37	0.08	0.06	0.06
query38	0.06	0.04	0.04
query39	0.04	0.03	0.04
query40	0.17	0.15	0.15
query41	0.08	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.04
Total cold run time: 95.73 s
Total hot run time: 23.79 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 70.69% (41/58) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 59.15% (25624/43320)
Line Coverage 43.23% (257154/594831)
Region Coverage 38.93% (203930/523780)
Branch Coverage 40.27% (93046/231031)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 70.69% (41/58) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 75.79% (32021/42252)
Line Coverage 60.43% (357041/590856)
Region Coverage 56.99% (299526/525548)
Branch Coverage 58.41% (134970/231069)

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 0.00% (0/183) 🎉
Increment coverage report
Complete coverage report

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

@github-actions github-actions Bot added approved Indicates a PR has been approved by one committer. reviewed labels Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

PR approved by anyone and no changes requested.

@morningman
morningman merged commit 8b0cafd into apache:master Aug 2, 2026
29 of 30 checks passed
github-actions Bot pushed a commit that referenced this pull request Aug 2, 2026
…PTZ arrow reader (#66344)

### What problem does this PR solve?

Related Issue: #65615

Problem Summary:

Two places where Doris says one thing about an Arrow value and does
another. Both were found
while reading Doris over Arrow Flight SQL, and both reproduce on master
on their own.

**1. `CommandGetTables` describes types the batches do not carry**

`FlightSqlSchemaHelper.getArrowType` is documented as mirroring
`convert_to_arrow_type` in the
backend -- the schema a client reads from `GetTables` is what it types
its columns by. Two of the
mappings had drifted from what BE actually writes:

- A `DATEV2` column was described as `Date(MILLISECOND)` -- Arrow date64
-- while BE writes
`arrow::Date32Type`, a day number. A client that trusts the schema
renders and compares the
  column as a datetime and then fails on the first batch with
  `not support convert to datetimev2 from arrow type: 16`.
- A complex column was described with a placeholder child: an array's
element type was the Null
type, a map's pair was a bare list, a struct had no fields at all. An
Arrow ARRAY/MAP/STRUCT
carries its element types in its children and nowhere else, so this said
"array of nothing"
about every array in the catalog while BE emitted `ListType(item)`,
`MapType(key, value)` and
  `StructType(fields)` in the data.

`describeTables` already reports the whole tree --
`Column.createChildrenColumn` even names an
array's element `item` and a map's pair `key`/`value`, which is what
Arrow calls them -- so the
children are now built from it, recursively. A descriptor that reports
no children keeps the old
placeholders: a source that cannot describe its nested types is no worse
off than before.

**2. A TIMESTAMPTZ column read from Arrow was decoded by copying its
bits**

`DataTypeTimeStampTzSerDe` never overrode `read_column_from_arrow`, so
the one it inherited from
`DataTypeNumberSerDe<TYPE_TIMESTAMPTZ>` ran: its fixed-width path
`memcpy`s the array's buffer
straight into the column. Arrow hands it int64 EPOCH values; the column
stores PACKED date/time
values. Both are eight bytes wide, so the width check passes, the scan
succeeds, and every row is
silently wrong -- a Doris datetime read back over Arrow Flight SQL
rendered as `+08:05` with no
date in it.

This is reachable today through the `remote_doris` catalog. Its schema
is the remote table's
`Column` objects deserialized verbatim
(`RemoteDorisRestClient.parseColumns`), so a remote
TIMESTAMPTZ column stays TIMESTAMPTZ locally; `RemoteDorisScanNode`
plans the scan as
`FORMAT_ARROW`; and `remote_doris_reader` materializes it through this
serde. The remote side
writes the epochs with the serde's own `write_column_to_arrow`, so both
halves of that round trip
are Doris code and the mismatch is entirely internal.

The other Arrow readers -- `arrow_stream_reader` for the `arrow` file
format,
`python_udtf_function`, `paimon_cpp_reader` -- share the same serde and
are covered by the same
change; whether each of them can present a TIMESTAMPTZ column today is
not something this PR
claims either way.

The reader converts by unit into the microseconds the column stores.
NANO is divided rather than
refused, since no Doris datetime type keeps sub-microsecond digits, and
the division floors so a
pre-1970 instant does not move forward by one microsecond. The value is
read as an instant on the
UTC line, the inverse of what `write_column_to_arrow` emits, so the
round trip is exact.

Two cases are deliberate. A null slot is not converted: the bytes under
it are whatever the source
left there, and running a garbage epoch through the range check would
fail a batch whose rows are
all well-formed. An Arrow type this serde cannot decode is an error
rather than a fallback --
accepting anything eight bytes wide is how the corruption above stayed
invisible.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. dev/4.1.x reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants