1313import org .apache .arrow .vector .complex .ListVector ;
1414import org .apache .arrow .vector .complex .MapVector ;
1515import org .apache .arrow .vector .types .pojo .ArrowType ;
16+ import org .apache .arrow .vector .types .pojo .Field ;
1617import org .apache .arrow .vector .util .Text ;
1718
1819import java .nio .charset .StandardCharsets ;
2122import java .time .LocalDateTime ;
2223import java .time .LocalTime ;
2324import java .time .ZoneOffset ;
25+ import java .time .format .DateTimeFormatter ;
2426import java .util .ArrayList ;
2527import java .util .LinkedHashMap ;
2628import java .util .List ;
29+ import java .util .Locale ;
2730import java .util .Map ;
31+ import java .util .regex .Pattern ;
2832
2933/** Reads Arrow vector cells as plain Java values, unwrapping Arrow {@link Text} recursively. */
3034public final class ArrowValues {
3135
36+ // Space-separator output matches the SQL plugin's ExprTimestampValue.
37+ private static final DateTimeFormatter TIMESTAMP_NO_NANO = DateTimeFormatter .ofPattern ("yyyy-MM-dd HH:mm:ss" , Locale .ROOT );
38+ private static final DateTimeFormatter TIMESTAMP_WITH_NANO = DateTimeFormatter .ofPattern ("yyyy-MM-dd HH:mm:ss.SSSSSSSSS" , Locale .ROOT );
39+ private static final DateTimeFormatter TIME_NO_NANO = DateTimeFormatter .ofPattern ("HH:mm:ss" , Locale .ROOT );
40+ private static final DateTimeFormatter TIME_WITH_NANO = DateTimeFormatter .ofPattern ("HH:mm:ss.SSSSSSSSS" , Locale .ROOT );
41+ // DataFusion CAST(temporal AS VARCHAR) — date and time joined by 'T', optional fraction.
42+ private static final Pattern ISO_TIMESTAMP_T = Pattern .compile ("^(\\ d{4}-\\ d{2}-\\ d{2})T(\\ d{2}:\\ d{2}:\\ d{2}(?:\\ .\\ d+)?)$" );
43+
3244 private ArrowValues () {}
3345
3446 public static Object toJavaValue (FieldVector vector , int index ) {
3547 if (vector .isNull (index )) return null ;
3648 if (vector instanceof VarCharVector v ) {
37- return new String (v .get (index ), StandardCharsets .UTF_8 );
49+ return spaceSeparator ( new String (v .get (index ), StandardCharsets .UTF_8 ) );
3850 }
3951 // MapVector extends ListVector — must come first.
4052 if (vector instanceof MapVector && vector .getObject (index ) instanceof List <?> entries ) {
@@ -48,74 +60,103 @@ public static Object toJavaValue(FieldVector vector, int index) {
4860 return map ;
4961 }
5062 Object value = vector .getObject (index );
51- if (vector instanceof ListVector && value instanceof List <?> raw ) {
52- return normalizeList (raw );
63+ if (vector instanceof ListVector lv && value instanceof List <?> raw ) {
64+ // child Arrow type drives temporal element formatting
65+ return normalizeList (raw , lv .getDataVector ().getField ());
5366 }
54- // Date/Time/Timestamp cells return java.time.* so ExprValueUtils wraps them as
55- // ExprDateValue / ExprTimeValue / ExprTimestampValue and formats them.
56- Object temporal = normalizeTemporal (vector .getField ().getType (), value );
67+ Object temporal = formatTemporal (vector .getField ().getType (), value );
5768 if (temporal != null ) {
5869 return temporal ;
5970 }
6071 return normalize (value );
6172 }
6273
63- /** Returns LocalDate / LocalTime / LocalDateTime, or null for non-temporal types. */
64- private static Object normalizeTemporal (ArrowType type , Object value ) {
74+ /** ISO-T temporal → space separator; other strings unchanged. */
75+ private static String spaceSeparator (String s ) {
76+ if (s == null ) return null ;
77+ var m = ISO_TIMESTAMP_T .matcher (s );
78+ return m .matches () ? m .group (1 ) + " " + m .group (2 ) : s ;
79+ }
80+
81+ private static Object formatTemporal (ArrowType type , Object value ) {
6582 if (value == null ) return null ;
6683 if (type instanceof ArrowType .Date date ) {
67- return toLocalDate (date , value );
84+ return formatDate (date , value );
6885 }
6986 if (type instanceof ArrowType .Time time ) {
70- return toLocalTime (time , value );
87+ return formatTime (time , value );
7188 }
7289 if (type instanceof ArrowType .Timestamp ts ) {
73- return toLocalDateTime (ts , value );
90+ return formatTimestamp (ts , value );
7491 }
7592 return null ;
7693 }
7794
78- private static LocalDate toLocalDate (ArrowType .Date type , Object value ) {
79- if (value instanceof LocalDate ld ) return ld ;
80- if (value instanceof LocalDateTime ldt ) return ldt .toLocalDate ();
81- long raw = ((Number ) value ).longValue ();
82- return switch (type .getUnit ()) {
83- case DAY -> LocalDate .ofEpochDay (raw );
84- case MILLISECOND -> LocalDate .ofEpochDay (Math .floorDiv (raw , 86_400_000L ));
85- };
95+ private static String formatDate (ArrowType .Date type , Object value ) {
96+ LocalDate ld ;
97+ if (value instanceof LocalDate d ) {
98+ ld = d ;
99+ } else if (value instanceof LocalDateTime ldt ) {
100+ ld = ldt .toLocalDate ();
101+ } else {
102+ long raw = ((Number ) value ).longValue ();
103+ ld = switch (type .getUnit ()) {
104+ case DAY -> LocalDate .ofEpochDay (raw );
105+ case MILLISECOND -> LocalDate .ofEpochDay (Math .floorDiv (raw , 86_400_000L ));
106+ };
107+ }
108+ return ld .format (DateTimeFormatter .ISO_LOCAL_DATE );
86109 }
87110
88- private static LocalTime toLocalTime (ArrowType .Time type , Object value ) {
89- if (value instanceof LocalTime lt ) return lt ;
90- if (value instanceof LocalDateTime ldt ) return ldt .toLocalTime ();
91- long raw = ((Number ) value ).longValue ();
92- long nanoOfDay = switch (type .getUnit ()) {
93- case SECOND -> raw * 1_000_000_000L ;
94- case MILLISECOND -> raw * 1_000_000L ;
95- case MICROSECOND -> raw * 1_000L ;
96- case NANOSECOND -> raw ;
97- };
98- return LocalTime .ofNanoOfDay (nanoOfDay );
111+ /** Time -> HH:mm:ss[.frac]; never prefixes with the 1970 epoch date. */
112+ private static String formatTime (ArrowType .Time type , Object value ) {
113+ LocalTime lt ;
114+ if (value instanceof LocalTime t ) {
115+ lt = t ;
116+ } else if (value instanceof LocalDateTime ldt ) {
117+ lt = ldt .toLocalTime ();
118+ } else {
119+ long raw = ((Number ) value ).longValue ();
120+ long nanoOfDay = switch (type .getUnit ()) {
121+ case SECOND -> raw * 1_000_000_000L ;
122+ case MILLISECOND -> raw * 1_000_000L ;
123+ case MICROSECOND -> raw * 1_000L ;
124+ case NANOSECOND -> raw ;
125+ };
126+ lt = LocalTime .ofNanoOfDay (nanoOfDay );
127+ }
128+ return lt .getNano () == 0 ? lt .format (TIME_NO_NANO ) : lt .format (TIME_WITH_NANO );
99129 }
100130
101- private static LocalDateTime toLocalDateTime (ArrowType .Timestamp type , Object value ) {
102- if (value instanceof LocalDateTime ldt ) return ldt ;
103- long raw = ((Number ) value ).longValue ();
104- Instant instant = switch (type .getUnit ()) {
105- case SECOND -> Instant .ofEpochSecond (raw );
106- case MILLISECOND -> Instant .ofEpochMilli (raw );
107- case MICROSECOND -> Instant .ofEpochSecond (Math .floorDiv (raw , 1_000_000L ), Math .floorMod (raw , 1_000_000L ) * 1_000L );
108- case NANOSECOND -> Instant .ofEpochSecond (Math .floorDiv (raw , 1_000_000_000L ), Math .floorMod (raw , 1_000_000_000L ));
109- };
110- return LocalDateTime .ofInstant (instant , ZoneOffset .UTC );
131+ /** Timestamp -> yyyy-MM-dd HH:mm:ss[.frac]. */
132+ private static String formatTimestamp (ArrowType .Timestamp type , Object value ) {
133+ LocalDateTime ldt ;
134+ if (value instanceof LocalDateTime t ) {
135+ ldt = t ;
136+ } else if (value instanceof LocalDate ld ) {
137+ ldt = ld .atStartOfDay ();
138+ } else {
139+ long raw = ((Number ) value ).longValue ();
140+ Instant instant = switch (type .getUnit ()) {
141+ case SECOND -> Instant .ofEpochSecond (raw );
142+ case MILLISECOND -> Instant .ofEpochMilli (raw );
143+ case MICROSECOND -> Instant .ofEpochSecond (Math .floorDiv (raw , 1_000_000L ), Math .floorMod (raw , 1_000_000L ) * 1_000L );
144+ case NANOSECOND -> Instant .ofEpochSecond (Math .floorDiv (raw , 1_000_000_000L ), Math .floorMod (raw , 1_000_000_000L ));
145+ };
146+ ldt = LocalDateTime .ofInstant (instant , ZoneOffset .UTC );
147+ }
148+ return ldt .getNano () == 0 ? ldt .format (TIMESTAMP_NO_NANO ) : ldt .format (TIMESTAMP_WITH_NANO );
111149 }
112150
113151 private static Object normalize (Object value ) {
114152 if (value instanceof Text t ) {
115- return t .toString ();
153+ return spaceSeparator (t .toString ());
154+ }
155+ if (value instanceof String s ) {
156+ return spaceSeparator (s );
116157 }
117158 if (value instanceof List <?> list ) {
118- return normalizeList (list );
159+ return normalizeList (list , null );
119160 }
120161 if (value instanceof Map <?, ?> m ) {
121162 LinkedHashMap <String , Object > out = new LinkedHashMap <>(m .size ());
@@ -128,10 +169,12 @@ private static Object normalize(Object value) {
128169 return value ;
129170 }
130171
131- private static List <Object > normalizeList (List <?> raw ) {
172+ private static List <Object > normalizeList (List <?> raw , Field childField ) {
173+ ArrowType childType = childField == null ? null : childField .getType ();
132174 List <Object > out = new ArrayList <>(raw .size ());
133175 for (Object element : raw ) {
134- out .add (normalize (element ));
176+ Object formatted = childType == null ? null : formatTemporal (childType , element );
177+ out .add (formatted != null ? formatted : normalize (element ));
135178 }
136179 return out ;
137180 }
0 commit comments