Skip to content

Commit 57bbcf6

Browse files
authored
Support interval type (#228)
* Support Interval type * Modify interval input format * Modify interval type output format as period * Support Interval type with pg format * Add Interval type to pg_type table * Convert '= ANY' to 'IN' * Support prepared statement
1 parent 8c335de commit 57bbcf6

File tree

14 files changed

+187
-22
lines changed

14 files changed

+187
-22
lines changed

graphmdl-base/src/main/java/io/graphmdl/base/type/IntervalType.java

+61-8
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import io.netty.buffer.ByteBuf;
1818
import org.joda.time.Period;
19+
import org.joda.time.format.PeriodFormatter;
20+
import org.joda.time.format.PeriodFormatterBuilder;
1921

2022
import javax.annotation.Nonnull;
2123

@@ -30,6 +32,48 @@ public class IntervalType
3032
private static final int TYPE_LEN = 16;
3133
private static final int TYPE_MOD = -1;
3234
public static final IntervalType INTERVAL = new IntervalType();
35+
private static final PeriodFormatter DAY_FORMATTER = new PeriodFormatterBuilder()
36+
.appendYears()
37+
.appendSuffix(" year", " years")
38+
.appendSeparator(" ")
39+
.appendMonths()
40+
.appendSuffix(" mon", " mons")
41+
.appendSeparator(" ")
42+
.appendWeeks()
43+
.appendSuffix(" weeks")
44+
.appendSeparator(" ")
45+
.appendDays()
46+
.appendSuffix(" day", " days")
47+
.toFormatter();
48+
49+
private static final PeriodFormatter TIME_FORMATTER = new PeriodFormatterBuilder()
50+
.printZeroAlways()
51+
.minimumPrintedDigits(2)
52+
.appendHours()
53+
.appendSeparator(":")
54+
.minimumPrintedDigits(2)
55+
.printZeroAlways()
56+
.appendMinutes()
57+
.appendSeparator(":")
58+
.minimumPrintedDigits(2)
59+
.printZeroAlways()
60+
.appendSecondsWithOptionalMillis()
61+
.toFormatter();
62+
63+
private static final PeriodFormatter PG_INTERVAL_FORMATTER = new PeriodFormatterBuilder()
64+
.appendYears()
65+
.appendSuffix(" years ")
66+
.appendMonths()
67+
.appendSuffix(" mons ")
68+
.appendDays()
69+
.appendSuffix(" days ")
70+
.appendHours()
71+
.appendSuffix(" hours ")
72+
.appendMinutes()
73+
.appendSuffix(" mins ")
74+
.appendSecondsWithOptionalMillis()
75+
.appendSuffix(" secs")
76+
.toFormatter();
3377

3478
private IntervalType()
3579
{
@@ -39,9 +83,7 @@ private IntervalType()
3983
@Override
4084
public int typArray()
4185
{
42-
// TODO support interval
43-
// return PGArray.INTERVAL_ARRAY.oid();
44-
throw new UnsupportedOperationException("no implementation");
86+
return PGArray.INTERVAL_ARRAY.oid();
4587
}
4688

4789
@Override
@@ -107,15 +149,26 @@ public Period readBinaryValue(ByteBuf buffer, int valueLength)
107149
@Override
108150
public byte[] encodeAsUTF8Text(@Nonnull Period value)
109151
{
110-
// StringBuilder sb = new StringBuilder();
111-
// IntervalType.PERIOD_FORMATTER.printTo(sb, (ReadablePeriod) value);
112-
return value.toString().getBytes(StandardCharsets.UTF_8);
152+
StringBuilder sb = new StringBuilder();
153+
sb.append(DAY_FORMATTER.print(value));
154+
sb.append(" ");
155+
// the negative sign need to be placed before the time, like -00:00:01
156+
if (value.getHours() < 0 || value.getMinutes() < 0 || value.getSeconds() < 0 || value.getMillis() < 0) {
157+
sb.append("-");
158+
}
159+
Period absValue = new Period(
160+
Math.abs(value.getHours()),
161+
Math.abs(value.getMinutes()),
162+
Math.abs(value.getSeconds()),
163+
Math.abs(value.getMillis()));
164+
sb.append(TIME_FORMATTER.print(absValue));
165+
166+
return sb.toString().replace("00:00:00", "").trim().getBytes(StandardCharsets.UTF_8);
113167
}
114168

115169
@Override
116170
public Period decodeUTF8Text(byte[] bytes)
117171
{
118-
// return IntervalType.PERIOD_FORMATTER.parsePeriod(new String(bytes, StandardCharsets.UTF_8));
119-
return new Period(new String(bytes, StandardCharsets.UTF_8));
172+
return PG_INTERVAL_FORMATTER.parsePeriod(new String(bytes, StandardCharsets.UTF_8));
120173
}
121174
}

graphmdl-base/src/main/java/io/graphmdl/base/type/PGArray.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ public class PGArray
5555
public static final PGArray EMPTY_RECORD_ARRAY = new PGArray(2287, RecordType.EMPTY_RECORD);
5656
public static final PGArray HSTORE_ARRAY = new PGArray(57645, HstoreType.HSTORE);
5757
public static final PGArray REGPROC_ARRAY = new PGArray(1008, RegprocType.REGPROC);
58+
public static final PGArray INTERVAL_ARRAY = new PGArray(1187, IntervalType.INTERVAL);
5859

5960
// TODO:
60-
// public static final PGArray INTERVAL_ARRAY = new PGArray(1187, IntervalType.INSTANCE);
6161
// public static final PGArray TIMETZ_ARRAY = new PGArray(1270, TimeTZType.INSTANCE);
6262
// public static final PGArray POINT_ARRAY = new PGArray(1017, PointType.INSTANCE);
6363
// public static final PGArray ANY_ARRAY = new PGArray(

graphmdl-base/src/main/java/io/graphmdl/base/type/PGTypes.java

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static io.graphmdl.base.type.DateType.DATE;
2828
import static io.graphmdl.base.type.HstoreType.HSTORE;
2929
import static io.graphmdl.base.type.IntegerType.INTEGER;
30+
import static io.graphmdl.base.type.IntervalType.INTERVAL;
3031
import static io.graphmdl.base.type.RealType.REAL;
3132
import static io.graphmdl.base.type.SmallIntType.SMALLINT;
3233
import static io.graphmdl.base.type.TimestampType.TIMESTAMP;
@@ -69,6 +70,7 @@ private PGTypes() {}
6970
// Just need a fake instance to do type mapping. We never use the field.
7071
TYPE_TABLE.put(HSTORE.oid(), HSTORE);
7172
TYPE_TABLE.put(UuidType.UUID.oid(), UuidType.UUID);
73+
TYPE_TABLE.put(INTERVAL.oid(), INTERVAL);
7274

7375
ImmutableMap.Builder<Integer, PGArray> innerToPgTypeBuilder = ImmutableMap.builder();
7476
// initial collection types

graphmdl-connector-client/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,9 @@
9090
<groupId>javax.ws.rs</groupId>
9191
<artifactId>javax.ws.rs-api</artifactId>
9292
</dependency>
93+
<dependency>
94+
<groupId>joda-time</groupId>
95+
<artifactId>joda-time</artifactId>
96+
</dependency>
9397
</dependencies>
9498
</project>

graphmdl-connector-client/src/main/java/io/graphmdl/connector/bigquery/BigQueryType.java

+8
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
import com.google.common.collect.ImmutableMap;
1919
import io.graphmdl.base.GraphMDLException;
2020
import io.graphmdl.base.type.DateType;
21+
import io.graphmdl.base.type.IntervalType;
2122
import io.graphmdl.base.type.NumericType;
2223
import io.graphmdl.base.type.PGType;
2324
import io.graphmdl.base.type.TimestampType;
25+
import org.joda.time.Period;
2426

2527
import java.util.Map;
2628
import java.util.Optional;
@@ -31,6 +33,7 @@
3133
import static com.google.cloud.bigquery.StandardSQLTypeName.DATE;
3234
import static com.google.cloud.bigquery.StandardSQLTypeName.FLOAT64;
3335
import static com.google.cloud.bigquery.StandardSQLTypeName.INT64;
36+
import static com.google.cloud.bigquery.StandardSQLTypeName.INTERVAL;
3437
import static com.google.cloud.bigquery.StandardSQLTypeName.JSON;
3538
import static com.google.cloud.bigquery.StandardSQLTypeName.NUMERIC;
3639
import static com.google.cloud.bigquery.StandardSQLTypeName.STRING;
@@ -65,6 +68,7 @@ private BigQueryType() {}
6568
.put(NUMERIC, NumericType.NUMERIC)
6669
.put(BIGNUMERIC, NumericType.NUMERIC)
6770
.put(JSON, VARCHAR)
71+
.put(INTERVAL, IntervalType.INTERVAL)
6872
.build();
6973

7074
pgTypeToBqTypeMap = ImmutableMap.<PGType<?>, StandardSQLTypeName>builder()
@@ -80,6 +84,7 @@ private BigQueryType() {}
8084
.put(DateType.DATE, DATE)
8185
.put(TimestampType.TIMESTAMP, TIMESTAMP)
8286
.put(BYTEA, BYTES)
87+
.put(IntervalType.INTERVAL, INTERVAL)
8388
.build();
8489
}
8590

