From commits-return-6797-apmail-drill-commits-archive=drill.apache.org@drill.apache.org Wed May 29 23:08:39 2019 Return-Path: X-Original-To: apmail-drill-commits-archive@www.apache.org Delivered-To: apmail-drill-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [207.244.88.153]) by minotaur.apache.org (Postfix) with SMTP id DF30018778 for ; Wed, 29 May 2019 23:08:38 +0000 (UTC) Received: (qmail 41124 invoked by uid 500); 29 May 2019 23:08:37 -0000 Delivered-To: apmail-drill-commits-archive@drill.apache.org Received: (qmail 41017 invoked by uid 500); 29 May 2019 23:08:37 -0000 Mailing-List: contact commits-help@drill.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: commits@drill.apache.org Delivered-To: mailing list commits@drill.apache.org Received: (qmail 40931 invoked by uid 99); 29 May 2019 23:08:37 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 29 May 2019 23:08:37 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 9AD378A4BF; Wed, 29 May 2019 23:08:37 +0000 (UTC) Date: Wed, 29 May 2019 23:08:41 +0000 To: "commits@drill.apache.org" Subject: [drill] 05/05: DRILL-4782 / DRILL-7139: Fix DATE_ADD and TO_TIME functions MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit From: sachouche@apache.org In-Reply-To: <155917131647.31483.2261075673997320518@gitbox.apache.org> References: <155917131647.31483.2261075673997320518@gitbox.apache.org> X-Git-Host: gitbox.apache.org X-Git-Repo: drill X-Git-Refname: refs/heads/master X-Git-Reftype: branch X-Git-Rev: 2615d68de4e44b1f03f5c047018548c06a7396b4 X-Git-NotificationType: diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated Message-Id: <20190529230837.9AD378A4BF@gitbox.apache.org> This is an automated email from the ASF dual-hosted git repository. sachouche pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/drill.git commit 2615d68de4e44b1f03f5c047018548c06a7396b4 Author: Dmytriy Grinchenko AuthorDate: Mon May 6 09:01:29 2019 +0300 DRILL-4782 / DRILL-7139: Fix DATE_ADD and TO_TIME functions - cast function for the day interval changed to round milliseconds to complete days - ToDateTypeFunctions#toTime now returning milliseconds of day - updated the way how DayInterval subtracts and adds, to follow the cast function logic UT core updates: - added vectorValue function to the queryBuilder to simplify retrieving value of the vector - refactored singleton query result functions at queryBuilder --- .../templates/CastStringTypesToInterval.java | 29 ++-- .../IntervalIntervalArithmetic.java | 22 ++- .../ToDateTypeFunctions.java | 2 +- .../expr/fn/impl/TestIntervalDayFunctions.java | 95 +++++++++++++ .../drill/exec/fn/impl/TestCastFunctions.java | 69 +++++---- .../exec/fn/impl/testing/TestDateConversions.java | 12 ++ .../java/org/apache/drill/test/QueryBuilder.java | 154 +++++++++++++++------ 7 files changed, 304 insertions(+), 79 deletions(-) diff --git a/exec/java-exec/src/main/codegen/templates/CastStringTypesToInterval.java b/exec/java-exec/src/main/codegen/templates/CastStringTypesToInterval.java index 196e860..7acfd69 100644 --- a/exec/java-exec/src/main/codegen/templates/CastStringTypesToInterval.java +++ b/exec/java-exec/src/main/codegen/templates/CastStringTypesToInterval.java @@ -86,24 +86,33 @@ public class CastEmptyString${type.from}To${type.to} implements DrillSimpleFunc org.joda.time.Period period = org.joda.time.Period.parse(input); <#if type.to == "Interval" || type.to == "NullableInterval"> - out.months = (period.getYears() * org.apache.drill.exec.vector.DateUtilities.yearsToMonths) + period.getMonths(); + out.months = period.getYears() * org.apache.drill.exec.vector.DateUtilities.yearsToMonths + period.getMonths(); out.days = period.getDays(); - out.milliseconds = (period.getHours() * org.apache.drill.exec.vector.DateUtilities.hoursToMillis) + - (period.getMinutes() * org.apache.drill.exec.vector.DateUtilities.minutesToMillis) + - (period.getSeconds() * org.apache.drill.exec.vector.DateUtilities.secondsToMillis) + - (period.getMillis()); + out.milliseconds = period.getHours() * org.apache.drill.exec.vector.DateUtilities.hoursToMillis + + period.getMinutes() * org.apache.drill.exec.vector.DateUtilities.minutesToMillis + + period.getSeconds() * org.apache.drill.exec.vector.DateUtilities.secondsToMillis + + period.getMillis(); <#elseif type.to == "IntervalDay" || type.to == "NullableIntervalDay"> out.days = period.getDays(); - out.milliseconds = (period.getHours() * org.apache.drill.exec.vector.DateUtilities.hoursToMillis) + - (period.getMinutes() * org.apache.drill.exec.vector.DateUtilities.minutesToMillis) + - (period.getSeconds() * org.apache.drill.exec.vector.DateUtilities.secondsToMillis) + - (period.getMillis()); + long millis = period.getHours() * (long) org.apache.drill.exec.vector.DateUtilities.hoursToMillis + + period.getMinutes() * (long) org.apache.drill.exec.vector.DateUtilities.minutesToMillis + + period.getSeconds() * (long) org.apache.drill.exec.vector.DateUtilities.secondsToMillis + + period.getMillis(); + + if (millis >= org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis) { + int daysFromMillis = (int) (millis / org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis); + millis -= daysFromMillis * org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis; + out.days += daysFromMillis; + } + + out.milliseconds = (int) millis; + <#elseif type.to == "IntervalYear" || type.to == "NullableIntervalYear"> - out.value = (period.getYears() * org.apache.drill.exec.vector.DateUtilities.yearsToMonths) + period.getMonths(); + out.value = period.getYears() * org.apache.drill.exec.vector.DateUtilities.yearsToMonths + period.getMonths(); } } diff --git a/exec/java-exec/src/main/codegen/templates/DateIntervalFunctionTemplates/IntervalIntervalArithmetic.java b/exec/java-exec/src/main/codegen/templates/DateIntervalFunctionTemplates/IntervalIntervalArithmetic.java index 7fecabb..ccd8e1f 100644 --- a/exec/java-exec/src/main/codegen/templates/DateIntervalFunctionTemplates/IntervalIntervalArithmetic.java +++ b/exec/java-exec/src/main/codegen/templates/DateIntervalFunctionTemplates/IntervalIntervalArithmetic.java @@ -65,7 +65,15 @@ public class ${intervaltype}Functions { out.value = left.value + right.value; <#elseif intervaltype == "IntervalDay"> out.days = left.days + right.days; - out.milliseconds = left.milliseconds + right.milliseconds; + long millis = (long) left.milliseconds + right.milliseconds; + + if (millis >= org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis) { + int daysFromMillis = (int) (millis / org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis); + + millis -= daysFromMillis * org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis; + out.days += daysFromMillis; + } + out.milliseconds = (int) millis; } } @@ -90,6 +98,18 @@ public class ${intervaltype}Functions { <#elseif intervaltype == "IntervalDay"> out.days = left.days - right.days; out.milliseconds = left.milliseconds - right.milliseconds; + + int daysFromMillis = out.milliseconds/org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis; + if (daysFromMillis != 0) { + out.milliseconds -= org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis*daysFromMillis; + out.days -= Math.abs(daysFromMillis); + } + + // if milliseconds are bellow zero, substract them from the days + if (out.milliseconds < 0 && out.days > 0) { + out.days -= 1; + out.milliseconds = org.apache.drill.exec.vector.DateUtilities.daysToStandardMillis + out.milliseconds; + } } } diff --git a/exec/java-exec/src/main/codegen/templates/DateIntervalFunctionTemplates/ToDateTypeFunctions.java b/exec/java-exec/src/main/codegen/templates/DateIntervalFunctionTemplates/ToDateTypeFunctions.java index 6ef196f..7f5784a 100644 --- a/exec/java-exec/src/main/codegen/templates/DateIntervalFunctionTemplates/ToDateTypeFunctions.java +++ b/exec/java-exec/src/main/codegen/templates/DateIntervalFunctionTemplates/ToDateTypeFunctions.java @@ -82,7 +82,7 @@ public class G${convert.from}To${convert.to} implements DrillSimpleFunc { <#elseif convert.to == "TimeStamp" || convert.to == "NullableTimeStamp"> out.value = org.joda.time.DateTime.parse(input, format).withZoneRetainFields(org.joda.time.DateTimeZone.UTC).getMillis(); <#elseif convert.to == "Time" || convert.to == "NullableTime"> - out.value = (int) ((format.parseDateTime(input)).withZoneRetainFields(org.joda.time.DateTimeZone.UTC).getMillis()); + out.value = format.parseDateTime(input).withZoneRetainFields(org.joda.time.DateTimeZone.UTC).getMillisOfDay(); } } diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/expr/fn/impl/TestIntervalDayFunctions.java b/exec/java-exec/src/test/java/org/apache/drill/exec/expr/fn/impl/TestIntervalDayFunctions.java new file mode 100644 index 0000000..2bc29c5 --- /dev/null +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/expr/fn/impl/TestIntervalDayFunctions.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.drill.exec.expr.fn.impl; + +import org.apache.drill.exec.vector.IntervalDayVector; +import org.apache.drill.test.ClusterFixture; +import org.apache.drill.test.ClusterTest; +import org.apache.drill.test.QueryBuilder; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class TestIntervalDayFunctions extends ClusterTest { + @BeforeClass + public static void setUp() throws Exception { + startCluster(ClusterFixture.builder(dirTestWatcher)); + } + + @Test + public void testIntervalDaySubtractFunction() throws Exception { + QueryBuilder.VectorQueryReader vectorQueryReader = + (recordsCount, vector) -> vector.getAccessor().getAsStringBuilder(0).toString(); + + String result = queryBuilder() + .sql("select cast('P6D' as interval day) - cast('P5DT4M20S' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("0 days 23:55:40", result); + + result = queryBuilder() + .sql("select cast('P4D' as interval day) - cast('P4DT4M' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("0 days 0:-4:00", result); + + result = queryBuilder() + .sql("select cast('P4D' as interval day) - cast('PT4M' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("3 days 23:56:00", result); + + result = queryBuilder() + .sql("select cast('P4D' as interval day) - cast('P5D' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("-1 day 0:00:00", result); + + result = queryBuilder() + .sql("select cast('P4D' as interval day) - cast('P4DT23H59M59S' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("0 days -23:-59:-59", result); + + result = queryBuilder() + .sql("select cast('P4D' as interval day) - cast('P5DT23H59S' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("-1 day -23:00:-59", result); + + result = queryBuilder() + .sql("select cast('P2D' as interval day) - cast('P1DT4M' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("0 days 23:56:00", result); + } + + @Test + public void testIntervalDayPlusFunction() throws Exception { + QueryBuilder.VectorQueryReader vectorQueryReader = + (recordsCount, vector) -> vector.getAccessor().getAsStringBuilder(0).toString(); + + String result = queryBuilder() + .sql("select cast('P1D' as interval day) + cast('P2DT1H' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("3 days 1:00:00", result); + + result = queryBuilder() + .sql("select cast('PT12H' as interval day) + cast('PT12H' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("1 day 0:00:00", result); + + result = queryBuilder() + .sql("select cast('PT11H' as interval day) + cast('PT12H59M60S' as interval day) as i") + .vectorValue("i", IntervalDayVector.class, vectorQueryReader); + Assert.assertEquals("1 day 0:00:00", result); + } +} diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/TestCastFunctions.java b/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/TestCastFunctions.java index 73b4b94..fc06cb1 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/TestCastFunctions.java +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/TestCastFunctions.java @@ -30,15 +30,13 @@ import java.util.Set; import org.apache.drill.categories.SqlFunctionTest; import org.apache.drill.categories.UnlikelyTest; import org.apache.drill.common.exceptions.UserRemoteException; -import org.apache.drill.common.expression.SchemaPath; import org.apache.drill.common.types.TypeProtos; import org.apache.drill.common.types.Types; import org.apache.drill.exec.planner.physical.PlannerSettings; import org.apache.drill.exec.record.BatchSchema; import org.apache.drill.exec.record.MaterializedField; -import org.apache.drill.exec.record.RecordBatchLoader; import org.apache.drill.exec.record.metadata.SchemaBuilder; -import org.apache.drill.exec.rpc.user.QueryDataBatch; +import org.apache.drill.exec.vector.IntervalDayVector; import org.apache.drill.exec.vector.IntervalYearVector; import org.apache.drill.test.ClusterFixture; import org.apache.drill.test.ClusterTest; @@ -698,30 +696,53 @@ public class TestCastFunctions extends ClusterTest { @Test // DRILL-6783 public void testCastVarCharIntervalYear() throws Exception { - String query = "select cast('P31M' as interval month) as i from cp.`employee.json` limit 10"; - List result = queryBuilder().sql(query).results(); - RecordBatchLoader loader = new RecordBatchLoader(cluster.drillbit().getContext().getAllocator()); - - QueryDataBatch b = result.get(0); - loader.load(b.getHeader().getDef(), b.getData()); - - IntervalYearVector vector = (IntervalYearVector) loader.getValueAccessorById( - IntervalYearVector.class, - loader.getValueVectorId(SchemaPath.getCompoundPath("i")).getFieldIds()) - .getValueVector(); - - Set resultSet = new HashSet<>(); - for (int i = 0; i < loader.getRecordCount(); i++) { - String displayValue = vector.getAccessor().getAsStringBuilder(i).toString(); - resultSet.add(displayValue); - } + Set results = queryBuilder() + .sql("select cast('P31M' as interval month) as i from cp.`employee.json` limit 10") + .vectorValue( + "i", + IntervalYearVector.class, + (recordCount, vector) -> { + Set r = new HashSet<>(); + for (int i = 0; i < recordCount; i++) { + r.add(vector.getAccessor().getAsStringBuilder(i).toString()); + } + return r; + } + ); Assert.assertEquals( - "Casting literal string as INTERVAL should yield the same result for each row", 1, resultSet.size()); - Assert.assertThat(resultSet, hasItem("2 years 7 months")); + "Casting literal string as INTERVAL should yield the same result for each row", 1, results.size()); + Assert.assertThat(results, hasItem("2 years 7 months")); + } - b.release(); - loader.clear(); + @Test + public void testCastVarCharIntervalDay() throws Exception { + String result = queryBuilder() + .sql("select cast('PT1H' as interval minute) as i from (values(1))") + .vectorValue( + "i", + IntervalDayVector.class, + (recordsCount, vector) -> vector.getAccessor().getAsStringBuilder(0).toString() + ); + Assert.assertEquals(result, "0 days 1:00:00"); + + result = queryBuilder() + .sql("select cast(concat('PT',107374,'M') as interval minute) as i from (values(1))") + .vectorValue( + "i", + IntervalDayVector.class, + (recordsCount, vector) -> vector.getAccessor().getAsStringBuilder(0).toString() + ); + Assert.assertEquals(result, "74 days 13:34:00"); + + result = queryBuilder() + .sql("select cast(concat('PT',107375,'M') as interval minute) as i from (values(1))") + .vectorValue( + "i", + IntervalDayVector.class, + (recordsCount, vector) -> vector.getAccessor().getAsStringBuilder(0).toString() + ); + Assert.assertEquals(result, "74 days 13:35:00"); } @Test // DRILL-6959 diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/testing/TestDateConversions.java b/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/testing/TestDateConversions.java index 363d326..c820408 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/testing/TestDateConversions.java +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/fn/impl/testing/TestDateConversions.java @@ -108,6 +108,18 @@ public class TestDateConversions extends BaseTestQuery { } @Test + public void testToTimeWithDateTimePatternFormat() throws Exception { + mockUsDateFormatSymbols(); + + testBuilder() + .sqlQuery("select TO_TIME('2016-03-03 00:00', 'yyyy-MM-dd HH:mm') as `result` from (values(1))") + .unOrdered() + .baselineColumns("result") + .baselineValues(LocalTime.of(0,0,0)) + .go(); + } + + @Test public void testPostgresTime() throws Exception { mockUsDateFormatSymbols(); diff --git a/exec/java-exec/src/test/java/org/apache/drill/test/QueryBuilder.java b/exec/java-exec/src/test/java/org/apache/drill/test/QueryBuilder.java index 30bff62..795cdae 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/test/QueryBuilder.java +++ b/exec/java-exec/src/test/java/org/apache/drill/test/QueryBuilder.java @@ -51,6 +51,7 @@ import org.apache.drill.exec.rpc.user.UserResultsListener; import org.apache.drill.exec.util.VectorUtil; import org.apache.drill.exec.vector.NullableVarCharVector; import org.apache.drill.exec.vector.ValueVector; +import org.apache.drill.exec.vector.accessor.ScalarReader; import org.apache.drill.test.BufferingQueryEventListener.QueryEvent; import org.apache.drill.test.ClientFixture.StatementParser; import org.apache.drill.test.rowSet.DirectRowSet; @@ -58,6 +59,7 @@ import org.apache.drill.test.rowSet.RowSet; import org.apache.drill.test.rowSet.RowSetReader; import org.apache.drill.shaded.guava.com.google.common.base.Preconditions; +import org.joda.time.Period; /** * Builder for a Drill query. Provides all types of query formats, @@ -213,6 +215,23 @@ public class QueryBuilder { public QueryState finalState() { return finalState; } } + /** + * Scalar reader function interface for a set of reader methods + * @param - reader returned type + */ + private interface SingletonScalarReader { + T read(ScalarReader reader); + } + + /** + * VectorQueryReader function interface + * @param - vector class + * @param - result type + */ + public interface VectorQueryReader { + T read(int recordsCount, V vector); + } + private final ClientFixture client; private QueryType queryType; private String queryText; @@ -371,24 +390,92 @@ public class QueryBuilder { } /** + * Run the query which expect to return vector {@code V} representation + * of type {@code T} for the column {@code columnName}. + *

