diff --git a/clirr-ignored-differences.xml b/clirr-ignored-differences.xml
new file mode 100644
index 000000000..235cb9074
--- /dev/null
+++ b/clirr-ignored-differences.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ 7012
+ com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection
+ com.google.cloud.spanner.Dialect getDialect()
+
+
diff --git a/pom.xml b/pom.xml
index eb7a3b557..925c0204a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,7 +51,6 @@
google-cloud-spanner-jdbc
4.13.2
3.0.2
- 1.4.4
1.1.3
4.3.1
2.2
@@ -63,7 +62,7 @@
com.google.cloud
google-cloud-spanner-bom
- 6.18.0
+ 6.19.1
pom
import
@@ -103,6 +102,10 @@
com.google.api.grpc
proto-google-common-protos
+
+ com.google.api
+ gax
+
com.google.cloud
google-cloud-spanner
@@ -120,10 +123,6 @@
com.google.guava
guava
-
- org.threeten
- threetenbp
-
io.grpc
grpc-netty-shaded
@@ -234,7 +233,7 @@
com.google.cloud.spanner.GceTestEnvConfig
- projects/gcloud-devel/instances/spanner-testing
+ projects/gcloud-devel/instances/spanner-testing-east1
2400
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java
index a666a7490..1a6cfe2ed 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcConnection.java
@@ -16,6 +16,9 @@
package com.google.cloud.spanner.jdbc;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
import com.google.cloud.spanner.connection.ConnectionOptions;
import com.google.common.annotations.VisibleForTesting;
import com.google.rpc.Code;
@@ -51,6 +54,7 @@ abstract class AbstractJdbcConnection extends AbstractJdbcWrapper
private final ConnectionOptions options;
private final com.google.cloud.spanner.connection.Connection spanner;
private final Properties clientInfo;
+ private AbstractStatementParser parser;
private SQLWarning firstWarning = null;
private SQLWarning lastWarning = null;
@@ -76,6 +80,22 @@ ConnectionOptions getConnectionOptions() {
return options;
}
+ @Override
+ public Dialect getDialect() {
+ return spanner.getDialect();
+ }
+
+ protected AbstractStatementParser getParser() throws SQLException {
+ if (parser == null) {
+ try {
+ parser = AbstractStatementParser.getInstance(spanner.getDialect());
+ } catch (SpannerException e) {
+ throw JdbcSqlExceptionFactory.of(e);
+ }
+ }
+ return parser;
+ }
+
@Override
public CallableStatement prepareCall(String sql) throws SQLException {
return checkClosedAndThrowUnsupported(CALLABLE_STATEMENTS_UNSUPPORTED);
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java
index 2a905660b..847f54d38 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcPreparedStatement.java
@@ -42,10 +42,11 @@
abstract class AbstractJdbcPreparedStatement extends JdbcStatement implements PreparedStatement {
private static final String METHOD_NOT_ON_PREPARED_STATEMENT =
"This method may not be called on a PreparedStatement";
- private final JdbcParameterStore parameters = new JdbcParameterStore();
+ private final JdbcParameterStore parameters;
- AbstractJdbcPreparedStatement(JdbcConnection connection) {
+ AbstractJdbcPreparedStatement(JdbcConnection connection) throws SQLException {
super(connection);
+ parameters = new JdbcParameterStore(connection.getDialect());
}
JdbcParameterStore getParameters() {
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java
index 9c516609a..6b5848a4a 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcStatement.java
@@ -20,6 +20,7 @@
import com.google.cloud.spanner.Options.QueryOption;
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
import com.google.cloud.spanner.connection.Connection;
import com.google.cloud.spanner.connection.StatementResult;
import com.google.cloud.spanner.connection.StatementResult.ClientSideStatementType;
@@ -35,14 +36,16 @@
abstract class AbstractJdbcStatement extends AbstractJdbcWrapper implements Statement {
private static final String CURSORS_NOT_SUPPORTED = "Cursors are not supported";
private static final String ONLY_FETCH_FORWARD_SUPPORTED = "Only fetch_forward is supported";
+ final AbstractStatementParser parser;
private boolean closed;
private boolean closeOnCompletion;
private boolean poolable;
private final JdbcConnection connection;
private int queryTimeout;
- AbstractJdbcStatement(JdbcConnection connection) {
+ AbstractJdbcStatement(JdbcConnection connection) throws SQLException {
this.connection = connection;
+ this.parser = connection.getParser();
}
@Override
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
index 25ff70a6f..ce1f84760 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapper.java
@@ -47,6 +47,7 @@ static int extractColumnType(Type type) {
if (type.equals(Type.float64())) return Types.DOUBLE;
if (type.equals(Type.int64())) return Types.BIGINT;
if (type.equals(Type.numeric())) return Types.NUMERIC;
+ if (type.equals(Type.pgNumeric())) return Types.NUMERIC;
if (type.equals(Type.string())) return Types.NVARCHAR;
if (type.equals(Type.json())) return Types.NVARCHAR;
if (type.equals(Type.timestamp())) return Types.TIMESTAMP;
@@ -106,6 +107,7 @@ static String getClassName(Type type) {
if (type == Type.float64()) return Double.class.getName();
if (type == Type.int64()) return Long.class.getName();
if (type == Type.numeric()) return BigDecimal.class.getName();
+ if (type == Type.pgNumeric()) return BigDecimal.class.getName();
if (type == Type.string()) return String.class.getName();
if (type == Type.json()) return String.class.getName();
if (type == Type.timestamp()) return Timestamp.class.getName();
@@ -116,6 +118,7 @@ static String getClassName(Type type) {
if (type.getArrayElementType() == Type.float64()) return Double[].class.getName();
if (type.getArrayElementType() == Type.int64()) return Long[].class.getName();
if (type.getArrayElementType() == Type.numeric()) return BigDecimal[].class.getName();
+ if (type.getArrayElementType() == Type.pgNumeric()) return BigDecimal[].class.getName();
if (type.getArrayElementType() == Type.string()) return String[].class.getName();
if (type.getArrayElementType() == Type.json()) return String[].class.getName();
if (type.getArrayElementType() == Type.timestamp()) return Timestamp[].class.getName();
@@ -145,6 +148,16 @@ static byte checkedCastToByte(BigDecimal val) throws SQLException {
}
}
+ /** Cast value and throw {@link SQLException} if out-of-range. */
+ static byte checkedCastToByte(BigInteger val) throws SQLException {
+ try {
+ return val.byteValueExact();
+ } catch (ArithmeticException e) {
+ throw JdbcSqlExceptionFactory.of(
+ String.format(OUT_OF_RANGE_MSG, "byte", val), com.google.rpc.Code.OUT_OF_RANGE);
+ }
+ }
+
/** Cast value and throw {@link SQLException} if out-of-range. */
static short checkedCastToShort(long val) throws SQLException {
if (val > Short.MAX_VALUE || val < Short.MIN_VALUE) {
@@ -164,6 +177,16 @@ static short checkedCastToShort(BigDecimal val) throws SQLException {
}
}
+ /** Cast value and throw {@link SQLException} if out-of-range. */
+ static short checkedCastToShort(BigInteger val) throws SQLException {
+ try {
+ return val.shortValueExact();
+ } catch (ArithmeticException e) {
+ throw JdbcSqlExceptionFactory.of(
+ String.format(OUT_OF_RANGE_MSG, "short", val), com.google.rpc.Code.OUT_OF_RANGE);
+ }
+ }
+
/** Cast value and throw {@link SQLException} if out-of-range. */
static int checkedCastToInt(long val) throws SQLException {
if (val > Integer.MAX_VALUE || val < Integer.MIN_VALUE) {
@@ -183,6 +206,16 @@ static int checkedCastToInt(BigDecimal val) throws SQLException {
}
}
+ /** Cast value and throw {@link SQLException} if out-of-range. */
+ static int checkedCastToInt(BigInteger val) throws SQLException {
+ try {
+ return val.intValueExact();
+ } catch (ArithmeticException e) {
+ throw JdbcSqlExceptionFactory.of(
+ String.format(OUT_OF_RANGE_MSG, "int", val), com.google.rpc.Code.OUT_OF_RANGE);
+ }
+ }
+
/** Cast value and throw {@link SQLException} if out-of-range. */
static float checkedCastToFloat(double val) throws SQLException {
if (val > Float.MAX_VALUE || val < -Float.MAX_VALUE) {
@@ -226,6 +259,16 @@ static long checkedCastToLong(BigDecimal val) throws SQLException {
}
}
+ /** Cast value and throw {@link SQLException} if out-of-range. */
+ static long checkedCastToLong(BigInteger val) throws SQLException {
+ try {
+ return val.longValueExact();
+ } catch (ArithmeticException e) {
+ throw JdbcSqlExceptionFactory.of(
+ String.format(OUT_OF_RANGE_MSG, "long", val), com.google.rpc.Code.OUT_OF_RANGE);
+ }
+ }
+
/**
* Parses the given string value as a double. Throws {@link SQLException} if the string is not a
* valid double value.
@@ -240,6 +283,20 @@ static double parseDouble(String val) throws SQLException {
}
}
+ /**
+ * Parses the given string value as a float. Throws {@link SQLException} if the string is not a
+ * valid float value.
+ */
+ static float parseFloat(String val) throws SQLException {
+ Preconditions.checkNotNull(val);
+ try {
+ return Float.parseFloat(val);
+ } catch (NumberFormatException e) {
+ throw JdbcSqlExceptionFactory.of(
+ String.format("%s is not a valid number", val), com.google.rpc.Code.INVALID_ARGUMENT, e);
+ }
+ }
+
/**
* Parses the given string value as a {@link Date} value. Throws {@link SQLException} if the
* string is not a valid {@link Date} value.
@@ -332,6 +389,20 @@ static Timestamp parseTimestamp(String val, Calendar cal) throws SQLException {
}
}
+ /**
+ * Parses the given string value as a {@link BigDecimal} value. Throws {@link SQLException} if the
+ * string is not a valid {@link BigDecimal} value.
+ */
+ static BigDecimal parseBigDecimal(String val) throws SQLException {
+ Preconditions.checkNotNull(val);
+ try {
+ return new BigDecimal(val);
+ } catch (NumberFormatException e) {
+ throw JdbcSqlExceptionFactory.of(
+ String.format("%s is not a valid number", val), com.google.rpc.Code.INVALID_ARGUMENT, e);
+ }
+ }
+
/** Should return true if this object has been closed */
public abstract boolean isClosed();
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java
index 7801dcfdc..679fa8716 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/CloudSpannerJdbcConnection.java
@@ -20,6 +20,7 @@
import com.google.cloud.spanner.AbortedException;
import com.google.cloud.spanner.CommitResponse;
import com.google.cloud.spanner.CommitStats;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.TimestampBound;
@@ -321,6 +322,11 @@ default String getStatementTag() throws SQLException {
*/
String getConnectionUrl();
+ /** @return The {@link Dialect} that is used by this connection. */
+ default Dialect getDialect() {
+ return Dialect.GOOGLE_STANDARD_SQL;
+ }
+
/**
* @see
* com.google.cloud.spanner.connection.Connection#addTransactionRetryListener(com.google.cloud.spanner.connection.TransactionRetryListener)
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
index 9e4623cab..867695fa8 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcConnection.java
@@ -23,7 +23,6 @@
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.connection.AutocommitDmlMode;
import com.google.cloud.spanner.connection.ConnectionOptions;
-import com.google.cloud.spanner.connection.StatementParser;
import com.google.cloud.spanner.connection.TransactionMode;
import com.google.common.collect.Iterators;
import java.sql.Array;
@@ -72,8 +71,8 @@ public JdbcPreparedStatement prepareStatement(String sql) throws SQLException {
@Override
public String nativeSQL(String sql) throws SQLException {
checkClosed();
- return JdbcParameterStore.convertPositionalParametersToNamedParameters(
- StatementParser.removeCommentsAndTrim(sql))
+ return getParser()
+ .convertPositionalParametersToNamedParameters('?', getParser().removeCommentsAndTrim(sql))
.sqlWithNamedParameters;
}
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
index 1a62775b7..7eecb2a67 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcDataType.java
@@ -203,6 +203,32 @@ public Type getSpannerType() {
return Type.numeric();
}
},
+ PG_NUMERIC {
+ @Override
+ public int getSqlType() {
+ return Types.NUMERIC;
+ }
+
+ @Override
+ public Class getJavaClass() {
+ return BigDecimal.class;
+ }
+
+ @Override
+ public Code getCode() {
+ return Code.PG_NUMERIC;
+ }
+
+ @Override
+ public List getArrayElements(ResultSet rs, int columnIndex) {
+ return rs.getValue(columnIndex).getNumericArray();
+ }
+
+ @Override
+ public Type getSpannerType() {
+ return Type.pgNumeric();
+ }
+ },
STRING {
@Override
public int getSqlType() {
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
index f5188653b..a520e221e 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterMetaData.java
@@ -16,7 +16,7 @@
package com.google.cloud.spanner.jdbc;
-import com.google.cloud.spanner.jdbc.JdbcParameterStore.ParametersInfo;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.ParameterMetaData;
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
index a475646a6..9ba490faa 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcParameterStore.java
@@ -17,12 +17,12 @@
package com.google.cloud.spanner.jdbc;
import com.google.cloud.ByteArray;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Statement.Builder;
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Value;
import com.google.cloud.spanner.ValueBinder;
-import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
import com.google.common.io.CharStreams;
import com.google.rpc.Code;
import java.io.IOException;
@@ -45,10 +45,13 @@
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
+import java.util.stream.Collectors;
/** This class handles the parameters of a {@link PreparedStatement}. */
class JdbcParameterStore {
@@ -78,7 +81,11 @@ private static final class JdbcParameter {
*/
private int highestIndex = 0;
- JdbcParameterStore() {}
+ private final Dialect dialect;
+
+ JdbcParameterStore(Dialect dialect) {
+ this.dialect = dialect;
+ }
void clearParameters() {
parametersList = new ArrayList<>(INITIAL_PARAMETERS_ARRAY_SIZE);
@@ -300,11 +307,18 @@ private boolean isValidTypeAndValue(Object value, int sqlType) {
|| value instanceof Reader
|| value instanceof URL;
case Types.DATE:
+ return value instanceof Date
+ || value instanceof Time
+ || value instanceof Timestamp
+ || value instanceof LocalDate;
case Types.TIME:
case Types.TIME_WITH_TIMEZONE:
case Types.TIMESTAMP:
case Types.TIMESTAMP_WITH_TIMEZONE:
- return value instanceof Date || value instanceof Time || value instanceof Timestamp;
+ return value instanceof Date
+ || value instanceof Time
+ || value instanceof Timestamp
+ || value instanceof OffsetDateTime;
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
@@ -365,102 +379,6 @@ private int getParameterArrayIndex(String columnName) {
return -1;
}
- /** Parameter information with positional parameters translated to named parameters. */
- static class ParametersInfo {
- final int numberOfParameters;
- final String sqlWithNamedParameters;
-
- private ParametersInfo(int numberOfParameters, String sqlWithNamedParameters) {
- this.numberOfParameters = numberOfParameters;
- this.sqlWithNamedParameters = sqlWithNamedParameters;
- }
- }
-
- /**
- * Converts all positional parameters (?) in the given sql string into named parameters. The
- * parameters are named @p1, @p2, etc. This method is used when converting a JDBC statement that
- * uses positional parameters to a Cloud Spanner {@link Statement} instance that requires named
- * parameters. The input SQL string may not contain any comments.
- *
- * @param sql The sql string without comments that should be converted
- * @return A {@link ParametersInfo} object containing a string with named parameters instead of
- * positional parameters and the number of parameters.
- * @throws JdbcSqlExceptionImpl If the input sql string contains an unclosed string/byte literal.
- */
- static ParametersInfo convertPositionalParametersToNamedParameters(String sql)
- throws SQLException {
- final char POS_PARAM = '?';
- final char SINGLE_QUOTE = '\'';
- final char DOUBLE_QUOTE = '"';
- final char BACKTICK_QUOTE = '`';
- boolean isInQuoted = false;
- char startQuote = 0;
- boolean lastCharWasEscapeChar = false;
- boolean isTripleQuoted = false;
- int paramIndex = 1;
- StringBuilder named = new StringBuilder(sql.length() + countOccurrencesOf(POS_PARAM, sql));
- for (int index = 0; index < sql.length(); index++) {
- char c = sql.charAt(index);
- if (isInQuoted) {
- if ((c == '\n' || c == '\r') && !isTripleQuoted) {
- throw JdbcSqlExceptionFactory.of(
- "SQL statement contains an unclosed literal: " + sql, Code.INVALID_ARGUMENT);
- } else if (c == startQuote) {
- if (lastCharWasEscapeChar) {
- lastCharWasEscapeChar = false;
- } else if (isTripleQuoted) {
- if (sql.length() > index + 2
- && sql.charAt(index + 1) == startQuote
- && sql.charAt(index + 2) == startQuote) {
- isInQuoted = false;
- startQuote = 0;
- isTripleQuoted = false;
- }
- } else {
- isInQuoted = false;
- startQuote = 0;
- }
- } else {
- lastCharWasEscapeChar = (c == '\\');
- }
- named.append(c);
- } else {
- if (c == POS_PARAM) {
- named.append("@p").append(paramIndex);
- paramIndex++;
- } else {
- if (c == SINGLE_QUOTE || c == DOUBLE_QUOTE || c == BACKTICK_QUOTE) {
- isInQuoted = true;
- startQuote = c;
- // check whether it is a triple-quote
- if (sql.length() > index + 2
- && sql.charAt(index + 1) == startQuote
- && sql.charAt(index + 2) == startQuote) {
- isTripleQuoted = true;
- }
- }
- named.append(c);
- }
- }
- }
- if (isInQuoted) {
- throw JdbcSqlExceptionFactory.of(
- "SQL statement contains an unclosed literal: " + sql, Code.INVALID_ARGUMENT);
- }
- return new ParametersInfo(paramIndex - 1, named.toString());
- }
-
- /** Convenience method that is used to estimate the number of parameters in a SQL statement. */
- private static int countOccurrencesOf(char c, String string) {
- int res = 0;
- for (int i = 0; i < string.length(); i++) {
- if (string.charAt(i) == c) {
- res++;
- }
- }
- return res;
- }
-
/** Bind a JDBC parameter to a parameter on a Spanner {@link Statement}. */
Builder bindParameterValue(ValueBinder binder, int index) throws SQLException {
return setValue(binder, getParameter(index), getType(index));
@@ -531,18 +449,25 @@ private Builder setParamWithKnownType(ValueBinder binder, Object value,
throw JdbcSqlExceptionFactory.of(value + " is not a valid double", Code.INVALID_ARGUMENT);
case Types.NUMERIC:
case Types.DECIMAL:
- if (value instanceof Number) {
- if (value instanceof BigDecimal) {
- return binder.to((BigDecimal) value);
+ if (dialect == Dialect.POSTGRESQL) {
+ if (value instanceof Number) {
+ return binder.to(Value.pgNumeric(value.toString()));
}
- try {
- return binder.to(new BigDecimal(value.toString()));
- } catch (NumberFormatException e) {
- // ignore and fall through to the exception.
+ throw JdbcSqlExceptionFactory.of(value + " is not a valid Number", Code.INVALID_ARGUMENT);
+ } else {
+ if (value instanceof Number) {
+ if (value instanceof BigDecimal) {
+ return binder.to((BigDecimal) value);
+ }
+ try {
+ return binder.to(new BigDecimal(value.toString()));
+ } catch (NumberFormatException e) {
+ // ignore and fall through to the exception.
+ }
}
+ throw JdbcSqlExceptionFactory.of(
+ value + " is not a valid BigDecimal", Code.INVALID_ARGUMENT);
}
- throw JdbcSqlExceptionFactory.of(
- value + " is not a valid BigDecimal", Code.INVALID_ARGUMENT);
case Types.CHAR:
case Types.VARCHAR:
case Types.LONGVARCHAR:
@@ -584,6 +509,11 @@ private Builder setParamWithKnownType(ValueBinder binder, Object value,
return binder.to(JdbcTypeConverter.toGoogleDate((Time) value));
} else if (value instanceof Timestamp) {
return binder.to(JdbcTypeConverter.toGoogleDate((Timestamp) value));
+ } else if (value instanceof LocalDate) {
+ LocalDate localDate = (LocalDate) value;
+ return binder.to(
+ com.google.cloud.Date.fromYearMonthDay(
+ localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()));
}
throw JdbcSqlExceptionFactory.of(value + " is not a valid date", Code.INVALID_ARGUMENT);
case Types.TIME:
@@ -596,6 +526,11 @@ private Builder setParamWithKnownType(ValueBinder binder, Object value,
return binder.to(JdbcTypeConverter.toGoogleTimestamp((Time) value));
} else if (value instanceof Timestamp) {
return binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value));
+ } else if (value instanceof OffsetDateTime) {
+ OffsetDateTime offsetDateTime = (OffsetDateTime) value;
+ return binder.to(
+ com.google.cloud.Timestamp.ofTimeSecondsAndNanos(
+ offsetDateTime.toEpochSecond(), offsetDateTime.getNano()));
}
throw JdbcSqlExceptionFactory.of(
value + " is not a valid timestamp", Code.INVALID_ARGUMENT);
@@ -698,12 +633,26 @@ private Builder setParamWithUnknownType(ValueBinder binder, Object valu
} else if (Double.class.isAssignableFrom(value.getClass())) {
return binder.to(((Double) value).doubleValue());
} else if (BigDecimal.class.isAssignableFrom(value.getClass())) {
- return binder.to((BigDecimal) value);
+ if (dialect == Dialect.POSTGRESQL) {
+ return binder.to(Value.pgNumeric(value.toString()));
+ } else {
+ return binder.to((BigDecimal) value);
+ }
} else if (Date.class.isAssignableFrom(value.getClass())) {
Date dateValue = (Date) value;
return binder.to(JdbcTypeConverter.toGoogleDate(dateValue));
+ } else if (LocalDate.class.isAssignableFrom(value.getClass())) {
+ LocalDate localDate = (LocalDate) value;
+ return binder.to(
+ com.google.cloud.Date.fromYearMonthDay(
+ localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()));
} else if (Timestamp.class.isAssignableFrom(value.getClass())) {
return binder.to(JdbcTypeConverter.toGoogleTimestamp((Timestamp) value));
+ } else if (OffsetDateTime.class.isAssignableFrom(value.getClass())) {
+ OffsetDateTime offsetDateTime = (OffsetDateTime) value;
+ return binder.to(
+ com.google.cloud.Timestamp.ofTimeSecondsAndNanos(
+ offsetDateTime.toEpochSecond(), offsetDateTime.getNano()));
} else if (Time.class.isAssignableFrom(value.getClass())) {
Time timeValue = (Time) value;
return binder.to(JdbcTypeConverter.toGoogleTimestamp(new Timestamp(timeValue.getTime())));
@@ -785,7 +734,11 @@ private Builder setArrayValue(ValueBinder binder, int type, Object valu
return binder.toFloat64Array((double[]) null);
case Types.NUMERIC:
case Types.DECIMAL:
- return binder.toNumericArray(null);
+ if (dialect == Dialect.POSTGRESQL) {
+ return binder.toPgNumericArray(null);
+ } else {
+ return binder.toNumericArray(null);
+ }
case Types.CHAR:
case Types.VARCHAR:
case Types.LONGVARCHAR:
@@ -850,7 +803,14 @@ private Builder setArrayValue(ValueBinder binder, int type, Object valu
} else if (Double[].class.isAssignableFrom(value.getClass())) {
return binder.toFloat64Array(toDoubleList((Double[]) value));
} else if (BigDecimal[].class.isAssignableFrom(value.getClass())) {
- return binder.toNumericArray(Arrays.asList((BigDecimal[]) value));
+ if (dialect == Dialect.POSTGRESQL) {
+ return binder.toPgNumericArray(
+ Arrays.stream((BigDecimal[]) value)
+ .map(bigDecimal -> bigDecimal == null ? null : bigDecimal.toString())
+ .collect(Collectors.toList()));
+ } else {
+ return binder.toNumericArray(Arrays.asList((BigDecimal[]) value));
+ }
} else if (Date[].class.isAssignableFrom(value.getClass())) {
return binder.toDateArray(JdbcTypeConverter.toGoogleDates((Date[]) value));
} else if (Timestamp[].class.isAssignableFrom(value.getClass())) {
@@ -908,7 +868,11 @@ private Builder setNullValue(ValueBinder binder, Integer sqlType) {
return binder.to((com.google.cloud.Date) null);
case Types.NUMERIC:
case Types.DECIMAL:
- return binder.to((BigDecimal) null);
+ if (dialect == Dialect.POSTGRESQL) {
+ return binder.to(Value.pgNumeric(null));
+ } else {
+ return binder.to((BigDecimal) null);
+ }
case Types.DOUBLE:
return binder.to((Double) null);
case Types.FLOAT:
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java
index a25b39c04..a1b327b23 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatement.java
@@ -19,10 +19,10 @@
import com.google.cloud.spanner.Options.QueryOption;
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
import com.google.cloud.spanner.ResultSets;
+import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Type;
-import com.google.cloud.spanner.connection.StatementParser;
-import com.google.cloud.spanner.jdbc.JdbcParameterStore.ParametersInfo;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.sql.PreparedStatement;
@@ -32,6 +32,7 @@
/** Implementation of {@link PreparedStatement} for Cloud Spanner. */
class JdbcPreparedStatement extends AbstractJdbcPreparedStatement {
+ private static final char POS_PARAM_CHAR = '?';
private final String sql;
private final String sqlWithoutComments;
private final ParametersInfo parameters;
@@ -39,9 +40,13 @@ class JdbcPreparedStatement extends AbstractJdbcPreparedStatement {
JdbcPreparedStatement(JdbcConnection connection, String sql) throws SQLException {
super(connection);
this.sql = sql;
- this.sqlWithoutComments = StatementParser.removeCommentsAndTrim(this.sql);
- this.parameters =
- JdbcParameterStore.convertPositionalParametersToNamedParameters(sqlWithoutComments);
+ try {
+ this.sqlWithoutComments = parser.removeCommentsAndTrim(this.sql);
+ this.parameters =
+ parser.convertPositionalParametersToNamedParameters(POS_PARAM_CHAR, sqlWithoutComments);
+ } catch (SpannerException e) {
+ throw JdbcSqlExceptionFactory.of(e);
+ }
}
ParametersInfo getParametersInfo() {
@@ -102,7 +107,7 @@ public JdbcParameterMetaData getParameterMetaData() throws SQLException {
@Override
public ResultSetMetaData getMetaData() throws SQLException {
checkClosed();
- if (StatementParser.INSTANCE.isUpdateStatement(sql)) {
+ if (getConnection().getParser().isUpdateStatement(sql)) {
// Return metadata for an empty result set as DML statements do not return any results (as a
// result set).
com.google.cloud.spanner.ResultSet resultSet =
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
index c06c147f8..089ef7b21 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSet.java
@@ -16,8 +16,11 @@
package com.google.cloud.spanner.jdbc;
+import static com.google.cloud.spanner.Type.Code.PG_NUMERIC;
+
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Type.Code;
+import com.google.cloud.spanner.Value;
import com.google.common.base.Preconditions;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
@@ -121,6 +124,12 @@ SQLException createInvalidToGetAs(String sqlType, Code type) {
com.google.rpc.Code.INVALID_ARGUMENT);
}
+ SQLException createCastException(String sqlType, Object value) {
+ return JdbcSqlExceptionFactory.of(
+ String.format("Cannot cast to %s: %s", sqlType, value),
+ com.google.rpc.Code.INVALID_ARGUMENT);
+ }
+
@Override
public String getString(int columnIndex) throws SQLException {
checkClosedAndValidRow();
@@ -140,6 +149,8 @@ public String getString(int columnIndex) throws SQLException {
return isNull ? null : Long.toString(spanner.getLong(spannerIndex));
case NUMERIC:
return isNull ? null : spanner.getBigDecimal(spannerIndex).toString();
+ case PG_NUMERIC:
+ return isNull ? null : spanner.getString(spannerIndex);
case STRING:
return isNull ? null : spanner.getString(spannerIndex);
case JSON:
@@ -168,6 +179,8 @@ public boolean getBoolean(int columnIndex) throws SQLException {
return !isNull && spanner.getLong(spannerIndex) != 0L;
case NUMERIC:
return !isNull && !spanner.getBigDecimal(spannerIndex).equals(BigDecimal.ZERO);
+ case PG_NUMERIC:
+ return !isNull && !spanner.getString(spannerIndex).equals("0");
case STRING:
return !isNull && Boolean.parseBoolean(spanner.getString(spannerIndex));
case BYTES:
@@ -198,6 +211,10 @@ public byte getByte(int columnIndex) throws SQLException {
return isNull ? (byte) 0 : checkedCastToByte(spanner.getLong(spannerIndex));
case NUMERIC:
return isNull ? (byte) 0 : checkedCastToByte(spanner.getBigDecimal(spannerIndex));
+ case PG_NUMERIC:
+ return isNull
+ ? (byte) 0
+ : checkedCastToByte(parseBigDecimal(spanner.getString(spannerIndex)).toBigInteger());
case STRING:
return isNull ? (byte) 0 : checkedCastToByte(parseLong(spanner.getString(spannerIndex)));
case BYTES:
@@ -228,6 +245,10 @@ public short getShort(int columnIndex) throws SQLException {
return isNull ? 0 : checkedCastToShort(spanner.getLong(spannerIndex));
case NUMERIC:
return isNull ? 0 : checkedCastToShort(spanner.getBigDecimal(spannerIndex));
+ case PG_NUMERIC:
+ return isNull
+ ? 0
+ : checkedCastToShort(parseBigDecimal(spanner.getString(spannerIndex)).toBigInteger());
case STRING:
return isNull ? 0 : checkedCastToShort(parseLong(spanner.getString(spannerIndex)));
case BYTES:
@@ -258,6 +279,10 @@ public int getInt(int columnIndex) throws SQLException {
return isNull ? 0 : checkedCastToInt(spanner.getLong(spannerIndex));
case NUMERIC:
return isNull ? 0 : checkedCastToInt(spanner.getBigDecimal(spannerIndex));
+ case PG_NUMERIC:
+ return isNull
+ ? 0
+ : checkedCastToInt(parseBigDecimal(spanner.getString(spannerIndex)).toBigInteger());
case STRING:
return isNull ? 0 : checkedCastToInt(parseLong(spanner.getString(spannerIndex)));
case BYTES:
@@ -285,7 +310,11 @@ public long getLong(int columnIndex) throws SQLException {
case INT64:
return isNull ? 0L : spanner.getLong(spannerIndex);
case NUMERIC:
- return isNull ? 0L : checkedCastToLong(spanner.getBigDecimal(spannerIndex));
+ return isNull ? 0 : checkedCastToLong(parseBigDecimal(spanner.getString(spannerIndex)));
+ case PG_NUMERIC:
+ return isNull
+ ? 0L
+ : checkedCastToLong(parseBigDecimal(spanner.getString(spannerIndex)).toBigInteger());
case STRING:
return isNull ? 0L : parseLong(spanner.getString(spannerIndex));
case BYTES:
@@ -314,6 +343,8 @@ public float getFloat(int columnIndex) throws SQLException {
return isNull ? 0 : checkedCastToFloat(spanner.getLong(spannerIndex));
case NUMERIC:
return isNull ? 0 : spanner.getBigDecimal(spannerIndex).floatValue();
+ case PG_NUMERIC:
+ return isNull ? 0 : parseFloat(spanner.getString(spannerIndex));
case STRING:
return isNull ? 0 : checkedCastToFloat(parseDouble(spanner.getString(spannerIndex)));
case BYTES:
@@ -342,6 +373,8 @@ public double getDouble(int columnIndex) throws SQLException {
return isNull ? 0 : spanner.getLong(spannerIndex);
case NUMERIC:
return isNull ? 0 : spanner.getBigDecimal(spannerIndex).doubleValue();
+ case PG_NUMERIC:
+ return isNull ? 0 : parseDouble(spanner.getString(spannerIndex));
case STRING:
return isNull ? 0 : parseDouble(spanner.getString(spannerIndex));
case BYTES:
@@ -358,7 +391,9 @@ public double getDouble(int columnIndex) throws SQLException {
@Override
public byte[] getBytes(int columnIndex) throws SQLException {
checkClosedAndValidRow();
- return isNull(columnIndex) ? null : spanner.getBytes(columnIndex - 1).toByteArray();
+ final boolean isNull = isNull(columnIndex);
+ final int spannerIndex = columnIndex - 1;
+ return isNull ? null : spanner.getBytes(spannerIndex).toByteArray();
}
@Override
@@ -380,6 +415,7 @@ public Date getDate(int columnIndex) throws SQLException {
case FLOAT64:
case INT64:
case NUMERIC:
+ case PG_NUMERIC:
case BYTES:
case JSON:
case STRUCT:
@@ -405,6 +441,7 @@ public Time getTime(int columnIndex) throws SQLException {
case FLOAT64:
case INT64:
case NUMERIC:
+ case PG_NUMERIC:
case BYTES:
case JSON:
case STRUCT:
@@ -431,6 +468,7 @@ public Timestamp getTimestamp(int columnIndex) throws SQLException {
case FLOAT64:
case INT64:
case NUMERIC:
+ case PG_NUMERIC:
case BYTES:
case JSON:
case STRUCT:
@@ -587,6 +625,14 @@ private Object getObject(Type type, int columnIndex) throws SQLException {
if (type == Type.float64()) return getDouble(columnIndex);
if (type == Type.int64()) return getLong(columnIndex);
if (type == Type.numeric()) return getBigDecimal(columnIndex);
+ if (type == Type.pgNumeric()) {
+ final String value = getString(columnIndex);
+ try {
+ return parseBigDecimal(value);
+ } catch (Exception e) {
+ return parseDouble(value);
+ }
+ }
if (type == Type.string()) return getString(columnIndex);
if (type == Type.json()) return getString(columnIndex);
if (type == Type.timestamp()) return getTimestamp(columnIndex);
@@ -665,6 +711,9 @@ private BigDecimal getBigDecimal(int columnIndex, boolean fixedScale, int scale)
case NUMERIC:
res = isNull ? null : spanner.getBigDecimal(spannerIndex);
break;
+ case PG_NUMERIC:
+ res = isNull ? null : parseBigDecimal(spanner.getString(spannerIndex));
+ break;
case STRING:
try {
res = isNull ? null : new BigDecimal(spanner.getString(spannerIndex));
@@ -735,10 +784,16 @@ public Array getArray(int columnIndex) throws SQLException {
throw JdbcSqlExceptionFactory.of(
"Column with index " + columnIndex + " does not contain an array",
com.google.rpc.Code.INVALID_ARGUMENT);
- JdbcDataType dataType = JdbcDataType.getType(type.getArrayElementType().getCode());
- List> elements = dataType.getArrayElements(spanner, columnIndex - 1);
-
- return JdbcArray.createArray(dataType, elements);
+ final Code elementCode = type.getArrayElementType().getCode();
+ final JdbcDataType dataType = JdbcDataType.getType(elementCode);
+ try {
+ List> elements = dataType.getArrayElements(spanner, columnIndex - 1);
+ return JdbcArray.createArray(dataType, elements);
+ } catch (NumberFormatException e) {
+ final String sqlType = "ARRAY<" + type.getArrayElementType() + ">";
+ final Value value = spanner.getValue(columnIndex - 1);
+ throw createCastException(sqlType, value);
+ }
}
@Override
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java
index 69162e807..ae312e909 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcResultSetMetaData.java
@@ -137,6 +137,7 @@ public int getPrecision(int column) {
case Types.DOUBLE:
return 14;
case Types.BIGINT:
+ case Types.INTEGER:
return 10;
case Types.NUMERIC:
return 14;
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java
index 3674ae02b..5c4bbed9e 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcStatement.java
@@ -23,7 +23,6 @@
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Type.StructField;
-import com.google.cloud.spanner.connection.StatementParser;
import com.google.cloud.spanner.connection.StatementResult;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
@@ -49,7 +48,7 @@ enum BatchType {
private BatchType currentBatchType = BatchType.NONE;
final List batchedStatements = new ArrayList<>();
- JdbcStatement(JdbcConnection connection) {
+ JdbcStatement(JdbcConnection connection) throws SQLException {
super(connection);
}
@@ -199,10 +198,10 @@ public int getFetchSize() throws SQLException {
* @throws SQLException if the sql statement is not allowed for batching.
*/
private BatchType determineStatementBatchType(String sql) throws SQLException {
- String sqlWithoutComments = StatementParser.removeCommentsAndTrim(sql);
- if (StatementParser.INSTANCE.isDdlStatement(sqlWithoutComments)) {
+ String sqlWithoutComments = parser.removeCommentsAndTrim(sql);
+ if (parser.isDdlStatement(sqlWithoutComments)) {
return BatchType.DDL;
- } else if (StatementParser.INSTANCE.isUpdateStatement(sqlWithoutComments)) {
+ } else if (parser.isUpdateStatement(sqlWithoutComments)) {
return BatchType.DML;
}
throw JdbcSqlExceptionFactory.of(
diff --git a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java
index 56043b041..f6cba1dcd 100644
--- a/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java
+++ b/src/main/java/com/google/cloud/spanner/jdbc/JdbcTypeConverter.java
@@ -29,15 +29,17 @@
import java.sql.Array;
import java.sql.SQLException;
import java.sql.Time;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import org.threeten.bp.Instant;
-import org.threeten.bp.ZoneId;
-import org.threeten.bp.ZonedDateTime;
-import org.threeten.bp.format.DateTimeFormatter;
/** Convenience class for converting values between Java, JDBC and Cloud Spanner. */
class JdbcTypeConverter {
@@ -144,9 +146,22 @@ static Object convert(Object value, Type type, Class> targetType) throws SQLEx
if (targetType.equals(java.sql.Date.class)) {
if (type.getCode() == Code.DATE) return value;
}
+ if (targetType.equals(LocalDate.class)) {
+ if (type.getCode() == Code.DATE) {
+ return ((java.sql.Date) value).toLocalDate();
+ }
+ }
if (targetType.equals(java.sql.Timestamp.class)) {
if (type.getCode() == Code.TIMESTAMP) return value;
}
+ if (targetType.equals(OffsetDateTime.class)) {
+ if (type.getCode() == Code.TIMESTAMP) {
+ Timestamp timestamp = Timestamp.of((java.sql.Timestamp) value);
+ return OffsetDateTime.ofInstant(
+ Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()),
+ ZoneId.systemDefault());
+ }
+ }
if (targetType.equals(java.sql.Array.class)) {
if (type.getCode() == Code.ARRAY) return value;
}
@@ -182,6 +197,9 @@ private static Value convertToSpannerValue(Object value, Type type) throws SQLEx
case NUMERIC:
return Value.numericArray(
Arrays.asList((BigDecimal[]) ((java.sql.Array) value).getArray()));
+ case PG_NUMERIC:
+ return Value.pgNumericArray(
+ Arrays.asList((String[]) ((java.sql.Array) value).getArray()));
case STRING:
return Value.stringArray(Arrays.asList((String[]) ((java.sql.Array) value).getArray()));
case TIMESTAMP:
@@ -206,6 +224,8 @@ private static Value convertToSpannerValue(Object value, Type type) throws SQLEx
return Value.int64((Long) value);
case NUMERIC:
return Value.numeric((BigDecimal) value);
+ case PG_NUMERIC:
+ return Value.pgNumeric(value == null ? null : value.toString());
case STRING:
return Value.string((String) value);
case TIMESTAMP:
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql
index 14106ee1e..3ee102a8c 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetColumns.sql
@@ -1,3 +1,4 @@
+/*GSQL*/
/*
* Copyright 2019 Google LLC
*
@@ -84,4 +85,4 @@ WHERE UPPER(C.TABLE_CATALOG) LIKE ?
AND UPPER(C.TABLE_SCHEMA) LIKE ?
AND UPPER(C.TABLE_NAME) LIKE ?
AND UPPER(C.COLUMN_NAME) LIKE ?
-ORDER BY TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
\ No newline at end of file
+ORDER BY TABLE_CATALOG, TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql
index 7d362ca58..d5c1620c2 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetCrossReferences.sql
@@ -1,3 +1,4 @@
+/*GSQL*/
/*
* Copyright 2019 Google LLC
*
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql
index 631886d71..ba6630c40 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetExportedKeys.sql
@@ -1,3 +1,4 @@
+/*GSQL*/
/*
* Copyright 2019 Google LLC
*
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql
index 50994a55b..4bc9296fb 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetImportedKeys.sql
@@ -1,3 +1,4 @@
+/*GSQL*/
/*
* Copyright 2019 Google LLC
*
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetIndexInfo.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetIndexInfo.sql
index e0b2a2f60..5fddb8b06 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetIndexInfo.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetIndexInfo.sql
@@ -1,3 +1,4 @@
+/*GSQL*/
/*
* Copyright 2019 Google LLC
*
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetPrimaryKeys.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetPrimaryKeys.sql
index cec8e7e05..455c474c2 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetPrimaryKeys.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetPrimaryKeys.sql
@@ -1,3 +1,4 @@
+/*GSQL*/
/*
* Copyright 2019 Google LLC
*
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetSchemas.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetSchemas.sql
index 1e302623f..3903236b1 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetSchemas.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetSchemas.sql
@@ -1,3 +1,4 @@
+/*GSQL*/
/*
* Copyright 2019 Google LLC
*
diff --git a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetTables.sql b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetTables.sql
index 1d4855810..8b3083bda 100644
--- a/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetTables.sql
+++ b/src/main/resources/com/google/cloud/spanner/jdbc/DatabaseMetaData_GetTables.sql
@@ -1,3 +1,4 @@
+/*GSQL*/
/*
* Copyright 2019 Google LLC
*
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java b/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java
index 3569e67c5..372bbb090 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/AbstractJdbcWrapperTest.java
@@ -17,10 +17,15 @@
package com.google.cloud.spanner.jdbc;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.google.cloud.Timestamp;
import com.google.rpc.Code;
+import java.math.BigDecimal;
+import java.math.BigInteger;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.Time;
@@ -67,10 +72,22 @@ public void testUnwrap() {
assertThat(unwrapSucceeds(subject, getClass())).isFalse();
}
- private static final class CheckedCastToByteChecker {
- public boolean cast(Long val) {
+ @FunctionalInterface
+ private interface SqlFunction {
+ R apply(T value) throws SQLException;
+ }
+
+ private static final class CheckedCastChecker {
+
+ private final SqlFunction checker;
+
+ public CheckedCastChecker(SqlFunction checker) {
+ this.checker = checker;
+ }
+
+ public boolean cast(T value) {
try {
- AbstractJdbcWrapper.checkedCastToByte(val);
+ checker.apply(value);
return true;
} catch (SQLException e) {
return false;
@@ -80,7 +97,8 @@ public boolean cast(Long val) {
@Test
public void testCheckedCastToByte() {
- CheckedCastToByteChecker checker = new CheckedCastToByteChecker();
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToByte);
assertThat(checker.cast(0L)).isTrue();
assertThat(checker.cast(1L)).isTrue();
assertThat(checker.cast((long) Byte.MAX_VALUE)).isTrue();
@@ -92,20 +110,38 @@ public void testCheckedCastToByte() {
assertThat(checker.cast(Long.MIN_VALUE)).isFalse();
}
- private static final class CheckedCastToShortChecker {
- public boolean cast(Long val) {
- try {
- AbstractJdbcWrapper.checkedCastToShort(val);
- return true;
- } catch (SQLException e) {
- return false;
- }
- }
+ @Test
+ public void testCheckedCastFromBigDecimalToByte() {
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToByte);
+ assertTrue(checker.cast(BigDecimal.ZERO));
+ assertTrue(checker.cast(BigDecimal.ONE));
+ assertTrue(checker.cast(BigDecimal.valueOf(-1)));
+ assertTrue(checker.cast(BigDecimal.valueOf(Byte.MIN_VALUE)));
+ assertTrue(checker.cast(BigDecimal.valueOf(Byte.MAX_VALUE)));
+
+ assertFalse(checker.cast(BigDecimal.valueOf(Byte.MAX_VALUE).add(BigDecimal.ONE)));
+ assertFalse(checker.cast(BigDecimal.valueOf(Byte.MIN_VALUE).subtract(BigDecimal.ONE)));
+ }
+
+ @Test
+ public void testCheckedCastFromBigIntegerToByte() {
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToByte);
+ assertTrue(checker.cast(BigInteger.ZERO));
+ assertTrue(checker.cast(BigInteger.ONE));
+ assertTrue(checker.cast(BigInteger.valueOf(-1)));
+ assertTrue(checker.cast(BigInteger.valueOf(Byte.MIN_VALUE)));
+ assertTrue(checker.cast(BigInteger.valueOf(Byte.MAX_VALUE)));
+
+ assertFalse(checker.cast(BigInteger.valueOf(Byte.MAX_VALUE).add(BigInteger.ONE)));
+ assertFalse(checker.cast(BigInteger.valueOf(Byte.MIN_VALUE).subtract(BigInteger.ONE)));
}
@Test
public void testCheckedCastToShort() {
- CheckedCastToShortChecker checker = new CheckedCastToShortChecker();
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToShort);
assertThat(checker.cast(0L)).isTrue();
assertThat(checker.cast(1L)).isTrue();
assertThat(checker.cast((long) Short.MAX_VALUE)).isTrue();
@@ -117,20 +153,38 @@ public void testCheckedCastToShort() {
assertThat(checker.cast(Long.MIN_VALUE)).isFalse();
}
- private static final class CheckedCastToIntChecker {
- public boolean cast(Long val) {
- try {
- AbstractJdbcWrapper.checkedCastToInt(val);
- return true;
- } catch (SQLException e) {
- return false;
- }
- }
+ @Test
+ public void testCheckedCastFromBigDecimalToShort() {
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToShort);
+ assertTrue(checker.cast(BigDecimal.ZERO));
+ assertTrue(checker.cast(BigDecimal.ONE));
+ assertTrue(checker.cast(BigDecimal.valueOf(-1)));
+ assertTrue(checker.cast(BigDecimal.valueOf(Short.MIN_VALUE)));
+ assertTrue(checker.cast(BigDecimal.valueOf(Short.MAX_VALUE)));
+
+ assertFalse(checker.cast(BigDecimal.valueOf(Short.MAX_VALUE).add(BigDecimal.ONE)));
+ assertFalse(checker.cast(BigDecimal.valueOf(Short.MIN_VALUE).subtract(BigDecimal.ONE)));
+ }
+
+ @Test
+ public void testCheckedCastFromBigIntegerToShort() {
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToShort);
+ assertTrue(checker.cast(BigInteger.ZERO));
+ assertTrue(checker.cast(BigInteger.ONE));
+ assertTrue(checker.cast(BigInteger.valueOf(-1)));
+ assertTrue(checker.cast(BigInteger.valueOf(Short.MIN_VALUE)));
+ assertTrue(checker.cast(BigInteger.valueOf(Short.MAX_VALUE)));
+
+ assertFalse(checker.cast(BigInteger.valueOf(Short.MAX_VALUE).add(BigInteger.ONE)));
+ assertFalse(checker.cast(BigInteger.valueOf(Short.MIN_VALUE).subtract(BigInteger.ONE)));
}
@Test
public void testCheckedCastToInt() {
- CheckedCastToIntChecker checker = new CheckedCastToIntChecker();
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToInt);
assertThat(checker.cast(0L)).isTrue();
assertThat(checker.cast(1L)).isTrue();
assertThat(checker.cast((long) Integer.MAX_VALUE)).isTrue();
@@ -142,20 +196,66 @@ public void testCheckedCastToInt() {
assertThat(checker.cast(Long.MIN_VALUE)).isFalse();
}
- private static final class CheckedCastToFloatChecker {
- public boolean cast(Double val) {
- try {
- AbstractJdbcWrapper.checkedCastToFloat(val);
- return true;
- } catch (SQLException e) {
- return false;
- }
- }
+ @Test
+ public void testCheckedCastFromBigDecimalToInt() {
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToInt);
+ assertTrue(checker.cast(BigDecimal.ZERO));
+ assertTrue(checker.cast(BigDecimal.ONE));
+ assertTrue(checker.cast(BigDecimal.valueOf(-1)));
+ assertTrue(checker.cast(BigDecimal.valueOf(Integer.MIN_VALUE)));
+ assertTrue(checker.cast(BigDecimal.valueOf(Integer.MAX_VALUE)));
+
+ assertFalse(checker.cast(BigDecimal.valueOf(Integer.MAX_VALUE).add(BigDecimal.ONE)));
+ assertFalse(checker.cast(BigDecimal.valueOf(Integer.MIN_VALUE).subtract(BigDecimal.ONE)));
+ }
+
+ @Test
+ public void testCheckedCastFromBigIntegerToInt() {
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToInt);
+ assertTrue(checker.cast(BigInteger.ZERO));
+ assertTrue(checker.cast(BigInteger.ONE));
+ assertTrue(checker.cast(BigInteger.valueOf(-1)));
+ assertTrue(checker.cast(BigInteger.valueOf(Integer.MIN_VALUE)));
+ assertTrue(checker.cast(BigInteger.valueOf(Integer.MAX_VALUE)));
+
+ assertFalse(checker.cast(BigInteger.valueOf(Integer.MAX_VALUE).add(BigInteger.ONE)));
+ assertFalse(checker.cast(BigInteger.valueOf(Integer.MIN_VALUE).subtract(BigInteger.ONE)));
+ }
+
+ @Test
+ public void testCheckedCastFromBigDecimalToLong() {
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToLong);
+ assertTrue(checker.cast(BigDecimal.ZERO));
+ assertTrue(checker.cast(BigDecimal.ONE));
+ assertTrue(checker.cast(BigDecimal.valueOf(-1)));
+ assertTrue(checker.cast(BigDecimal.valueOf(Long.MIN_VALUE)));
+ assertTrue(checker.cast(BigDecimal.valueOf(Long.MAX_VALUE)));
+
+ assertFalse(checker.cast(BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE)));
+ assertFalse(checker.cast(BigDecimal.valueOf(Long.MIN_VALUE).subtract(BigDecimal.ONE)));
+ }
+
+ @Test
+ public void testCheckedCastFromBigIntegerToLong() {
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToLong);
+ assertTrue(checker.cast(BigInteger.ZERO));
+ assertTrue(checker.cast(BigInteger.ONE));
+ assertTrue(checker.cast(BigInteger.valueOf(-1)));
+ assertTrue(checker.cast(BigInteger.valueOf(Long.MIN_VALUE)));
+ assertTrue(checker.cast(BigInteger.valueOf(Long.MAX_VALUE)));
+
+ assertFalse(checker.cast(BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE)));
+ assertFalse(checker.cast(BigInteger.valueOf(Long.MIN_VALUE).subtract(BigInteger.ONE)));
}
@Test
public void testCheckedCastToFloat() {
- CheckedCastToFloatChecker checker = new CheckedCastToFloatChecker();
+ final CheckedCastChecker checker =
+ new CheckedCastChecker<>(AbstractJdbcWrapper::checkedCastToFloat);
assertThat(checker.cast(0D)).isTrue();
assertThat(checker.cast(1D)).isTrue();
assertThat(checker.cast((double) Float.MAX_VALUE)).isTrue();
@@ -167,6 +267,30 @@ public void testCheckedCastToFloat() {
assertThat(checker.cast(-Double.MAX_VALUE)).isFalse();
}
+ @Test
+ public void testParseBigDecimal() throws SQLException {
+ assertEquals(BigDecimal.valueOf(123, 2), AbstractJdbcWrapper.parseBigDecimal("1.23"));
+ try {
+ AbstractJdbcWrapper.parseBigDecimal("NaN");
+ fail("missing expected SQLException");
+ } catch (SQLException e) {
+ assertTrue(e instanceof JdbcSqlException);
+ assertEquals(Code.INVALID_ARGUMENT.getNumber(), e.getErrorCode());
+ }
+ }
+
+ @Test
+ public void testParseFloat() throws SQLException {
+ assertEquals(3.14F, AbstractJdbcWrapper.parseFloat("3.14"), 0.001F);
+ try {
+ AbstractJdbcWrapper.parseFloat("invalid number");
+ fail("missing expected SQLException");
+ } catch (SQLException e) {
+ assertTrue(e instanceof JdbcSqlException);
+ assertEquals(Code.INVALID_ARGUMENT.getNumber(), e.getErrorCode());
+ }
+ }
+
private boolean unwrapSucceeds(AbstractJdbcWrapper subject, Class> iface) {
try {
subject.unwrap(iface);
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java b/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java
index efc2d8df1..2ab75e59b 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/ITAbstractJdbcTest.java
@@ -17,6 +17,7 @@
package com.google.cloud.spanner.jdbc;
import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.GceTestEnvConfig;
import com.google.cloud.spanner.IntegrationTestEnv;
import com.google.cloud.spanner.connection.AbstractSqlScriptVerifier;
@@ -33,6 +34,7 @@
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.util.Arrays;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
@@ -48,7 +50,7 @@ public ITJdbcConnectionProvider() {}
@Override
public JdbcGenericConnection getConnection() {
try {
- return JdbcGenericConnection.of(createConnection());
+ return JdbcGenericConnection.of(createConnection(getDialect()));
} catch (SQLException e) {
throw new RuntimeException(e);
}
@@ -66,7 +68,8 @@ protected void after() {
};
private static final String DEFAULT_KEY_FILE = null;
- private static Database database;
+ private static Database googleStandardSqlDatabase;
+ private static Database postgresDatabase;
protected static String getKeyFile() {
return System.getProperty(GceTestEnvConfig.GCE_CREDENTIALS_FILE, DEFAULT_KEY_FILE);
@@ -81,12 +84,20 @@ protected static IntegrationTestEnv getTestEnv() {
}
protected static Database getDatabase() {
- return database;
+ return getDatabase(Dialect.GOOGLE_STANDARD_SQL);
+ }
+
+ protected static Database getDatabase(Dialect dialect) {
+ if (dialect == Dialect.POSTGRESQL) {
+ return postgresDatabase;
+ }
+ return googleStandardSqlDatabase;
}
@BeforeClass
public static void setup() {
- database = env.getTestHelper().createTestDatabase();
+ googleStandardSqlDatabase = env.getTestHelper().createTestDatabase();
+ postgresDatabase = env.getTestHelper().createTestDatabase(Dialect.POSTGRESQL, Arrays.asList());
}
@AfterClass
@@ -101,10 +112,11 @@ public static void teardown() {
*
* @return The newly opened JDBC connection.
*/
- public CloudSpannerJdbcConnection createConnection() throws SQLException {
+ public CloudSpannerJdbcConnection createConnection(Dialect dialect) throws SQLException {
// Create a connection URL for the generic connection API.
StringBuilder url =
- ITAbstractSpannerTest.extractConnectionUrl(env.getTestHelper().getOptions(), getDatabase());
+ ITAbstractSpannerTest.extractConnectionUrl(
+ env.getTestHelper().getOptions(), getDatabase(dialect));
// Prepend it with 'jdbc:' to make it a valid JDBC connection URL.
url.insert(0, "jdbc:");
if (hasValidKeyFile()) {
@@ -112,7 +124,12 @@ public CloudSpannerJdbcConnection createConnection() throws SQLException {
}
appendConnectionUri(url);
- return DriverManager.getConnection(url.toString()).unwrap(CloudSpannerJdbcConnection.class);
+ return DriverManager.getConnection(url.toString() + ";dialect=" + dialect.name())
+ .unwrap(CloudSpannerJdbcConnection.class);
+ }
+
+ public CloudSpannerJdbcConnection createConnection() throws SQLException {
+ return createConnection(Dialect.GOOGLE_STANDARD_SQL);
}
protected void appendConnectionUri(StringBuilder uri) {}
@@ -142,32 +159,42 @@ protected boolean doCreateMusicTables() {
@Before
public void createTestTable() throws SQLException {
if (doCreateDefaultTestTable()) {
- try (Connection connection = createConnection()) {
+ try (Connection connection = createConnection(getDialect())) {
connection.setAutoCommit(true);
if (!tableExists(connection, "TEST")) {
connection.setAutoCommit(false);
+ String createTableDdl;
+ if (getDialect() == Dialect.GOOGLE_STANDARD_SQL) {
+ createTableDdl =
+ "CREATE TABLE TEST (ID INT64 NOT NULL, NAME STRING(100) NOT NULL) PRIMARY KEY (ID)";
+ } else {
+ createTableDdl =
+ "CREATE TABLE TEST (ID BIGINT PRIMARY KEY, NAME VARCHAR(100) NOT NULL)";
+ }
connection.createStatement().execute("START BATCH DDL");
- connection
- .createStatement()
- .execute(
- "CREATE TABLE TEST (ID INT64 NOT NULL, NAME STRING(100) NOT NULL) PRIMARY KEY (ID)");
+ connection.createStatement().execute(createTableDdl);
connection.createStatement().execute("RUN BATCH");
}
}
}
}
+ public Dialect getDialect() {
+ return Dialect.GOOGLE_STANDARD_SQL;
+ }
+
@Before
public void createMusicTables() throws SQLException {
if (doCreateMusicTables()) {
- try (Connection connection = createConnection()) {
+ try (Connection connection = createConnection(getDialect())) {
connection.setAutoCommit(true);
if (!tableExists(connection, "Singers")) {
- String scriptFile;
+ String scriptFile = "CreateMusicTables.sql";
+ if (getDialect() == Dialect.POSTGRESQL) {
+ scriptFile = "CreateMusicTables_PG.sql";
+ }
if (EmulatorSpannerHelper.isUsingEmulator()) {
scriptFile = "CreateMusicTables_Emulator.sql";
- } else {
- scriptFile = "CreateMusicTables.sql";
}
for (String statement :
AbstractSqlScriptVerifier.readStatementsFromFile(scriptFile, getClass())) {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java
index 2ec3f1b08..c1c90e8f6 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionGeneratedSqlScriptTest.java
@@ -19,6 +19,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.connection.AbstractConnectionImplTest;
import com.google.cloud.spanner.connection.AbstractSqlScriptVerifier.GenericConnection;
@@ -30,7 +31,9 @@
import java.sql.SQLException;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
/**
* This test executes a SQL script that has been generated from the log of all the subclasses of
@@ -38,16 +41,29 @@
* connection reacts correctly in all possible states (i.e. DML statements should not be allowed
* when the connection is in read-only mode, or when a read-only transaction has started etc.)
*/
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class JdbcConnectionGeneratedSqlScriptTest {
+ @Parameter public Dialect dialect;
+
+ @Parameters(name = "dialect = {0}")
+ public static Object[] data() {
+ return Dialect.values();
+ }
static class TestConnectionProvider implements GenericConnectionProvider {
+ private final Dialect dialect;
+
+ TestConnectionProvider(Dialect dialect) {
+ this.dialect = dialect;
+ }
+
@Override
public GenericConnection getConnection() {
ConnectionOptions options = mock(ConnectionOptions.class);
when(options.getUri()).thenReturn(ConnectionImplTest.URI);
com.google.cloud.spanner.connection.Connection spannerConnection =
ConnectionImplTest.createConnection(options);
+ when(spannerConnection.getDialect()).thenReturn(dialect);
when(options.getConnection()).thenReturn(spannerConnection);
try {
JdbcConnection connection =
@@ -65,7 +81,7 @@ public GenericConnection getConnection() {
@Test
public void testGeneratedScript() throws Exception {
- JdbcSqlScriptVerifier verifier = new JdbcSqlScriptVerifier(new TestConnectionProvider());
+ JdbcSqlScriptVerifier verifier = new JdbcSqlScriptVerifier(new TestConnectionProvider(dialect));
verifier.verifyStatementsInFile(
"ConnectionImplGeneratedSqlScriptTest.sql", SqlScriptVerifier.class, false);
}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java
index 3205cbb5e..79eafe01c 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcConnectionTest.java
@@ -26,6 +26,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.ResultSets;
import com.google.cloud.spanner.SpannerExceptionFactory;
@@ -53,28 +54,43 @@
import java.util.concurrent.Executor;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
import org.mockito.Mockito;
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class JdbcConnectionTest {
- private static final com.google.cloud.spanner.ResultSet SELECT1_RESULTSET =
- ResultSets.forRows(
- Type.struct(StructField.of("", Type.int64())),
- Collections.singletonList(Struct.newBuilder().set("").to(1L).build()));
+ @Parameter public Dialect dialect;
+
+ @Parameters(name = "dialect = {0}")
+ public static Object[] data() {
+ return Dialect.values();
+ }
+
+ private com.google.cloud.spanner.ResultSet createSelect1ResultSet() {
+ return ResultSets.forRows(
+ Type.struct(StructField.of("", Type.int64())),
+ Collections.singletonList(Struct.newBuilder().set("").to(1L).build()));
+ }
private JdbcConnection createConnection(ConnectionOptions options) throws SQLException {
com.google.cloud.spanner.connection.Connection spannerConnection =
ConnectionImplTest.createConnection(options);
+ when(spannerConnection.getDialect()).thenReturn(dialect);
when(options.getConnection()).thenReturn(spannerConnection);
return new JdbcConnection(
"jdbc:cloudspanner://localhost/projects/project/instances/instance/databases/database;credentialsUrl=url",
options);
}
+ private ConnectionOptions mockOptions() {
+ return mock(ConnectionOptions.class);
+ }
+
@Test
public void testAutoCommit() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
when(options.isAutocommit()).thenReturn(true);
try (Connection connection = createConnection(options)) {
assertThat(connection.getAutoCommit()).isTrue();
@@ -90,7 +106,7 @@ public void testAutoCommit() throws SQLException {
@Test
public void testReadOnly() {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
when(options.isAutocommit()).thenReturn(true);
when(options.isReadOnly()).thenReturn(true);
try (Connection connection = createConnection(options)) {
@@ -109,7 +125,7 @@ public void testReadOnly() {
@Test
public void testCommit() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
try (JdbcConnection connection = createConnection(options)) {
// verify that there is no transaction started
assertThat(connection.getSpannerConnection().isTransactionStarted()).isFalse();
@@ -128,7 +144,7 @@ public void testCommit() throws SQLException {
@Test
public void testRollback() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
try (JdbcConnection connection = createConnection(options)) {
// verify that there is no transaction started
assertThat(connection.getSpannerConnection().isTransactionStarted()).isFalse();
@@ -302,7 +318,7 @@ private void testClosed(
private void testInvokeMethodOnClosedConnection(Method method, Object... args)
throws SQLException, IllegalAccessException, IllegalArgumentException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
JdbcConnection connection = createConnection(options);
connection.close();
boolean valid = false;
@@ -323,7 +339,7 @@ private void testInvokeMethodOnClosedConnection(Method method, Object... args)
@Test
public void testTransactionIsolation() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
try (JdbcConnection connection = createConnection(options)) {
assertThat(connection.getTransactionIsolation())
.isEqualTo(Connection.TRANSACTION_SERIALIZABLE);
@@ -359,7 +375,7 @@ public void testTransactionIsolation() throws SQLException {
@Test
public void testHoldability() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
try (JdbcConnection connection = createConnection(options)) {
assertThat(connection.getHoldability()).isEqualTo(ResultSet.CLOSE_CURSORS_AT_COMMIT);
// assert that setting it to this value is ok.
@@ -388,7 +404,7 @@ public void testHoldability() throws SQLException {
@Test
public void testWarnings() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
try (JdbcConnection connection = createConnection(options)) {
assertThat((Object) connection.getWarnings()).isNull();
@@ -413,7 +429,7 @@ public void testWarnings() throws SQLException {
@Test
public void getDefaultClientInfo() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
try (JdbcConnection connection = createConnection(options)) {
Properties defaultProperties = connection.getClientInfo();
assertThat(defaultProperties.stringPropertyNames())
@@ -423,7 +439,7 @@ public void getDefaultClientInfo() throws SQLException {
@Test
public void testSetInvalidClientInfo() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
try (JdbcConnection connection = createConnection(options)) {
assertThat((Object) connection.getWarnings()).isNull();
connection.setClientInfo("test", "foo");
@@ -445,7 +461,7 @@ public void testSetInvalidClientInfo() throws SQLException {
@Test
public void testSetClientInfo() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
try (JdbcConnection connection = createConnection(options)) {
try (ResultSet validProperties = connection.getMetaData().getClientInfoProperties()) {
while (validProperties.next()) {
@@ -477,15 +493,16 @@ public void testSetClientInfo() throws SQLException {
@Test
public void testIsValid() throws SQLException {
// Setup.
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
com.google.cloud.spanner.connection.Connection spannerConnection =
mock(com.google.cloud.spanner.connection.Connection.class);
+ when(spannerConnection.getDialect()).thenReturn(dialect);
when(options.getConnection()).thenReturn(spannerConnection);
Statement statement = Statement.of(JdbcConnection.IS_VALID_QUERY);
// Verify that an opened connection that returns a result set is valid.
try (JdbcConnection connection = new JdbcConnection("url", options)) {
- when(spannerConnection.executeQuery(statement)).thenReturn(SELECT1_RESULTSET);
+ when(spannerConnection.executeQuery(statement)).thenReturn(createSelect1ResultSet());
assertThat(connection.isValid(1)).isTrue();
try {
// Invalid timeout value.
@@ -506,14 +523,14 @@ public void testIsValid() throws SQLException {
@Test
public void testIsValidOnClosedConnection() throws SQLException {
- Connection connection = createConnection(mock(ConnectionOptions.class));
+ Connection connection = createConnection(mockOptions());
connection.close();
assertThat(connection.isValid(1)).isFalse();
}
@Test
public void testCreateStatement() throws SQLException {
- try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) {
+ try (JdbcConnection connection = createConnection(mockOptions())) {
for (int resultSetType :
new int[] {
ResultSet.TYPE_FORWARD_ONLY,
@@ -587,7 +604,7 @@ private void assertCreateStatementFails(
@Test
public void testPrepareStatement() throws SQLException {
- try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) {
+ try (JdbcConnection connection = createConnection(mockOptions())) {
for (int resultSetType :
new int[] {
ResultSet.TYPE_FORWARD_ONLY,
@@ -663,7 +680,7 @@ private void assertPrepareStatementFails(
@Test
public void testPrepareStatementWithAutoGeneratedKeys() throws SQLException {
String sql = "INSERT INTO FOO (COL1) VALUES (?)";
- try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) {
+ try (JdbcConnection connection = createConnection(mockOptions())) {
PreparedStatement statement =
connection.prepareStatement(sql, java.sql.Statement.NO_GENERATED_KEYS);
ResultSet rs = statement.getGeneratedKeys();
@@ -679,7 +696,7 @@ public void testPrepareStatementWithAutoGeneratedKeys() throws SQLException {
@Test
public void testCatalog() throws SQLException {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
when(options.getDatabaseName()).thenReturn("test");
try (JdbcConnection connection = createConnection(options)) {
// The connection should always return the empty string as the current catalog, as no other
@@ -699,7 +716,7 @@ public void testCatalog() throws SQLException {
@Test
public void testSchema() throws SQLException {
- try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) {
+ try (JdbcConnection connection = createConnection(mockOptions())) {
assertThat(connection.getSchema()).isEqualTo("");
// This should be allowed.
connection.setSchema("");
@@ -715,7 +732,7 @@ public void testSchema() throws SQLException {
@Test
public void testIsReturnCommitStats() throws SQLException {
- try (JdbcConnection connection = createConnection(mock(ConnectionOptions.class))) {
+ try (JdbcConnection connection = createConnection(mockOptions())) {
assertFalse(connection.isReturnCommitStats());
connection.setReturnCommitStats(true);
assertTrue(connection.isReturnCommitStats());
@@ -724,7 +741,7 @@ public void testIsReturnCommitStats() throws SQLException {
@Test
public void testIsReturnCommitStats_throwsSqlException() {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
com.google.cloud.spanner.connection.Connection spannerConnection =
mock(com.google.cloud.spanner.connection.Connection.class);
when(options.getConnection()).thenReturn(spannerConnection);
@@ -746,7 +763,7 @@ public void testIsReturnCommitStats_throwsSqlException() {
@Test
public void testSetReturnCommitStats_throwsSqlException() {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
com.google.cloud.spanner.connection.Connection spannerConnection =
mock(com.google.cloud.spanner.connection.Connection.class);
when(options.getConnection()).thenReturn(spannerConnection);
@@ -769,7 +786,7 @@ public void testSetReturnCommitStats_throwsSqlException() {
@Test
public void testGetCommitResponse_throwsSqlException() {
- ConnectionOptions options = mock(ConnectionOptions.class);
+ ConnectionOptions options = mockOptions();
com.google.cloud.spanner.connection.Connection spannerConnection =
mock(com.google.cloud.spanner.connection.Connection.class);
when(options.getConnection()).thenReturn(spannerConnection);
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java
index af7b8942b..f9011e4f2 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcDatabaseMetaDataWithMockedServerTest.java
@@ -16,12 +16,15 @@
package com.google.cloud.spanner.jdbc;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.MockSpannerServiceImpl;
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
+import com.google.cloud.spanner.connection.AbstractStatementParser.ParametersInfo;
import com.google.cloud.spanner.connection.SpannerPool;
-import com.google.cloud.spanner.connection.StatementParser;
-import com.google.cloud.spanner.jdbc.JdbcParameterStore.ParametersInfo;
import com.google.protobuf.ListValue;
import com.google.protobuf.Value;
import com.google.spanner.v1.ResultSetMetadata;
@@ -39,12 +42,15 @@
import java.sql.SQLException;
import org.junit.After;
import org.junit.AfterClass;
+import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class JdbcDatabaseMetaDataWithMockedServerTest {
private static final ResultSetMetadata RESULTSET_METADATA =
ResultSetMetadata.newBuilder()
@@ -66,8 +72,23 @@ public class JdbcDatabaseMetaDataWithMockedServerTest {
.setMetadata(RESULTSET_METADATA)
.build();
+ private static final String GSQL_STATEMENT = "/*GSQL*/";
+
+ /* Checks if the SQL statement starts with /*GSQL*/
+ private boolean isGoogleSql(String sql) {
+ return sql.startsWith(GSQL_STATEMENT);
+ }
+
+ @Parameter public Dialect dialect;
+
+ @Parameters(name = "dialect = {0}")
+ public static Object[] data() {
+ return Dialect.values();
+ }
+
private static MockSpannerServiceImpl mockSpanner;
private static Server server;
+ private AbstractStatementParser parser;
@BeforeClass
public static void startStaticServer() throws IOException {
@@ -84,6 +105,11 @@ public static void stopServer() throws Exception {
server.awaitTermination();
}
+ @Before
+ public void setup() {
+ parser = AbstractStatementParser.getInstance(dialect);
+ }
+
@After
public void reset() {
// Close Spanner pool to prevent reusage of the same Spanner instance (and thereby the same
@@ -106,9 +132,9 @@ private Connection createConnection() throws SQLException {
@Test
public void getTablesInDdlBatch() throws SQLException {
String sql =
- StatementParser.removeCommentsAndTrim(
+ parser.removeCommentsAndTrim(
JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql"));
- ParametersInfo params = JdbcParameterStore.convertPositionalParametersToNamedParameters(sql);
+ ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql);
mockSpanner.putStatementResult(
StatementResult.query(
Statement.newBuilder(params.sqlWithNamedParameters)
@@ -140,9 +166,9 @@ public void getTablesInDdlBatch() throws SQLException {
@Test
public void getColumnsInDdlBatch() throws SQLException {
String sql =
- StatementParser.removeCommentsAndTrim(
+ parser.removeCommentsAndTrim(
JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetColumns.sql"));
- ParametersInfo params = JdbcParameterStore.convertPositionalParametersToNamedParameters(sql);
+ ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql);
mockSpanner.putStatementResult(
StatementResult.query(
Statement.newBuilder(params.sqlWithNamedParameters)
@@ -175,9 +201,8 @@ public void getKeysInDdlBatch() throws SQLException {
"DatabaseMetaData_GetImportedKeys.sql",
"DatabaseMetaData_GetExportedKeys.sql"
}) {
- String sql =
- StatementParser.removeCommentsAndTrim(JdbcDatabaseMetaData.readSqlFromFile(fileName));
- ParametersInfo params = JdbcParameterStore.convertPositionalParametersToNamedParameters(sql);
+ String sql = parser.removeCommentsAndTrim(JdbcDatabaseMetaData.readSqlFromFile(fileName));
+ ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql);
mockSpanner.putStatementResult(
StatementResult.query(
Statement.newBuilder(params.sqlWithNamedParameters)
@@ -212,9 +237,9 @@ public void getKeysInDdlBatch() throws SQLException {
@Test
public void getCrossReferencesInDdlBatch() throws SQLException {
String sql =
- StatementParser.removeCommentsAndTrim(
+ parser.removeCommentsAndTrim(
JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetCrossReferences.sql"));
- ParametersInfo params = JdbcParameterStore.convertPositionalParametersToNamedParameters(sql);
+ ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql);
mockSpanner.putStatementResult(
StatementResult.query(
Statement.newBuilder(params.sqlWithNamedParameters)
@@ -247,9 +272,9 @@ public void getCrossReferencesInDdlBatch() throws SQLException {
@Test
public void getIndexInfoInDdlBatch() throws SQLException {
String sql =
- StatementParser.removeCommentsAndTrim(
+ parser.removeCommentsAndTrim(
JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetIndexInfo.sql"));
- ParametersInfo params = JdbcParameterStore.convertPositionalParametersToNamedParameters(sql);
+ ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql);
mockSpanner.putStatementResult(
StatementResult.query(
Statement.newBuilder(params.sqlWithNamedParameters)
@@ -280,9 +305,9 @@ public void getIndexInfoInDdlBatch() throws SQLException {
@Test
public void getSchemasInDdlBatch() throws SQLException {
String sql =
- StatementParser.removeCommentsAndTrim(
+ parser.removeCommentsAndTrim(
JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetSchemas.sql"));
- ParametersInfo params = JdbcParameterStore.convertPositionalParametersToNamedParameters(sql);
+ ParametersInfo params = parser.convertPositionalParametersToNamedParameters('?', sql);
mockSpanner.putStatementResult(
StatementResult.query(
Statement.newBuilder(params.sqlWithNamedParameters)
@@ -306,4 +331,99 @@ public void getSchemasInDdlBatch() throws SQLException {
connection.createStatement().execute("ABORT BATCH");
}
}
+
+ @Test
+ public void verifyGoogleSqlHeaderIsCorrectlyParsed() {
+ // Verify that the `/*GSQL*/` header is kept in the SQL statement without comments if the
+ // dialect is PostgreSQL, and that it is removed if the dialect is Google_Standard_Sql.
+ if (dialect == Dialect.POSTGRESQL) {
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile(
+ "DatabaseMetaData_GetCrossReferences.sql"))))
+ .isTrue();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile(
+ "DatabaseMetaData_GetImportedKeys.sql"))))
+ .isTrue();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetPrimaryKeys.sql"))))
+ .isTrue();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql"))))
+ .isTrue();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetColumns.sql"))))
+ .isTrue();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile(
+ "DatabaseMetaData_GetExportedKeys.sql"))))
+ .isTrue();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetIndexInfo.sql"))))
+ .isTrue();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetSchemas.sql"))))
+ .isTrue();
+ } else {
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile(
+ "DatabaseMetaData_GetCrossReferences.sql"))))
+ .isFalse();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile(
+ "DatabaseMetaData_GetImportedKeys.sql"))))
+ .isFalse();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetPrimaryKeys.sql"))))
+ .isFalse();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetTables.sql"))))
+ .isFalse();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetColumns.sql"))))
+ .isFalse();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile(
+ "DatabaseMetaData_GetExportedKeys.sql"))))
+ .isFalse();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetIndexInfo.sql"))))
+ .isFalse();
+ assertThat(
+ isGoogleSql(
+ parser.removeCommentsAndTrim(
+ JdbcDatabaseMetaData.readSqlFromFile("DatabaseMetaData_GetSchemas.sql"))))
+ .isFalse();
+ }
+ }
}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java
index 0668f88fa..2c1bfe820 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcGrpcErrorTest.java
@@ -19,9 +19,11 @@
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
+import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.MockSpannerServiceImpl;
import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime;
import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.admin.database.v1.MockDatabaseAdminImpl;
import com.google.cloud.spanner.admin.instance.v1.MockInstanceAdminImpl;
@@ -113,7 +115,6 @@ public static void startStaticServer() throws IOException {
@AfterClass
public static void stopServer() throws Exception {
- SpannerPool.closeSpannerPool();
server.shutdown();
server.awaitTermination();
}
@@ -122,7 +123,20 @@ public static void stopServer() throws Exception {
public void reset() {
// Close Spanner pool to prevent reusage of the same Spanner instance (and thereby the same
// session pool).
- SpannerPool.closeSpannerPool();
+ try {
+ SpannerPool.closeSpannerPool();
+ } catch (SpannerException e) {
+ // Ignore leaked session errors that can be caused by the internal dialect auto-detection that
+ // is executed at startup. This query can still be running when an error is caused by tests in
+ // this class, and that will be registered as a session leak as that session has not yet been
+ // checked in to the pool.
+ if (!(e.getErrorCode() == ErrorCode.FAILED_PRECONDITION
+ && e.getMessage()
+ .contains(
+ "There is/are 1 connection(s) still open. Close all connections before calling closeSpanner()"))) {
+ throw e;
+ }
+ }
mockSpanner.removeAllExecutionTimes();
mockSpanner.reset();
}
@@ -205,7 +219,13 @@ public void autocommitExecuteSql() {
}
@Test
- public void autocommitPDMLExecuteSql() {
+ public void autocommitPDMLExecuteSql() throws SQLException {
+ // Make sure the dialect auto-detection has finished before we instruct the RPC to always return
+ // an error.
+ try (java.sql.Connection connection = createConnection()) {
+ connection.unwrap(CloudSpannerJdbcConnection.class).getDialect();
+ }
+
mockSpanner.setExecuteStreamingSqlExecutionTime(
SimulatedExecutionTime.ofException(serverException));
try (java.sql.Connection connection = createConnection()) {
@@ -316,7 +336,13 @@ public void transactionalRollback() throws SQLException {
}
@Test
- public void autocommitExecuteStreamingSql() {
+ public void autocommitExecuteStreamingSql() throws SQLException {
+ // Make sure the dialect auto-detection has finished before we instruct the RPC to always return
+ // an error.
+ try (java.sql.Connection connection = createConnection()) {
+ connection.unwrap(CloudSpannerJdbcConnection.class).getDialect();
+ }
+
mockSpanner.setExecuteStreamingSqlExecutionTime(
SimulatedExecutionTime.ofException(serverException));
try (java.sql.Connection connection = createConnection()) {
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java
index 89f4ae4cf..0596d829e 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcParameterStoreTest.java
@@ -16,18 +16,20 @@
package com.google.cloud.spanner.jdbc;
-import static com.google.cloud.spanner.jdbc.JdbcParameterStore.convertPositionalParametersToNamedParameters;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import com.google.cloud.ByteArray;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.Value;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
import com.google.common.io.CharStreams;
-import com.google.common.truth.Truth;
import com.google.rpc.Code;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -43,20 +45,39 @@
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.Types;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class JdbcParameterStoreTest {
+ @Parameters(name = "dialect = {0}")
+ public static Object[] parameters() {
+ return Dialect.values();
+ }
+
+ @Parameter public Dialect dialect;
+
+ private AbstractStatementParser parser;
+
+ @Before
+ public void setUp() {
+ parser = AbstractStatementParser.getInstance(dialect);
+ }
/** Tests setting a {@link Value} as a parameter value. */
@Test
public void testSetValueAsParameter() throws SQLException {
- JdbcParameterStore params = new JdbcParameterStore();
+ JdbcParameterStore params = new JdbcParameterStore(dialect);
params.setParameter(1, Value.bool(true));
verifyParameter(params, Value.bool(true));
params.setParameter(1, Value.bytes(ByteArray.copyFrom("test")));
@@ -108,7 +129,7 @@ public void testSetValueAsParameter() throws SQLException {
@SuppressWarnings("deprecation")
@Test
public void testSetParameterWithType() throws SQLException, IOException {
- JdbcParameterStore params = new JdbcParameterStore();
+ JdbcParameterStore params = new JdbcParameterStore(dialect);
// test the valid default combinations
params.setParameter(1, true, Types.BOOLEAN);
assertTrue((Boolean) params.getParameter(1));
@@ -150,6 +171,24 @@ public void testSetParameterWithType() throws SQLException, IOException {
assertEquals(new Timestamp(0L), params.getParameter(1));
verifyParameter(
params, Value.timestamp(com.google.cloud.Timestamp.ofTimeSecondsAndNanos(0L, 0)));
+ OffsetDateTime offsetDateTime =
+ OffsetDateTime.of(2021, 9, 24, 12, 27, 59, 42457, ZoneOffset.ofHours(2));
+ params.setParameter(1, offsetDateTime, Types.TIMESTAMP_WITH_TIMEZONE);
+ assertEquals(offsetDateTime, params.getParameter(1));
+ verifyParameter(
+ params,
+ Value.timestamp(
+ com.google.cloud.Timestamp.ofTimeSecondsAndNanos(
+ offsetDateTime.toEpochSecond(), offsetDateTime.getNano())));
+ LocalDate localDate = LocalDate.of(2021, 9, 24);
+ params.setParameter(1, localDate, Types.DATE);
+ assertEquals(localDate, params.getParameter(1));
+ verifyParameter(
+ params,
+ Value.date(
+ com.google.cloud.Date.fromYearMonthDay(
+ localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth())));
+
params.setParameter(1, new byte[] {1, 2, 3}, Types.BINARY);
assertArrayEquals(new byte[] {1, 2, 3}, (byte[]) params.getParameter(1));
verifyParameter(params, Value.bytes(ByteArray.copyFrom(new byte[] {1, 2, 3})));
@@ -187,7 +226,11 @@ public void testSetParameterWithType() throws SQLException, IOException {
verifyParameter(params, Value.json(jsonString));
params.setParameter(1, BigDecimal.ONE, Types.DECIMAL);
- verifyParameter(params, Value.numeric(BigDecimal.ONE));
+ if (dialect == Dialect.POSTGRESQL) {
+ verifyParameter(params, Value.pgNumeric(BigDecimal.ONE.toString()));
+ } else {
+ verifyParameter(params, Value.numeric(BigDecimal.ONE));
+ }
// types that should lead to int64
for (int type : new int[] {Types.TINYINT, Types.SMALLINT, Types.INTEGER, Types.BIGINT}) {
@@ -340,34 +383,41 @@ public void testSetParameterWithType() throws SQLException, IOException {
// types that should lead to numeric
for (int type : new int[] {Types.DECIMAL, Types.NUMERIC}) {
+ final Value expectedIntegralNumeric =
+ dialect == Dialect.POSTGRESQL ? Value.pgNumeric("1") : Value.numeric(BigDecimal.ONE);
+ final Value expectedRationalNumeric =
+ dialect == Dialect.POSTGRESQL
+ ? Value.pgNumeric("1.0")
+ : Value.numeric(BigDecimal.valueOf(1.0));
+
params.setParameter(1, BigDecimal.ONE, type);
assertEquals(BigDecimal.ONE, params.getParameter(1));
- verifyParameter(params, Value.numeric(BigDecimal.ONE));
+ verifyParameter(params, expectedIntegralNumeric);
params.setParameter(1, (byte) 1, type);
assertEquals(1, ((Byte) params.getParameter(1)).byteValue());
- verifyParameter(params, Value.numeric(BigDecimal.ONE));
+ verifyParameter(params, expectedIntegralNumeric);
params.setParameter(1, (short) 1, type);
assertEquals(1, ((Short) params.getParameter(1)).shortValue());
- verifyParameter(params, Value.numeric(BigDecimal.ONE));
+ verifyParameter(params, expectedIntegralNumeric);
params.setParameter(1, 1, type);
assertEquals(1, ((Integer) params.getParameter(1)).intValue());
- verifyParameter(params, Value.numeric(BigDecimal.ONE));
+ verifyParameter(params, expectedIntegralNumeric);
params.setParameter(1, 1L, type);
assertEquals(1, ((Long) params.getParameter(1)).longValue());
- verifyParameter(params, Value.numeric(BigDecimal.ONE));
+ verifyParameter(params, expectedIntegralNumeric);
params.setParameter(1, (float) 1, type);
assertEquals(1.0f, (Float) params.getParameter(1), 0.0f);
- verifyParameter(params, Value.numeric(BigDecimal.valueOf(1.0)));
+ verifyParameter(params, expectedRationalNumeric);
params.setParameter(1, (double) 1, type);
assertEquals(1.0d, (Double) params.getParameter(1), 0.0d);
- verifyParameter(params, Value.numeric(BigDecimal.valueOf(1.0)));
+ verifyParameter(params, expectedRationalNumeric);
}
}
@Test
public void testSetInvalidParameterWithType() throws SQLException, IOException {
- JdbcParameterStore params = new JdbcParameterStore();
+ JdbcParameterStore params = new JdbcParameterStore(dialect);
// types that should lead to int64, but with invalid values.
for (int type : new int[] {Types.TINYINT, Types.SMALLINT, Types.INTEGER, Types.BIGINT}) {
@@ -484,7 +534,7 @@ private void assertInvalidParameter(JdbcParameterStore params, Object value, int
@SuppressWarnings("deprecation")
@Test
public void testSetParameterWithoutType() throws SQLException {
- JdbcParameterStore params = new JdbcParameterStore();
+ JdbcParameterStore params = new JdbcParameterStore(dialect);
params.setParameter(1, (byte) 1, (Integer) null);
assertEquals(1, ((Byte) params.getParameter(1)).byteValue());
verifyParameter(params, Value.int64(1));
@@ -563,7 +613,7 @@ private boolean asciiStreamsEqual(InputStream is1, InputStream is2) throws IOExc
/** Tests setting array types of parameters */
@Test
public void testSetArrayParameter() throws SQLException {
- JdbcParameterStore params = new JdbcParameterStore();
+ JdbcParameterStore params = new JdbcParameterStore(dialect);
params.setParameter(
1, JdbcArray.createArray("BOOL", new Boolean[] {true, false, true}), Types.ARRAY);
assertEquals(
@@ -737,40 +787,47 @@ private void verifyParameterBindFails(JdbcParameterStore params) throws SQLExcep
}
@Test
- public void testConvertPositionalParametersToNamedParameters() throws SQLException {
+ public void testGoogleStandardSQLDialectConvertPositionalParametersToNamedParameters() {
+ assumeTrue(dialect == Dialect.GOOGLE_STANDARD_SQL);
assertEquals(
"select * from foo where name=@p1",
- convertPositionalParametersToNamedParameters("select * from foo where name=?")
+ parser.convertPositionalParametersToNamedParameters('?', "select * from foo where name=?")
.sqlWithNamedParameters);
assertEquals(
"@p1'?test?\"?test?\"?'@p2",
- convertPositionalParametersToNamedParameters("?'?test?\"?test?\"?'?")
+ parser.convertPositionalParametersToNamedParameters('?', "?'?test?\"?test?\"?'?")
.sqlWithNamedParameters);
assertEquals(
"@p1'?it\\'?s'@p2",
- convertPositionalParametersToNamedParameters("?'?it\\'?s'?").sqlWithNamedParameters);
+ parser.convertPositionalParametersToNamedParameters('?', "?'?it\\'?s'?")
+ .sqlWithNamedParameters);
assertEquals(
"@p1'?it\\\"?s'@p2",
- convertPositionalParametersToNamedParameters("?'?it\\\"?s'?").sqlWithNamedParameters);
+ parser.convertPositionalParametersToNamedParameters('?', "?'?it\\\"?s'?")
+ .sqlWithNamedParameters);
assertEquals(
"@p1\"?it\\\"?s\"@p2",
- convertPositionalParametersToNamedParameters("?\"?it\\\"?s\"?").sqlWithNamedParameters);
+ parser.convertPositionalParametersToNamedParameters('?', "?\"?it\\\"?s\"?")
+ .sqlWithNamedParameters);
assertEquals(
"@p1`?it\\`?s`@p2",
- convertPositionalParametersToNamedParameters("?`?it\\`?s`?").sqlWithNamedParameters);
+ parser.convertPositionalParametersToNamedParameters('?', "?`?it\\`?s`?")
+ .sqlWithNamedParameters);
assertEquals(
"@p1'''?it\\'?s'''@p2",
- convertPositionalParametersToNamedParameters("?'''?it\\'?s'''?").sqlWithNamedParameters);
+ parser.convertPositionalParametersToNamedParameters('?', "?'''?it\\'?s'''?")
+ .sqlWithNamedParameters);
assertEquals(
"@p1\"\"\"?it\\\"?s\"\"\"@p2",
- convertPositionalParametersToNamedParameters("?\"\"\"?it\\\"?s\"\"\"?")
+ parser.convertPositionalParametersToNamedParameters('?', "?\"\"\"?it\\\"?s\"\"\"?")
.sqlWithNamedParameters);
assertEquals(
"@p1```?it\\`?s```@p2",
- convertPositionalParametersToNamedParameters("?```?it\\`?s```?").sqlWithNamedParameters);
+ parser.convertPositionalParametersToNamedParameters('?', "?```?it\\`?s```?")
+ .sqlWithNamedParameters);
assertEquals(
"@p1'''?it\\'?s \n ?it\\'?s'''@p2",
- convertPositionalParametersToNamedParameters("?'''?it\\'?s \n ?it\\'?s'''?")
+ parser.convertPositionalParametersToNamedParameters('?', "?'''?it\\'?s \n ?it\\'?s'''?")
.sqlWithNamedParameters);
assertUnclosedLiteral("?'?it\\'?s \n ?it\\'?s'?");
@@ -779,23 +836,26 @@ public void testConvertPositionalParametersToNamedParameters() throws SQLExcepti
assertEquals(
"select 1, @p1, 'test?test', \"test?test\", foo.* from `foo` where col1=@p2 and col2='test' and col3=@p3 and col4='?' and col5=\"?\" and col6='?''?''?'",
- convertPositionalParametersToNamedParameters(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
"select 1, ?, 'test?test', \"test?test\", foo.* from `foo` where col1=? and col2='test' and col3=? and col4='?' and col5=\"?\" and col6='?''?''?'")
.sqlWithNamedParameters);
assertEquals(
"select * " + "from foo " + "where name=@p1 " + "and col2 like @p2 " + "and col3 > @p3",
- convertPositionalParametersToNamedParameters(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
"select * " + "from foo " + "where name=? " + "and col2 like ? " + "and col3 > ?")
.sqlWithNamedParameters);
assertEquals(
"select * " + "from foo " + "where id between @p1 and @p2",
- convertPositionalParametersToNamedParameters(
- "select * " + "from foo " + "where id between ? and ?")
+ parser.convertPositionalParametersToNamedParameters(
+ '?', "select * " + "from foo " + "where id between ? and ?")
.sqlWithNamedParameters);
assertEquals(
"select * " + "from foo " + "limit @p1 offset @p2",
- convertPositionalParametersToNamedParameters("select * " + "from foo " + "limit ? offset ?")
+ parser.convertPositionalParametersToNamedParameters(
+ '?', "select * " + "from foo " + "limit ? offset ?")
.sqlWithNamedParameters);
assertEquals(
"select * "
@@ -808,7 +868,93 @@ public void testConvertPositionalParametersToNamedParameters() throws SQLExcepti
+ "and col6 not in (@p6, @p7, @p8) "
+ "and col7 in (@p9, @p10, @p11) "
+ "and col8 between @p12 and @p13",
- convertPositionalParametersToNamedParameters(
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ "select * "
+ + "from foo "
+ + "where col1=? "
+ + "and col2 like ? "
+ + "and col3 > ? "
+ + "and col4 < ? "
+ + "and col5 != ? "
+ + "and col6 not in (?, ?, ?) "
+ + "and col7 in (?, ?, ?) "
+ + "and col8 between ? and ?")
+ .sqlWithNamedParameters);
+ }
+
+ @Test
+ public void testPostgresDialectConvertPositionalParametersToNamedParameters() {
+ assumeTrue(dialect == Dialect.POSTGRESQL);
+ assertEquals(
+ "select * from foo where name=$1",
+ parser.convertPositionalParametersToNamedParameters('?', "select * from foo where name=?")
+ .sqlWithNamedParameters);
+ assertEquals(
+ "$1'?test?\"?test?\"?'$2",
+ parser.convertPositionalParametersToNamedParameters('?', "?'?test?\"?test?\"?'?")
+ .sqlWithNamedParameters);
+ assertEquals(
+ "$1'?it\\'?s'$2",
+ parser.convertPositionalParametersToNamedParameters('?', "?'?it\\'?s'?")
+ .sqlWithNamedParameters);
+ assertEquals(
+ "$1'?it\\\"?s'$2",
+ parser.convertPositionalParametersToNamedParameters('?', "?'?it\\\"?s'?")
+ .sqlWithNamedParameters);
+ assertEquals(
+ "$1\"?it\\\"?s\"$2",
+ parser.convertPositionalParametersToNamedParameters('?', "?\"?it\\\"?s\"?")
+ .sqlWithNamedParameters);
+ assertEquals(
+ "$1'''?it\\'?s'''$2",
+ parser.convertPositionalParametersToNamedParameters('?', "?'''?it\\'?s'''?")
+ .sqlWithNamedParameters);
+ assertEquals(
+ "$1\"\"\"?it\\\"?s\"\"\"$2",
+ parser.convertPositionalParametersToNamedParameters('?', "?\"\"\"?it\\\"?s\"\"\"?")
+ .sqlWithNamedParameters);
+
+ assertUnclosedLiteral("?'?it\\'?s \n ?it\\'?s'?");
+ assertUnclosedLiteral("?'?it\\'?s \n ?it\\'?s?");
+ assertUnclosedLiteral("?'''?it\\'?s \n ?it\\'?s'?");
+
+ assertEquals(
+ "select 1, $1, 'test?test', \"test?test\", foo.* from `foo` where col1=$2 and col2='test' and col3=$3 and col4='?' and col5=\"?\" and col6='?''?''?'",
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ "select 1, ?, 'test?test', \"test?test\", foo.* from `foo` where col1=? and col2='test' and col3=? and col4='?' and col5=\"?\" and col6='?''?''?'")
+ .sqlWithNamedParameters);
+
+ assertEquals(
+ "select * " + "from foo " + "where name=$1 " + "and col2 like $2 " + "and col3 > $3",
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
+ "select * " + "from foo " + "where name=? " + "and col2 like ? " + "and col3 > ?")
+ .sqlWithNamedParameters);
+ assertEquals(
+ "select * " + "from foo " + "where id between $1 and $2",
+ parser.convertPositionalParametersToNamedParameters(
+ '?', "select * " + "from foo " + "where id between ? and ?")
+ .sqlWithNamedParameters);
+ assertEquals(
+ "select * " + "from foo " + "limit $1 offset $2",
+ parser.convertPositionalParametersToNamedParameters(
+ '?', "select * " + "from foo " + "limit ? offset ?")
+ .sqlWithNamedParameters);
+ assertEquals(
+ "select * "
+ + "from foo "
+ + "where col1=$1 "
+ + "and col2 like $2 "
+ + "and col3 > $3 "
+ + "and col4 < $4 "
+ + "and col5 != $5 "
+ + "and col6 not in ($6, $7, $8) "
+ + "and col7 in ($9, $10, $11) "
+ + "and col8 between $12 and $13",
+ parser.convertPositionalParametersToNamedParameters(
+ '?',
"select * "
+ "from foo "
+ "where col1=? "
@@ -824,17 +970,16 @@ public void testConvertPositionalParametersToNamedParameters() throws SQLExcepti
private void assertUnclosedLiteral(String sql) {
try {
- convertPositionalParametersToNamedParameters(sql);
+ parser.convertPositionalParametersToNamedParameters('?', sql);
fail("missing expected exception");
- } catch (SQLException t) {
- Truth.assertThat((Throwable) t).isInstanceOf(JdbcSqlException.class);
- JdbcSqlException e = (JdbcSqlException) t;
- Truth.assertThat(e.getCode()).isSameInstanceAs(Code.INVALID_ARGUMENT);
- Truth.assertThat(e.getMessage())
- .startsWith(
- Code.INVALID_ARGUMENT.name()
- + ": SQL statement contains an unclosed literal: "
- + sql);
+ } catch (SpannerException e) {
+ assertEquals(Code.INVALID_ARGUMENT.getNumber(), e.getCode());
+ assertTrue(
+ e.getMessage()
+ .startsWith(
+ Code.INVALID_ARGUMENT.name()
+ + ": SQL statement contains an unclosed literal: "
+ + sql));
}
}
}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
index fdc9969cb..99f897415 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcPreparedStatementTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
@@ -26,6 +27,8 @@
import static org.mockito.Mockito.when;
import com.google.cloud.ByteArray;
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode;
import com.google.cloud.spanner.ResultSet;
import com.google.cloud.spanner.ResultSets;
@@ -34,6 +37,7 @@
import com.google.cloud.spanner.Type;
import com.google.cloud.spanner.Type.StructField;
import com.google.cloud.spanner.Value;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
import com.google.cloud.spanner.connection.Connection;
import com.google.rpc.Code;
import java.io.ByteArrayInputStream;
@@ -55,10 +59,19 @@
import java.util.UUID;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class JdbcPreparedStatementTest {
+ @Parameter public Dialect dialect;
+
+ @Parameters(name = "dialect = {0}")
+ public static Object[] data() {
+ return Dialect.values();
+ }
+
private String generateSqlWithParameters(int numberOfParams) {
StringBuilder sql = new StringBuilder("INSERT INTO FOO (");
boolean first = true;
@@ -90,6 +103,8 @@ private JdbcConnection createMockConnection() throws SQLException {
private JdbcConnection createMockConnection(Connection spanner) throws SQLException {
JdbcConnection connection = mock(JdbcConnection.class);
+ when(connection.getDialect()).thenReturn(dialect);
+ when(connection.getParser()).thenReturn(AbstractStatementParser.getInstance(dialect));
when(connection.getSpannerConnection()).thenReturn(spanner);
when(connection.createBlob()).thenCallRealMethod();
when(connection.createClob()).thenCallRealMethod();
@@ -329,7 +344,10 @@ public void testGetResultSetMetadata() throws SQLException {
Type.struct(
StructField.of("ID", Type.int64()),
StructField.of("NAME", Type.string()),
- StructField.of("AMOUNT", Type.float64())),
+ StructField.of("AMOUNT", Type.float64()),
+ dialect == Dialect.POSTGRESQL
+ ? StructField.of("PERCENTAGE", Type.pgNumeric())
+ : StructField.of("PERCENTAGE", Type.numeric())),
Collections.singletonList(
Struct.newBuilder()
.set("ID")
@@ -338,18 +356,25 @@ public void testGetResultSetMetadata() throws SQLException {
.to("foo")
.set("AMOUNT")
.to(Math.PI)
+ .set("PERCENTAGE")
+ .to(
+ dialect == Dialect.POSTGRESQL
+ ? Value.pgNumeric("1.23")
+ : Value.numeric(new BigDecimal("1.23")))
.build()));
when(connection.analyzeQuery(Statement.of(sql), QueryAnalyzeMode.PLAN)).thenReturn(rs);
try (JdbcPreparedStatement ps =
new JdbcPreparedStatement(createMockConnection(connection), sql)) {
ResultSetMetaData metadata = ps.getMetaData();
- assertEquals(3, metadata.getColumnCount());
+ assertEquals(4, metadata.getColumnCount());
assertEquals("ID", metadata.getColumnLabel(1));
assertEquals("NAME", metadata.getColumnLabel(2));
assertEquals("AMOUNT", metadata.getColumnLabel(3));
+ assertEquals("PERCENTAGE", metadata.getColumnLabel(4));
assertEquals(Types.BIGINT, metadata.getColumnType(1));
assertEquals(Types.NVARCHAR, metadata.getColumnType(2));
assertEquals(Types.DOUBLE, metadata.getColumnType(3));
+ assertEquals(Types.NUMERIC, metadata.getColumnType(4));
}
}
@@ -363,4 +388,17 @@ public void testGetResultSetMetaDataForDml() throws SQLException {
assertEquals(0, metadata.getColumnCount());
}
}
+
+ @Test
+ public void testInvalidSql() {
+ String sql = "SELECT * FROM Singers WHERE SingerId='";
+ SQLException sqlException =
+ assertThrows(
+ SQLException.class,
+ () -> new JdbcPreparedStatement(createMockConnection(mock(Connection.class)), sql));
+ assertTrue(sqlException instanceof JdbcSqlException);
+ JdbcSqlException jdbcSqlException = (JdbcSqlException) sqlException;
+ assertEquals(
+ ErrorCode.INVALID_ARGUMENT.getGrpcStatusCode().value(), jdbcSqlException.getErrorCode());
+ }
}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
index 7ed85ef89..024f6c243 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcResultSetTest.java
@@ -49,6 +49,10 @@
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Time;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
@@ -1729,4 +1733,24 @@ public void testGetObjectAsValue() throws SQLException {
Value.timestampArray(TIMESTAMP_ARRAY_VALUE),
subject.getObject(TIMESTAMP_ARRAY_COL, Value.class));
}
+
+ @Test
+ public void testGetLocalDate() throws SQLException {
+ LocalDate localDate = subject.getObject(DATE_COL_NOT_NULL, LocalDate.class);
+ assertEquals(
+ LocalDate.of(DATE_VALUE.getYear(), DATE_VALUE.getMonth(), DATE_VALUE.getDayOfMonth()),
+ localDate);
+ assertFalse(subject.wasNull());
+ }
+
+ @Test
+ public void testGetOffsetDateTime() throws SQLException {
+ OffsetDateTime offsetDateTime = subject.getObject(TIMESTAMP_COL_NOT_NULL, OffsetDateTime.class);
+ assertEquals(
+ OffsetDateTime.ofInstant(
+ Instant.ofEpochSecond(TIMESTAMP_VALUE.getSeconds(), TIMESTAMP_VALUE.getNanos()),
+ ZoneOffset.systemDefault()),
+ offsetDateTime);
+ assertFalse(subject.wasNull());
+ }
}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcSqlScriptVerifier.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcSqlScriptVerifier.java
index 3143d5ac1..51e121337 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcSqlScriptVerifier.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcSqlScriptVerifier.java
@@ -19,12 +19,12 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.connection.AbstractSqlScriptVerifier;
-import com.google.cloud.spanner.connection.StatementParser;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
import com.google.cloud.spanner.connection.StatementResult.ResultType;
import com.google.rpc.Code;
import java.sql.Array;
-import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
@@ -118,18 +118,18 @@ protected Object getFirstValue() throws Exception {
}
public static class JdbcGenericConnection extends GenericConnection {
- private final Connection connection;
+ private final CloudSpannerJdbcConnection connection;
/**
* Use this to strip comments from a statement before the statement is executed. This should
* only be used when the connection is used in a unit test with a mocked underlying connection.
*/
private boolean stripCommentsBeforeExecute;
- public static JdbcGenericConnection of(Connection connection) {
+ public static JdbcGenericConnection of(CloudSpannerJdbcConnection connection) {
return new JdbcGenericConnection(connection);
}
- private JdbcGenericConnection(Connection connection) {
+ private JdbcGenericConnection(CloudSpannerJdbcConnection connection) {
this.connection = connection;
}
@@ -137,7 +137,7 @@ private JdbcGenericConnection(Connection connection) {
protected GenericStatementResult execute(String sql) throws SQLException {
Statement statement = connection.createStatement();
if (isStripCommentsBeforeExecute()) {
- sql = StatementParser.removeCommentsAndTrim(sql);
+ sql = AbstractStatementParser.getInstance(getDialect()).removeCommentsAndTrim(sql);
}
boolean result = statement.execute(sql);
return new JdbcGenericStatementResult(statement, result);
@@ -157,6 +157,11 @@ boolean isStripCommentsBeforeExecute() {
void setStripCommentsBeforeExecute(boolean stripCommentsBeforeExecute) {
this.stripCommentsBeforeExecute = stripCommentsBeforeExecute;
}
+
+ @Override
+ public Dialect getDialect() {
+ return connection.getDialect();
+ }
}
public JdbcSqlScriptVerifier() {}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java
index e73be4ef7..3ee9edcb6 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcStatementTest.java
@@ -24,11 +24,12 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.SpannerExceptionFactory;
import com.google.cloud.spanner.Type;
+import com.google.cloud.spanner.connection.AbstractStatementParser;
import com.google.cloud.spanner.connection.Connection;
-import com.google.cloud.spanner.connection.StatementParser;
import com.google.cloud.spanner.connection.StatementResult;
import com.google.cloud.spanner.connection.StatementResult.ResultType;
import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
@@ -42,19 +43,29 @@
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
import org.mockito.stubbing.Answer;
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class JdbcStatementTest {
private static final String SELECT = "SELECT 1";
private static final String UPDATE = "UPDATE FOO SET BAR=1 WHERE BAZ=2";
private static final String LARGE_UPDATE = "UPDATE FOO SET BAR=1 WHERE 1=1";
private static final String DDL = "CREATE INDEX FOO ON BAR(ID)";
+ @Parameter public Dialect dialect;
+
+ @Parameters(name = "dialect = {0}")
+ public static Object[] data() {
+ return Dialect.values();
+ }
+
@SuppressWarnings("unchecked")
- private JdbcStatement createStatement() {
+ private JdbcStatement createStatement() throws SQLException {
Connection spanner = mock(Connection.class);
+ when(spanner.getDialect()).thenReturn(dialect);
com.google.cloud.spanner.ResultSet resultSet = mock(com.google.cloud.spanner.ResultSet.class);
when(resultSet.next()).thenReturn(true, false);
@@ -106,7 +117,8 @@ private JdbcStatement createStatement() {
List statements =
(List) invocation.getArguments()[0];
if (statements.isEmpty()
- || StatementParser.INSTANCE.isDdlStatement(statements.get(0).getSql())) {
+ || AbstractStatementParser.getInstance(dialect)
+ .isDdlStatement(statements.get(0).getSql())) {
return new long[0];
}
long[] res =
@@ -118,6 +130,8 @@ private JdbcStatement createStatement() {
});
JdbcConnection connection = mock(JdbcConnection.class);
+ when(connection.getDialect()).thenReturn(dialect);
+ when(connection.getParser()).thenReturn(AbstractStatementParser.getInstance(dialect));
when(connection.getSpannerConnection()).thenReturn(spanner);
return new JdbcStatement(connection);
}
@@ -126,6 +140,7 @@ private JdbcStatement createStatement() {
public void testQueryTimeout() throws SQLException {
final String select = "SELECT 1";
JdbcConnection connection = mock(JdbcConnection.class);
+ when(connection.getDialect()).thenReturn(dialect);
Connection spanner = mock(Connection.class);
when(connection.getSpannerConnection()).thenReturn(spanner);
StatementResult result = mock(StatementResult.class);
@@ -230,8 +245,8 @@ public void testExecuteQuery() throws SQLException {
@Test
public void testExecuteQueryWithUpdateStatement() {
- Statement statement = createStatement();
try {
+ Statement statement = createStatement();
statement.executeQuery(UPDATE);
fail("missing expected exception");
} catch (SQLException e) {
@@ -244,8 +259,8 @@ public void testExecuteQueryWithUpdateStatement() {
@Test
public void testExecuteQueryWithDdlStatement() {
- Statement statement = createStatement();
try {
+ Statement statement = createStatement();
statement.executeQuery(DDL);
fail("missing expected exception");
} catch (SQLException e) {
@@ -271,6 +286,7 @@ public void testExecuteUpdate() throws SQLException {
@Test
public void testInternalExecuteUpdate() throws SQLException {
JdbcConnection connection = mock(JdbcConnection.class);
+ when(connection.getDialect()).thenReturn(dialect);
Connection spannerConnection = mock(Connection.class);
when(connection.getSpannerConnection()).thenReturn(spannerConnection);
com.google.cloud.spanner.Statement updateStatement =
@@ -293,6 +309,7 @@ public void testInternalExecuteUpdate() throws SQLException {
@Test
public void testInternalExecuteLargeUpdate() throws SQLException {
JdbcConnection connection = mock(JdbcConnection.class);
+ when(connection.getDialect()).thenReturn(dialect);
Connection spannerConnection = mock(Connection.class);
when(connection.getSpannerConnection()).thenReturn(spannerConnection);
com.google.cloud.spanner.Statement updateStatement =
@@ -329,8 +346,8 @@ public void testExecuteLargeUpdate() throws SQLException {
@Test
public void testExecuteUpdateWithSelectStatement() {
- Statement statement = createStatement();
try {
+ Statement statement = createStatement();
statement.executeUpdate(SELECT);
fail("missing expected exception");
} catch (SQLException e) {
@@ -436,7 +453,9 @@ public void testLargeDmlBatch() throws SQLException {
@Test
public void testConvertUpdateCounts() {
- try (JdbcStatement statement = new JdbcStatement(mock(JdbcConnection.class))) {
+ JdbcConnection connection = mock(JdbcConnection.class);
+ when(connection.getDialect()).thenReturn(dialect);
+ try (JdbcStatement statement = new JdbcStatement(connection)) {
int[] updateCounts = statement.convertUpdateCounts(new long[] {1L, 2L, 3L});
assertThat(updateCounts).asList().containsExactly(1, 2, 3);
updateCounts = statement.convertUpdateCounts(new long[] {0L, 0L, 0L});
@@ -450,8 +469,10 @@ public void testConvertUpdateCounts() {
}
@Test
- public void testConvertUpdateCountsToSuccessNoInfo() {
- try (JdbcStatement statement = new JdbcStatement(mock(JdbcConnection.class))) {
+ public void testConvertUpdateCountsToSuccessNoInfo() throws SQLException {
+ JdbcConnection connection = mock(JdbcConnection.class);
+ when(connection.getDialect()).thenReturn(dialect);
+ try (JdbcStatement statement = new JdbcStatement(connection)) {
long[] updateCounts = new long[3];
statement.convertUpdateCountsToSuccessNoInfo(new long[] {1L, 2L, 3L}, updateCounts);
assertThat(updateCounts)
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTimeoutSqlTest.java b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTimeoutSqlTest.java
index 8e36a7db1..cd462b431 100644
--- a/src/test/java/com/google/cloud/spanner/jdbc/JdbcTimeoutSqlTest.java
+++ b/src/test/java/com/google/cloud/spanner/jdbc/JdbcTimeoutSqlTest.java
@@ -16,12 +16,15 @@
package com.google.cloud.spanner.jdbc;
+import com.google.cloud.spanner.Dialect;
import com.google.cloud.spanner.jdbc.JdbcConnectionGeneratedSqlScriptTest.TestConnectionProvider;
import java.sql.Connection;
import java.sql.Statement;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
/**
* As JDBC connections store the statement timeout on {@link Statement} objects instead of on the
@@ -30,11 +33,19 @@
* timeouts, while the underlying {@link com.google.cloud.spanner.connection.Connection}s use
* milliseconds. This test script tests a number of special cases regarding this.
*/
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class JdbcTimeoutSqlTest {
+
+ @Parameter public Dialect dialect;
+
+ @Parameters(name = "dialect = {0}")
+ public static Object[] data() {
+ return Dialect.values();
+ }
+
@Test
public void testTimeoutScript() throws Exception {
- JdbcSqlScriptVerifier verifier = new JdbcSqlScriptVerifier(new TestConnectionProvider());
+ JdbcSqlScriptVerifier verifier = new JdbcSqlScriptVerifier(new TestConnectionProvider(dialect));
verifier.verifyStatementsInFile("TimeoutSqlScriptTest.sql", getClass(), false);
}
}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/PgNumericPreparedStatementTest.java b/src/test/java/com/google/cloud/spanner/jdbc/PgNumericPreparedStatementTest.java
new file mode 100644
index 000000000..fde7c42e0
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/jdbc/PgNumericPreparedStatementTest.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed 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 com.google.cloud.spanner.jdbc;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.cloud.spanner.Dialect;
+import com.google.cloud.spanner.MockSpannerServiceImpl;
+import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.connection.SpannerPool;
+import com.google.common.collect.ImmutableMap;
+import com.google.protobuf.ListValue;
+import com.google.protobuf.NullValue;
+import com.google.protobuf.Struct;
+import com.google.protobuf.Value;
+import com.google.spanner.v1.ExecuteSqlRequest;
+import com.google.spanner.v1.Type;
+import com.google.spanner.v1.TypeAnnotationCode;
+import com.google.spanner.v1.TypeCode;
+import io.grpc.Server;
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import java.math.BigDecimal;
+import java.net.InetSocketAddress;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.Arrays;
+import java.util.Map;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class PgNumericPreparedStatementTest {
+
+ private static final String PROJECT = "my-project";
+ private static final String INSTANCE = "my-instance";
+ private static final String DATABASE = "my-database";
+ private static final String QUERY = "INSERT INTO Table (col1) VALUES (?)";
+ private static final String REWRITTEN_QUERY = "INSERT INTO Table (col1) VALUES ($1)";
+ private static MockSpannerServiceImpl mockSpanner;
+ private static InetSocketAddress address;
+ private static Server server;
+ private Connection connection;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ mockSpanner = new MockSpannerServiceImpl();
+ mockSpanner.setAbortProbability(0.0D);
+ mockSpanner.putStatementResult(StatementResult.detectDialectResult(Dialect.POSTGRESQL));
+
+ address = new InetSocketAddress("localhost", 0);
+ server = NettyServerBuilder.forAddress(address).addService(mockSpanner).build().start();
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ SpannerPool.closeSpannerPool();
+ server.shutdown();
+ server.awaitTermination();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ final String endpoint = address.getHostString() + ":" + server.getPort();
+ final String url =
+ String.format(
+ "jdbc:cloudspanner://%s/projects/%s/instances/%s/databases/%s?usePlainText=true;dialect=POSTGRESQL",
+ endpoint, PROJECT, INSTANCE, DATABASE);
+ connection = DriverManager.getConnection(url);
+ mockSpanner.reset();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ connection.close();
+ }
+
+ @Test
+ public void testSetByteAsObject() throws SQLException {
+ final Byte param = 1;
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetShortAsObject() throws SQLException {
+ final Short param = 1;
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetIntAsObject() throws SQLException {
+ final Integer param = 1;
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetLongAsObject() throws SQLException {
+ final Long param = 1L;
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetFloatAsObject() throws SQLException {
+ final Float param = 1F;
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetFloatNaNAsObject() throws SQLException {
+ final Float param = Float.NaN;
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetDoubleAsObject() throws SQLException {
+ final Double param = 1D;
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetDoubleNaNAsObject() throws SQLException {
+ final Double param = Double.NaN;
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetBigDecimalAsObject() throws SQLException {
+ final BigDecimal param = new BigDecimal("1.23");
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetBigDecimalAsObjectWithoutExplicitType() throws SQLException {
+ final BigDecimal param = new BigDecimal("1.23");
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setObject(1, param);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetBigDecimal() throws SQLException {
+ final BigDecimal param = new BigDecimal("1");
+
+ mockScalarUpdateWithParam(param.toString());
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setBigDecimal(1, param);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(param.toString());
+ }
+
+ @Test
+ public void testSetNull() throws SQLException {
+ mockScalarUpdateWithParam(null);
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setNull(1, Types.NUMERIC);
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithScalar(null);
+ }
+
+ @Test
+ public void testSetNumericArray() throws SQLException {
+ final BigDecimal[] param = {BigDecimal.ONE, null, BigDecimal.TEN};
+
+ mockArrayUpdateWithParam(Arrays.asList("1", null, "10"));
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setArray(1, connection.createArrayOf("numeric", param));
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithArray(Arrays.asList("1", null, "10"));
+ }
+
+ @Test
+ public void testSetNullArray() throws SQLException {
+ mockArrayUpdateWithParam(null);
+ try (PreparedStatement preparedStatement = connection.prepareStatement(QUERY)) {
+ preparedStatement.setArray(1, connection.createArrayOf("numeric", null));
+ preparedStatement.executeUpdate();
+ }
+ assertRequestWithArray(null);
+ }
+
+ private void mockScalarUpdateWithParam(String value) {
+ mockSpanner.putStatementResult(
+ StatementResult.update(
+ Statement.newBuilder(REWRITTEN_QUERY)
+ .bind("p1")
+ .to(com.google.cloud.spanner.Value.pgNumeric(value))
+ .build(),
+ 1));
+ }
+
+ private void mockArrayUpdateWithParam(Iterable value) {
+ mockSpanner.putStatementResult(
+ StatementResult.update(
+ Statement.newBuilder(REWRITTEN_QUERY)
+ .bind("p1")
+ .to(com.google.cloud.spanner.Value.pgNumericArray(value))
+ .build(),
+ 1));
+ }
+
+ private void assertRequestWithScalar(String value) {
+ final ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
+ final String actualSql = request.getSql();
+ final Struct actualParams = request.getParams();
+ final Map actualParamTypes = request.getParamTypesMap();
+
+ final Value parameterValue = protoValueFromString(value);
+ final Struct expectedParams = Struct.newBuilder().putFields("p1", parameterValue).build();
+ final ImmutableMap expectedTypes =
+ ImmutableMap.of(
+ "p1",
+ Type.newBuilder()
+ .setCode(TypeCode.NUMERIC)
+ .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC)
+ .build());
+ assertEquals(REWRITTEN_QUERY, actualSql);
+ assertEquals(expectedParams, actualParams);
+ assertEquals(expectedTypes, actualParamTypes);
+ }
+
+ private void assertRequestWithArray(Iterable value) {
+ final ExecuteSqlRequest request = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class).get(0);
+ final String actualSql = request.getSql();
+ final Struct actualParams = request.getParams();
+ final Map actualParamTypes = request.getParamTypesMap();
+
+ Value parameterValue;
+ if (value != null) {
+ final ListValue.Builder builder = ListValue.newBuilder();
+ value.forEach(v -> builder.addValues(protoValueFromString(v)));
+ parameterValue = Value.newBuilder().setListValue(builder.build()).build();
+ } else {
+ parameterValue = Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build();
+ }
+ final Struct expectedParams = Struct.newBuilder().putFields("p1", parameterValue).build();
+ final ImmutableMap expectedTypes =
+ ImmutableMap.of(
+ "p1",
+ Type.newBuilder()
+ .setCode(TypeCode.ARRAY)
+ .setArrayElementType(
+ Type.newBuilder()
+ .setCode(TypeCode.NUMERIC)
+ .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC))
+ .build());
+ assertEquals(REWRITTEN_QUERY, actualSql);
+ assertEquals(expectedParams, actualParams);
+ assertEquals(expectedTypes, actualParamTypes);
+ }
+
+ private Value protoValueFromString(String value) {
+ if (value == null) {
+ return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build();
+ } else {
+ return Value.newBuilder().setStringValue(value).build();
+ }
+ }
+}
diff --git a/src/test/java/com/google/cloud/spanner/jdbc/PgNumericResultSetTest.java b/src/test/java/com/google/cloud/spanner/jdbc/PgNumericResultSetTest.java
new file mode 100644
index 000000000..119897af9
--- /dev/null
+++ b/src/test/java/com/google/cloud/spanner/jdbc/PgNumericResultSetTest.java
@@ -0,0 +1,801 @@
+/*
+ * Copyright 2021 Google LLC
+ *
+ * Licensed 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 com.google.cloud.spanner.jdbc;
+
+import static com.google.protobuf.NullValue.NULL_VALUE;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.cloud.spanner.MockSpannerServiceImpl;
+import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult;
+import com.google.cloud.spanner.connection.SpannerPool;
+import com.google.cloud.spanner.jdbc.JdbcSqlExceptionFactory.JdbcSqlExceptionImpl;
+import com.google.common.io.ByteSource;
+import com.google.protobuf.ListValue;
+import com.google.spanner.v1.ResultSetMetadata;
+import com.google.spanner.v1.StructType;
+import com.google.spanner.v1.StructType.Field;
+import com.google.spanner.v1.TypeAnnotationCode;
+import com.google.spanner.v1.TypeCode;
+import io.grpc.Server;
+import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.InetSocketAddress;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.sql.Blob;
+import java.sql.Clob;
+import java.sql.Connection;
+import java.sql.Date;
+import java.sql.DriverManager;
+import java.sql.NClob;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class PgNumericResultSetTest {
+
+ private static final String PROJECT = "my-project";
+ private static final String INSTANCE = "my-instance";
+ private static final String DATABASE = "my-database";
+ private static final String COLUMN_NAME = "PgNumeric";
+ private static final ResultSetMetadata SCALAR_METADATA =
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName(COLUMN_NAME)
+ .setType(
+ com.google.spanner.v1.Type.newBuilder()
+ .setCode(TypeCode.NUMERIC)
+ .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC))))
+ .build();
+ private static final ResultSetMetadata ARRAY_METADATA =
+ ResultSetMetadata.newBuilder()
+ .setRowType(
+ StructType.newBuilder()
+ .addFields(
+ Field.newBuilder()
+ .setName(COLUMN_NAME)
+ .setType(
+ com.google.spanner.v1.Type.newBuilder()
+ .setCode(TypeCode.ARRAY)
+ .setArrayElementType(
+ com.google.spanner.v1.Type.newBuilder()
+ .setCode(TypeCode.NUMERIC)
+ .setTypeAnnotation(TypeAnnotationCode.PG_NUMERIC)))))
+ .build();
+ private static final String QUERY = "SELECT " + COLUMN_NAME + " FROM Table WHERE Id = 0";
+ private static final int MAX_PG_NUMERIC_SCALE = 131_072;
+ private static final int MAX_PG_NUMERIC_PRECISION = 16_383;
+
+ private static MockSpannerServiceImpl mockSpanner;
+ private static InetSocketAddress address;
+ private static Server server;
+ private Connection connection;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ mockSpanner = new MockSpannerServiceImpl();
+ mockSpanner.setAbortProbability(0.0D);
+
+ address = new InetSocketAddress("localhost", 0);
+ server = NettyServerBuilder.forAddress(address).addService(mockSpanner).build().start();
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ SpannerPool.closeSpannerPool();
+ server.shutdown();
+ server.awaitTermination();
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ final String endpoint = address.getHostString() + ":" + server.getPort();
+ final String url =
+ String.format(
+ "jdbc:cloudspanner://%s/projects/%s/instances/%s/databases/%s?usePlainText=true",
+ endpoint, PROJECT, INSTANCE, DATABASE);
+ connection = DriverManager.getConnection(url);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ connection.close();
+ }
+
+ @Test
+ public void testGetString() throws Exception {
+ final String maxScale = String.join("", Collections.nCopies(MAX_PG_NUMERIC_SCALE, "1"));
+ final String maxPrecision =
+ "0." + String.join("", Collections.nCopies(MAX_PG_NUMERIC_PRECISION, "2"));
+
+ mockScalarResults("0", "1", "1.23", maxScale, maxPrecision, "NaN", null);
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getString, ResultSet::getString);
+
+ matcher.nextAndAssertEquals("0");
+ matcher.nextAndAssertEquals("1");
+ matcher.nextAndAssertEquals("1.23");
+ matcher.nextAndAssertEquals(maxScale);
+ matcher.nextAndAssertEquals(maxPrecision);
+ matcher.nextAndAssertEquals("NaN");
+ matcher.nextAndAssertEquals(null);
+ }
+ }
+
+ @Test
+ public void testGetNString() throws Exception {
+ final String maxScale = String.join("", Collections.nCopies(MAX_PG_NUMERIC_SCALE, "1"));
+ final String maxPrecision =
+ "0." + String.join("", Collections.nCopies(MAX_PG_NUMERIC_PRECISION, "2"));
+
+ mockScalarResults("0", "1", "1.23", maxScale, maxPrecision, "NaN", null);
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getNString, ResultSet::getNString);
+
+ matcher.nextAndAssertEquals("0");
+ matcher.nextAndAssertEquals("1");
+ matcher.nextAndAssertEquals("1.23");
+ matcher.nextAndAssertEquals(maxScale);
+ matcher.nextAndAssertEquals(maxPrecision);
+ matcher.nextAndAssertEquals("NaN");
+ matcher.nextAndAssertEquals(null);
+ }
+ }
+
+ @Test
+ public void testGetBoolean() throws Exception {
+ mockScalarResults("0", null, "1", "NaN", "1.00");
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getBoolean, ResultSet::getBoolean);
+
+ // 0 == false
+ matcher.nextAndAssertEquals(false);
+ // NULL == false
+ matcher.nextAndAssertEquals(false);
+ // anything else == true
+ matcher.nextAndAssertEquals(true); // "1" == true
+ matcher.nextAndAssertEquals(true); // "Nan" == true
+ matcher.nextAndAssertEquals(true); // "1.00" == true
+ }
+ }
+
+ @Test
+ public void testGetByte() throws Exception {
+ final String minValue = Byte.MIN_VALUE + "";
+ final String underflow = String.valueOf((int) Byte.MIN_VALUE - 1);
+ final String maxValue = Byte.MAX_VALUE + "";
+ final String overflow = String.valueOf((int) Short.MAX_VALUE + 1);
+
+ mockScalarResults(minValue, maxValue, "1.23", null, "NaN", underflow, overflow);
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getByte, ResultSet::getByte);
+
+ matcher.nextAndAssertEquals(Byte.MIN_VALUE);
+ matcher.nextAndAssertEquals(Byte.MAX_VALUE);
+ matcher.nextAndAssertEquals((byte) 1);
+ // NULL == 0
+ matcher.nextAndAssertEquals((byte) 0);
+ matcher.nextAndAssertError(JdbcSqlExceptionImpl.class, "NaN is not a valid number");
+ matcher.nextAndAssertError(
+ JdbcSqlExceptionImpl.class, "Value out of range for byte: " + underflow);
+ matcher.nextAndAssertError(
+ JdbcSqlExceptionImpl.class, "Value out of range for byte: " + overflow);
+ }
+ }
+
+ @Test
+ public void testGetShort() throws Exception {
+ final String minValue = Short.MIN_VALUE + "";
+ final String underflow = String.valueOf((int) Short.MIN_VALUE - 1);
+ final String maxValue = Short.MAX_VALUE + "";
+ final String overflow = String.valueOf((int) Short.MAX_VALUE + 1);
+
+ mockScalarResults(minValue, maxValue, "1.23", null, "NaN", underflow, overflow);
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getShort, ResultSet::getShort);
+
+ matcher.nextAndAssertEquals(Short.MIN_VALUE);
+ matcher.nextAndAssertEquals(Short.MAX_VALUE);
+ matcher.nextAndAssertEquals((short) 1);
+ // NULL == 0
+ matcher.nextAndAssertEquals((short) 0);
+ matcher.nextAndAssertError(JdbcSqlExceptionImpl.class, "NaN is not a valid number");
+ matcher.nextAndAssertError(
+ JdbcSqlExceptionImpl.class, "Value out of range for short: " + underflow);
+ matcher.nextAndAssertError(
+ JdbcSqlExceptionImpl.class, "Value out of range for short: " + overflow);
+ }
+ }
+
+ @Test
+ public void testGetInt() throws Exception {
+ final String minValue = Integer.MIN_VALUE + "";
+ final String underflow = String.valueOf((long) Integer.MIN_VALUE - 1L);
+ final String maxValue = Integer.MAX_VALUE + "";
+ final String overflow = String.valueOf((long) Integer.MAX_VALUE + 1L);
+
+ mockScalarResults(minValue, maxValue, "1.23", null, "NaN", underflow, overflow);
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getInt, ResultSet::getInt);
+
+ matcher.nextAndAssertEquals(Integer.MIN_VALUE);
+ matcher.nextAndAssertEquals(Integer.MAX_VALUE);
+ matcher.nextAndAssertEquals(1);
+ // NULL == 0
+ matcher.nextAndAssertEquals(0);
+ matcher.nextAndAssertError(JdbcSqlExceptionImpl.class, "NaN is not a valid number");
+ matcher.nextAndAssertError(
+ JdbcSqlExceptionImpl.class, "Value out of range for int: " + underflow);
+ matcher.nextAndAssertError(
+ JdbcSqlExceptionImpl.class, "Value out of range for int: " + overflow);
+ }
+ }
+
+ @Test
+ public void testGetLong() throws Exception {
+ final String minValue = Long.MIN_VALUE + "";
+ final String underflow = BigDecimal.valueOf(Long.MIN_VALUE).subtract(BigDecimal.ONE).toString();
+ final String maxValue = Long.MAX_VALUE + "";
+ final String overflow = BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE).toString();
+
+ mockScalarResults(minValue, maxValue, "1.23", null, "NaN", underflow, overflow);
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getLong, ResultSet::getLong);
+
+ matcher.nextAndAssertEquals(Long.MIN_VALUE);
+ matcher.nextAndAssertEquals(Long.MAX_VALUE);
+ matcher.nextAndAssertEquals(1L);
+ // NULL == 0
+ matcher.nextAndAssertEquals((long) 0);
+ matcher.nextAndAssertError(JdbcSqlExceptionImpl.class, "NaN is not a valid number");
+ matcher.nextAndAssertError(
+ JdbcSqlExceptionImpl.class, "Value out of range for long: " + underflow);
+ matcher.nextAndAssertError(
+ JdbcSqlExceptionImpl.class, "Value out of range for long: " + overflow);
+ }
+ }
+
+ // TODO(thiagotnunes): Confirm that it is ok to wrap around in under / over flows (like pg)
+ @Test
+ public void testGetFloat() throws Exception {
+ final String minValue = Float.MIN_VALUE + "";
+ final String underflow =
+ BigDecimal.valueOf(Float.MIN_VALUE).subtract(BigDecimal.ONE).toString();
+ final String maxValue = (Float.MAX_VALUE - 1) + "";
+ final String overflow = BigDecimal.valueOf(Float.MAX_VALUE).add(BigDecimal.ONE).toString();
+
+ mockScalarResults(
+ minValue, maxValue, "1.23", null, "NaN", "-Infinity", "+Infinity", underflow, overflow);
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getFloat, ResultSet::getFloat);
+
+ matcher.nextAndAssertEquals(Float.MIN_VALUE);
+ matcher.nextAndAssertEquals(Float.MAX_VALUE);
+ matcher.nextAndAssertEquals(1.23F);
+ // NULL == 0
+ matcher.nextAndAssertEquals(0F);
+ matcher.nextAndAssertEquals(Float.NaN);
+ matcher.nextAndAssertEquals(Float.NEGATIVE_INFINITY);
+ matcher.nextAndAssertEquals(Float.POSITIVE_INFINITY);
+ // Value rolls back to 0 + (underflow value)
+ matcher.nextAndAssertEquals(-1F);
+ // Value is capped at Float.MAX_VALUE
+ matcher.nextAndAssertEquals(Float.MAX_VALUE);
+ }
+ }
+
+ @Test
+ public void testGetDouble() throws Exception {
+ final String minValue = Double.MIN_VALUE + "";
+ final String underflow =
+ BigDecimal.valueOf(Double.MIN_VALUE).subtract(BigDecimal.ONE).toString();
+ final String maxValue = (Double.MAX_VALUE - 1) + "";
+ final String overflow = BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.ONE).toString();
+
+ mockScalarResults(
+ minValue, maxValue, "1.23", null, "NaN", "-Infinity", "+Infinity", underflow, overflow);
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getDouble, ResultSet::getDouble);
+
+ matcher.nextAndAssertEquals(Double.MIN_VALUE);
+ matcher.nextAndAssertEquals(Double.MAX_VALUE);
+ matcher.nextAndAssertEquals(1.23D);
+ // NULL == 0
+ matcher.nextAndAssertEquals(0D);
+ matcher.nextAndAssertEquals(Double.NaN);
+ matcher.nextAndAssertEquals(Double.NEGATIVE_INFINITY);
+ matcher.nextAndAssertEquals(Double.POSITIVE_INFINITY);
+ // Value rolls back to 0 + (underflow value)
+ matcher.nextAndAssertEquals(-1D);
+ // Value is capped at Double.MAX_VALUE
+ matcher.nextAndAssertEquals(Double.MAX_VALUE);
+ }
+ }
+
+ @Test
+ public void testGetBigDecimal() throws Exception {
+ final String maxScale = String.join("", Collections.nCopies(MAX_PG_NUMERIC_SCALE, "1"));
+ final String maxPrecision =
+ "0." + String.join("", Collections.nCopies(MAX_PG_NUMERIC_PRECISION, "2"));
+
+ mockScalarResults(maxScale, maxPrecision, "0", "1.23", null, "NaN");
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher matcher =
+ resultSetMatcherFrom(resultSet, ResultSet::getBigDecimal, ResultSet::getBigDecimal);
+
+ // Default representation is BigDecimal
+ matcher.nextAndAssertEquals(new BigDecimal(maxScale));
+ matcher.nextAndAssertEquals(new BigDecimal(maxPrecision));
+ matcher.nextAndAssertEquals(BigDecimal.ZERO);
+ matcher.nextAndAssertEquals(new BigDecimal("1.23"));
+ matcher.nextAndAssertEquals(null);
+ matcher.nextAndAssertError(JdbcSqlExceptionImpl.class, "NaN is not a valid number");
+ }
+ }
+
+ @Test
+ public void testGetObject() throws Exception {
+ final String maxScale = String.join("", Collections.nCopies(MAX_PG_NUMERIC_SCALE, "1"));
+ final String maxPrecision =
+ "0." + String.join("", Collections.nCopies(MAX_PG_NUMERIC_PRECISION, "2"));
+
+ mockScalarResults(maxScale, maxPrecision, null, "NaN", "-Infinity", "+Infinity");
+
+ try (Statement statement = connection.createStatement();
+ ResultSet resultSet = statement.executeQuery(QUERY)) {
+
+ final ResultSetMatcher