@@ -102,6 +107,9 @@ public static Object toBqValue(PGType<?> pgType, Object value)
102107
if (pgType.equals(SMALLINT) && value instanceof Short) {
103108
return ((Short) value).intValue();
104109
}
110+
if (pgType.equals(IntervalType.INTERVAL) && value instanceof Period) {
111+
return value.toString();
112+
}
105113
return value;
106114
}
107115
}

graphmdl-main/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@
143143
<artifactId>javax.ws.rs-api</artifactId>
144144
</dependency>
145145

146+
<dependency>
147+
<groupId>joda-time</groupId>
148+
<artifactId>joda-time</artifactId>
149+
</dependency>
150+
146151
<dependency>
147152
<groupId>org.apache.commons</groupId>
148153
<artifactId>commons-lang3</artifactId>

graphmdl-main/src/main/java/io/graphmdl/main/connector/bigquery/BigQueryRecordIterator.java

+31
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,18 @@
2323
import io.graphmdl.base.type.PGType;
2424
import io.graphmdl.base.type.PGTypes;
2525
import io.graphmdl.connector.bigquery.BigQueryType;
26+
import org.apache.commons.lang3.StringUtils;
27+
import org.joda.time.Period;
2628

2729
import java.util.Iterator;
2830
import java.util.List;
2931
import java.util.concurrent.atomic.AtomicInteger;
32+
import java.util.regex.Matcher;
33+
import java.util.regex.Pattern;
3034