+ *

+   * Example:
+   *
+   *  Set results = queryBuilder()
+   *      .sql(query)
+   *      .vectorValue(
+   *        "columnName",
+   *        SomeVector.class,
+   *        (resultRecordCount, vector) -> {
+   *          Set r = new HashSet<>();
+   *          for (int i = 0; i < resultRecordCount; i++) {
+   *            r.add(vector.getAccessor().getAsStringBuilder(i).toString());
+   *          }
+   *          return r;
+   *        }
+   *      );
+   * 
+ * @param columnName name of the column to read + * @param vectorClass returned by the query vector class + * @param reader lambda to read the vector value representation + * @param vector class + * @param return type + * @return result produced by {@code reader} lambda or {@code null} if no records returned from the query + * + */ + @SuppressWarnings("unchecked") + public T vectorValue(String columnName, Class vectorClass, VectorQueryReader reader) + throws RpcException, SchemaChangeException { + + List result = results(); + RecordBatchLoader loader = new RecordBatchLoader(client.allocator()); + QueryDataBatch queryDataBatch = null; + + try { + queryDataBatch = result.get(0); + loader.load(queryDataBatch.getHeader().getDef(), queryDataBatch.getData()); + + V vector = (V) loader.getValueAccessorById( + vectorClass, + loader.getValueVectorId(SchemaPath.getCompoundPath(columnName)).getFieldIds()) + .getValueVector(); + + return (loader.getRecordCount() > 0) ? reader.read(loader.getRecordCount(), vector) : null; + } finally { + if (queryDataBatch != null) { + queryDataBatch.release(); + } + loader.clear(); + } + } + + /** * Run the query that is expected to return (at least) one row - * with the only (or first) column returning a long value. - * The long value cannot be null. + * with the only (or first) column returning a {@link T} value. + * The {@link T} value cannot be null. * * @return the value of the first column of the first row * @throws RpcException if anything goes wrong */ - - public long singletonLong() throws RpcException { + private T singletonGeneric(SingletonScalarReader scalarReader) throws RpcException { RowSet rowSet = rowSet(); if (rowSet == null) { throw new IllegalStateException("No rows returned"); } - RowSetReader reader = rowSet.reader(); - reader.next(); - long value = reader.scalar(0).getLong(); - rowSet.clear(); - return value; + try { + RowSetReader reader = rowSet.reader(); + reader.next(); + return scalarReader.read(reader.scalar(0)); + } finally { + rowSet.clear(); + } + } + + /** + * Run the query that is expected to return (at least) one row + * with the only (or first) column returning a long value. + * The long value cannot be null. + * + * @return the value of the first column of the first row + * @throws RpcException if anything goes wrong + */ + public long singletonLong() throws RpcException { + return singletonGeneric(ScalarReader::getLong); } /** @@ -399,17 +486,8 @@ public class QueryBuilder { * @return the value of the first column of the first row * @throws RpcException if anything goes wrong */ - public double singletonDouble() throws RpcException { - RowSet rowSet = rowSet(); - if (rowSet == null) { - throw new IllegalStateException("No rows returned"); - } - RowSetReader reader = rowSet.reader(); - reader.next(); - double value = reader.scalar(0).getDouble(); - rowSet.clear(); - return value; + return singletonGeneric(ScalarReader::getDouble); } /** @@ -420,17 +498,20 @@ public class QueryBuilder { * @return the value of the first column of the first row * @throws RpcException if anything goes wrong */ - public int singletonInt() throws RpcException { - RowSet rowSet = rowSet(); - if (rowSet == null) { - throw new IllegalStateException("No rows returned"); - } - RowSetReader reader = rowSet.reader(); - reader.next(); - int value = reader.scalar(0).getInt(); - rowSet.clear(); - return value; + return singletonGeneric(ScalarReader::getInt); + } + + /** + * Run the query that is expected to return (at least) one row + * with the only (or first) column returning a {@link Period} value. + * The {@link Period} value cannot be null. + * + * @return the value of the first column of the first row + * @throws RpcException if anything goes wrong + */ + public Period singletonPeriod() throws RpcException { + return singletonGeneric(ScalarReader::getPeriod); } /** @@ -443,20 +524,7 @@ public class QueryBuilder { */ public String singletonString() throws RpcException { - RowSet rowSet = rowSet(); - if (rowSet == null) { - throw new IllegalStateException("No rows returned"); - } - RowSetReader reader = rowSet.reader(); - reader.next(); - String value; - if (reader.scalar(0).isNull()) { - value = null; - } else { - value = reader.scalar(0).getString(); - } - rowSet.clear(); - return value; + return singletonGeneric(ScalarReader::getString); } /**