1616
1717package com .google .cloud .bigquery ;
1818
19+ import static com .google .cloud .bigquery .QueryParameterValue .TIMESTAMP_FORMATTER ;
1920import static com .google .common .truth .Truth .assertThat ;
20- import static java .time .temporal .ChronoField .HOUR_OF_DAY ;
21- import static java .time .temporal .ChronoField .MINUTE_OF_HOUR ;
22- import static java .time .temporal .ChronoField .NANO_OF_SECOND ;
23- import static java .time .temporal .ChronoField .SECOND_OF_MINUTE ;
21+ import static org .junit .Assert .assertThrows ;
2422
2523import com .google .api .services .bigquery .model .QueryParameterType ;
2624import com .google .common .collect .ImmutableMap ;
2927import java .text .ParseException ;
3028import java .time .Instant ;
3129import java .time .Period ;
32- import java .time .ZoneOffset ;
33- import java .time .format .DateTimeFormatter ;
34- import java .time .format .DateTimeFormatterBuilder ;
3530import java .util .ArrayList ;
3631import java .util .Date ;
3732import java .util .HashMap ;
4338
4439public class QueryParameterValueTest {
4540
46- private static final DateTimeFormatter TIMESTAMPFORMATTER =
47- new DateTimeFormatterBuilder ()
48- .parseLenient ()
49- .append (DateTimeFormatter .ISO_LOCAL_DATE )
50- .appendLiteral (' ' )
51- .appendValue (HOUR_OF_DAY , 2 )
52- .appendLiteral (':' )
53- .appendValue (MINUTE_OF_HOUR , 2 )
54- .optionalStart ()
55- .appendLiteral (':' )
56- .appendValue (SECOND_OF_MINUTE , 2 )
57- .optionalStart ()
58- .appendFraction (NANO_OF_SECOND , 6 , 9 , true )
59- .optionalStart ()
60- .appendOffset ("+HHMM" , "+00:00" )
61- .optionalEnd ()
62- .toFormatter ()
63- .withZone (ZoneOffset .UTC );
64-
6541 private static final QueryParameterValue QUERY_PARAMETER_VALUE =
6642 QueryParameterValue .newBuilder ()
6743 .setType (StandardSQLTypeName .STRING )
@@ -326,6 +302,7 @@ public void testStringArray() {
326302
327303 @ Test
328304 public void testTimestampFromLong () {
305+ // Expects output to be ISO8601 string with microsecond precision
329306 QueryParameterValue value = QueryParameterValue .timestamp (1408452095220000L );
330307 assertThat (value .getValue ()).isEqualTo ("2014-08-19 12:41:35.220000+00:00" );
331308 assertThat (value .getType ()).isEqualTo (StandardSQLTypeName .TIMESTAMP );
@@ -340,18 +317,52 @@ public void testTimestampWithFormatter() {
340317 long secs = Math .floorDiv (timestampInMicroseconds , microseconds );
341318 int nano = (int ) Math .floorMod (timestampInMicroseconds , microseconds ) * 1000 ;
342319 Instant instant = Instant .ofEpochSecond (secs , nano );
343- String expected = TIMESTAMPFORMATTER .format (instant );
320+ String expected = TIMESTAMP_FORMATTER .format (instant );
344321 assertThat (expected )
345322 .isEqualTo (QueryParameterValue .timestamp (timestampInMicroseconds ).getValue ());
346323 }
347324
348325 @ Test
349- public void testTimestamp () {
350- QueryParameterValue value = QueryParameterValue .timestamp ("2014-08-19 12:41:35.220000+00:00" );
351- assertThat (value .getValue ()).isEqualTo ("2014-08-19 12:41:35.220000+00:00" );
352- assertThat (value .getType ()).isEqualTo (StandardSQLTypeName .TIMESTAMP );
353- assertThat (value .getArrayType ()).isNull ();
354- assertThat (value .getArrayValues ()).isNull ();
326+ public void testTimestampFromString () {
327+ // QueryParameterValue value = QueryParameterValue.timestamp("2014-08-19
328+ // 12:41:35.220000+00:00");
329+ // assertThat(value.getValue()).isEqualTo("2014-08-19 12:41:35.220000+00:00");
330+ // assertThat(value.getType()).isEqualTo(StandardSQLTypeName.TIMESTAMP);
331+ // assertThat(value.getArrayType()).isNull();
332+ // assertThat(value.getArrayValues()).isNull();
333+ //
334+ // QueryParameterValue value1 =
335+ // QueryParameterValue.timestamp("2025-08-19 12:34:56.123456789+00:00");
336+ // assertThat(value1.getValue()).isEqualTo("2025-08-19 12:34:56.123456789+00:00");
337+ // assertThat(value1.getType()).isEqualTo(StandardSQLTypeName.TIMESTAMP);
338+ // assertThat(value1.getArrayType()).isNull();
339+ // assertThat(value1.getArrayValues()).isNull();
340+
341+ // The following test cases test more than nanosecond precision
342+ // 10 digits of precision (1 digit more than nanosecond)
343+ QueryParameterValue value2 =
344+ QueryParameterValue .timestamp ("2025-12-08 12:34:56.1234567890+00:00" );
345+ assertThat (value2 .getValue ()).isEqualTo ("2025-12-08 12:34:56.1234567890+00:00" );
346+ assertThat (value2 .getType ()).isEqualTo (StandardSQLTypeName .TIMESTAMP );
347+ assertThat (value2 .getArrayType ()).isNull ();
348+ assertThat (value2 .getArrayValues ()).isNull ();
349+
350+ // 12 digits (picosecond precision)
351+ QueryParameterValue value3 =
352+ QueryParameterValue .timestamp ("2025-12-08 12:34:56.123456789123+00:00" );
353+ assertThat (value3 .getValue ()).isEqualTo ("2025-12-08 12:34:56.123456789123+00:00" );
354+ assertThat (value3 .getType ()).isEqualTo (StandardSQLTypeName .TIMESTAMP );
355+ assertThat (value3 .getArrayType ()).isNull ();
356+ assertThat (value3 .getArrayValues ()).isNull ();
357+
358+ // More than picosecond precision
359+ assertThrows (
360+ IllegalArgumentException .class ,
361+ () -> QueryParameterValue .timestamp ("2025-12-08 12:34:56.1234567891234+00:00" ));
362+ assertThrows (
363+ IllegalArgumentException .class ,
364+ () ->
365+ QueryParameterValue .timestamp ("2025-12-08 12:34:56.123456789123456789123456789+00:00" ));
355366 }
356367
357368 @ Test
@@ -373,10 +384,31 @@ public void testTimestampWithDateTimeFormatterBuilder() {
373384 assertThat (value2 .getArrayValues ()).isNull ();
374385 }
375386
376- @ Test (expected = IllegalArgumentException .class )
377- public void testInvalidTimestamp () {
387+ @ Test
388+ public void testInvalidTimestampStringValues () {
389+ assertThrows (IllegalArgumentException .class , () -> QueryParameterValue .timestamp ("abc" ));
390+
378391 // missing the time
379- QueryParameterValue .timestamp ("2014-08-19" );
392+ assertThrows (IllegalArgumentException .class , () -> QueryParameterValue .timestamp ("2014-08-19" ));
393+
394+ // missing the hour
395+ assertThrows (
396+ IllegalArgumentException .class , () -> QueryParameterValue .timestamp ("2014-08-19 12" ));
397+
398+ // can't have the 'T' separator
399+ assertThrows (
400+ IllegalArgumentException .class , () -> QueryParameterValue .timestamp ("2014-08-19T12" ));
401+ assertThrows (
402+ IllegalArgumentException .class ,
403+ () -> QueryParameterValue .timestamp ("2014-08-19T12:34:00.123456" ));
404+
405+ // Fractional part has picosecond length, but fractional part is not a valid number
406+ assertThrows (
407+ IllegalArgumentException .class ,
408+ () -> QueryParameterValue .timestamp ("2014-08-19T12:34:00.123456789abc+00:00" ));
409+ assertThrows (
410+ IllegalArgumentException .class ,
411+ () -> QueryParameterValue .timestamp ("2014-08-19T12:34:00.123456abc789+00:00" ));
380412 }
381413
382414 @ Test
0 commit comments