3135
import static com.google.common.collect.ImmutableList.toImmutableList;
36+
import static java.lang.String.format;
37+
import static java.util.Locale.ENGLISH;
3238
import static java.util.Objects.requireNonNull;
3339

3440
public class BigQueryRecordIterator
@@ -117,6 +123,8 @@ private Object getFieldValue(StandardSQLTypeName typeName, FieldValue fieldValue
117123
case NUMERIC:
118124
case BIGNUMERIC:
119125
return fieldValue.getNumericValue();
126+
case INTERVAL:
127+
return convertBigQueryIntervalToPeriod(fieldValue.getStringValue());
120128
default:
121129
throw new IllegalArgumentException("Unsupported type: " + typeName);
122130
}
@@ -126,4 +134,27 @@ public List<PGType> getTypes()
126134
{
127135
return types;
128136
}
137+
138+
private Period convertBigQueryIntervalToPeriod(String value)
139+
{
140+
// BigQuery interval format: [sign]Y-M [sign]D [sign]H:M:S[.F], and F up to six digits
141+
Pattern pattern = Pattern.compile("(?<NEG>-?)(?<Y>[0-9]+)-(?<M>[0-9]+) (?<D>-?[0-9]+) (?<NEGTIME>-?)(?<H>[0-9]+):(?<MIN>[0-9]+):(?<S>[0-9]+).?(?<F>[0-9]{1,6})?");
142+
Matcher matcher = pattern.matcher(value);
143+
if (!matcher.matches()) {
144+
throw new IllegalArgumentException(format(ENGLISH, "Invalid interval format: %s", value));
145+
}
146+
int sign = matcher.group("NEG").isEmpty() ? 1 : -1;
147+
int year = Integer.parseInt(matcher.group("Y")) * sign;
148+
int mon = Integer.parseInt(matcher.group("M")) * sign;
149+
int day = Integer.parseInt(matcher.group("D"));
150+
int signTime = matcher.group("NEGTIME").isEmpty() ? 1 : -1;
151+
int hour = Integer.parseInt(matcher.group("H")) * signTime;
152+
int min = Integer.parseInt(matcher.group("MIN")) * signTime;
153+
int sec = Integer.parseInt(matcher.group("S")) * signTime;
154+
if (matcher.group("F") == null) {
155+
return new Period(year, mon, 0, day, hour, min, sec, 0);
156+
}
157+
String micro = StringUtils.rightPad(matcher.group("F"), 6, '0');
158+
return new Period(year, mon, 0, day, hour, min, sec, Integer.parseInt(micro) / 1000);
159+
}
129160
}

graphmdl-main/src/main/java/io/graphmdl/main/sql/bigquery/RewriteToBigQueryType.java

+2
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ private GenericDataType toBigQueryGenericDataType(GenericDataType genericDataTyp
152152
return new GenericDataType(nodeLocation, new Identifier("ARRAY"), parameters);
153153
case "DATE":
154154
return new GenericDataType(nodeLocation, new Identifier("DATE"), parameters);
155+
case "INTERVAL":
156+
return new GenericDataType(nodeLocation, new Identifier("INTERVAL"), parameters);
155157
default:
156158
throw new UnsupportedOperationException("Unsupported type: " + typeName);
157159
}

graphmdl-tests/src/main/java/io/graphmdl/testing/DataType.java

+9
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.function.Function;
3838

3939
import static io.graphmdl.base.type.DateType.DATE;
40+
import static io.graphmdl.base.type.IntervalType.INTERVAL;
4041
import static io.graphmdl.base.type.TimestampType.TIMESTAMP;
4142
import static java.lang.String.format;
4243
import static java.time.temporal.ChronoField.NANO_OF_SECOND;
@@ -203,6 +204,14 @@ public static DataType<LocalDateTime> timestampDataType(int precision)
203204
format.toFormatter()::format);
204205
}
205206

207+
public static DataType<String> intervalType()
208+
{
209+
return dataType(
210+
"interval",
211+
INTERVAL,
212+
value -> format("INTERVAL %s", value));
213+
}
214+
206215
public static String formatStringLiteral(String value)
207216
{
208217
return "'" + value.replace("'", "''") + "'";

graphmdl-tests/src/test/java/io/graphmdl/testing/bigquery/TestJdbcResultSet.java

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import com.google.common.collect.ImmutableMap;
1818
import io.graphmdl.testing.AbstractWireProtocolTest;
19+
import org.postgresql.util.PGInterval;
1920
import org.testng.annotations.AfterMethod;
2021
import org.testng.annotations.BeforeMethod;
2122
import org.testng.annotations.Test;
@@ -131,6 +132,11 @@ public void testObjectTypesSpecific()
131132
checkRepresentation("UUID '0397e63b-2b78-4b7b-9c87-e085fa225dd8'", Types.VARCHAR, "0397e63b-2b78-4b7b-9c87-e085fa225dd8");
132133
checkRepresentation("JSON '{\"name\":\"alice\"}'", Types.VARCHAR, "{\"name\":\"alice\"}");
133134

135+
PGInterval intervalObj = new PGInterval();
136+
intervalObj.setType("interval");
137+
intervalObj.setValue("1 year");
138+
checkRepresentation("INTERVAL '1' year", Types.OTHER, intervalObj);
139+
134140
checkRepresentation("DATE '2018-02-13'", Types.DATE, (rs, column) -> {
135141
assertEquals(rs.getObject(column), Date.valueOf(LocalDate.of(2018, 2, 13)));
136142
assertEquals(rs.getDate(column), Date.valueOf(LocalDate.of(2018, 2, 13)));

graphmdl-tests/src/test/java/io/graphmdl/testing/bigquery/TestWireProtocolType.java

+29-6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.graphmdl.base.type.PGTypes;
2020
import io.graphmdl.testing.AbstractWireProtocolTest;
2121
import io.graphmdl.testing.DataType;
22+
import org.postgresql.util.PGInterval;
2223
import org.postgresql.util.PGobject;
2324
import org.testng.annotations.Test;
2425

@@ -52,6 +53,7 @@
5253
import static io.graphmdl.testing.DataType.decimalDataType;
5354
import static io.graphmdl.testing.DataType.doubleDataType;
5455
import static io.graphmdl.testing.DataType.integerDataType;
56+
import static io.graphmdl.testing.DataType.intervalType;
5557
import static io.graphmdl.testing.DataType.jsonDataType;
5658
import static io.graphmdl.testing.DataType.nameDataType;
5759
import static io.graphmdl.testing.DataType.realDataType;
@@ -280,6 +282,33 @@ public void testTimestamp()
280282
.executeSuite();
281283
}
282284

285+
@Test
286+
public void testInterval()
287+
{
288+
createTypeTest()
289+
.addInput(intervalType(), "'1' year", value -> toPGInterval("1 year"))
290+
.addInput(intervalType(), "'-5' DAY", value -> toPGInterval("-5 days"))
291+
.addInput(intervalType(), "'-1' SECOND", value -> toPGInterval("-00:00:01"))
292+
.addInput(intervalType(), "'-25' MONTH", value -> toPGInterval("-2 years -1 mons"))
293+
.addInput(intervalType(), "'10:20:30.52' HOUR TO SECOND", value -> toPGInterval("10:20:30.520"))
294+
.addInput(intervalType(), "'1-2' YEAR TO MONTH", value -> toPGInterval("1 year 2 mons"))
295+
.addInput(intervalType(), "'1 5:30' DAY TO MINUTE", value -> toPGInterval("1 day 05:30:00"))
296+
.executeSuite();
297+
}
298+
299+
private static PGobject toPGInterval(String value)
300+
{
301+
try {
302+
PGInterval pgInterval = new PGInterval();
303+
pgInterval.setType("interval");
304+
pgInterval.setValue(value);
305+
return pgInterval;
306+
}
307+
catch (SQLException e) {
308+
throw new RuntimeException(e);
309+
}
310+
}
311+
283312
private static void checkIsGap(ZoneId zone, LocalDateTime dateTime)
284313
{
285314
verify(isGap(zone, dateTime), "Expected %s to be a gap in %s", dateTime, zone);
@@ -377,12 +406,6 @@ private void execute(int rowCopies)
377406
if (actual instanceof Array) {
378407
assertArrayEquals((Array) actual, (List<?>) expectedResults.get(i), expectedTypeName.get(i));
379408
}
380-
else if (expectedResults.get(i) instanceof PGobject) {
381-
PGobject expected = (PGobject) expectedResults.get(i);
382-
if ("inet".equals(expected.getType())) {
383-
assertThat(actual).isEqualTo(expected.getValue());
384-
}
385-
}
386409
else if (TYPE_FORCED_TO_LONG.contains(expectedTypeName.get(i))) {
387410
assertThat(Long.valueOf((long) actual).intValue()).isEqualTo(expectedResults.get(i));
388411
}

graphmdl-tests/src/test/java/io/graphmdl/testing/bigquery/TestWireProtocolWithBigquery.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.graphmdl.testing.TestingWireProtocolClient;
2323
import org.assertj.core.api.AssertionsForClassTypes;
2424
import org.intellij.lang.annotations.Language;
25+
import org.postgresql.util.PGInterval;
2526
import org.testng.annotations.DataProvider;
2627
import org.testng.annotations.Test;
2728

@@ -887,7 +888,8 @@ public Object[][] paramTypes()
887888
// TODO support timestamptz
888889
// {"timestamptz", ZonedDateTime.of(LocalDateTime.of(1900, 1, 3, 12, 10, 16, 123000000), ZoneId.of("America/Los_Angeles"))},
889890
{"json", "{\"test\":3, \"test2\":4}"},
890-
{"bytea", "test1".getBytes(UTF_8)}
891+
{"bytea", "test1".getBytes(UTF_8)},
892+
{"interval", new PGInterval(1, 5, -3, 7, 55, 20)}
891893

892894
// TODO: type support
893895
// {"any", new Object[] {1, "test", new BigDecimal(10)}}

0 commit comments

Comments
 (0)