diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java index 35e20893f..e6dd3c8e8 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/ArrayParser.java @@ -104,17 +104,17 @@ protected String spannerParse() { protected byte[] binaryParse() { ByteArrayOutputStream arrayStream = new ByteArrayOutputStream(); try { - arrayStream.write(toBinary(1, Types.INTEGER)); // dimension - arrayStream.write(toBinary(1, Types.INTEGER)); // Set null flag - arrayStream.write(toBinary(this.arrayType, Types.INTEGER)); // Set type - arrayStream.write(toBinary(this.item.size(), Types.INTEGER)); // Set array length - arrayStream.write(toBinary(0, Types.INTEGER)); // Lower bound (?) + arrayStream.write(IntegerParser.binaryParse(1)); // dimension + arrayStream.write(IntegerParser.binaryParse(1)); // Set null flag + arrayStream.write(IntegerParser.binaryParse(this.arrayType)); // Set type + arrayStream.write(IntegerParser.binaryParse(this.item.size())); // Set array length + arrayStream.write(IntegerParser.binaryParse(0)); // Lower bound (?) for (Object currentItem : this.item) { if (currentItem == null) { - arrayStream.write(toBinary(-1, Types.INTEGER)); + arrayStream.write(IntegerParser.binaryParse(-1)); } else { byte[] data = Parser.create(currentItem, this.arrayType).binaryParse(); - arrayStream.write(toBinary(data.length, Types.INTEGER)); + arrayStream.write(IntegerParser.binaryParse(data.length)); arrayStream.write(data); } } diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParser.java index 634a41a77..cdbe2a7ef 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BinaryParser.java @@ -14,6 +14,7 @@ package com.google.cloud.spanner.pgadapter.parsers; +import java.nio.charset.StandardCharsets; import java.sql.ResultSet; import java.sql.SQLException; import org.postgresql.util.PGbytea; @@ -32,6 +33,24 @@ public BinaryParser(Object item) { this.item = (byte[]) item; } + public BinaryParser(byte[] item, FormatCode formatCode) { + switch (formatCode) { + case TEXT: + try { + this.item = PGbytea.toBytes(item); + break; + } catch (SQLException e) { + throw new IllegalArgumentException( + "Invalid binary value: " + new String(item, StandardCharsets.UTF_8), e); + } + case BINARY: + this.item = item; + break; + default: + throw new IllegalArgumentException("Unsupported format: " + formatCode); + } + } + @Override protected String stringParse() { return PGbytea.toPGString(this.item); diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java index 25184db2f..2f4070a6d 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/BooleanParser.java @@ -16,6 +16,7 @@ import java.sql.ResultSet; import java.sql.SQLException; +import org.postgresql.util.ByteConverter; /** * Parse specified data to boolean. For most cases it is simply translating from chars 't'/'f' to @@ -34,6 +35,20 @@ public BooleanParser(Object item) { this.item = (Boolean) item; } + public BooleanParser(byte[] item, FormatCode formatCode) { + switch (formatCode) { + case TEXT: + String stringValue = new String(item, UTF8); + this.item = stringValue.equals(TRUE_KEY); + break; + case BINARY: + this.item = ByteConverter.bool(item, 0); + break; + default: + throw new IllegalArgumentException("Unsupported format: " + formatCode); + } + } + @Override public Boolean getItem() { return this.item; @@ -48,4 +63,11 @@ protected String stringParse() { protected String spannerParse() { return Boolean.toString(this.item); } + + @Override + protected byte[] binaryParse() { + byte[] result = new byte[1]; + ByteConverter.bool(result, 0, this.item); + return result; + } } diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DateParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DateParser.java index 610cb7187..03b1d1056 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DateParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DateParser.java @@ -17,7 +17,6 @@ import com.google.common.base.Preconditions; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Types; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDate; @@ -34,10 +33,20 @@ public DateParser(Object item) { this.item = (java.sql.Date) item; } - public DateParser(byte[] item) { - long days = ByteConverter.int4(item, 0) + PG_EPOCH_DAYS; - this.validateRange(days); - this.item = java.sql.Date.valueOf(LocalDate.ofEpochDay(days)); + public DateParser(byte[] item, FormatCode formatCode) { + switch (formatCode) { + case TEXT: + String stringValue = new String(item, UTF8); + this.item = java.sql.Date.valueOf(stringValue); + break; + case BINARY: + long days = ByteConverter.int4(item, 0) + PG_EPOCH_DAYS; + this.validateRange(days); + this.item = java.sql.Date.valueOf(LocalDate.ofEpochDay(days)); + break; + default: + throw new IllegalArgumentException("Unsupported format: " + formatCode); + } } /** @@ -74,9 +83,9 @@ protected String stringParse() { @Override protected byte[] binaryParse() { - Long days = this.item.toLocalDate().toEpochDay() - PG_EPOCH_DAYS; - this.validateRange(days); - return toBinary(days.intValue(), Types.INTEGER); + long days = this.item.toLocalDate().toEpochDay() - PG_EPOCH_DAYS; + int daysAsInt = validateRange(days); + return IntegerParser.binaryParse(daysAsInt); } /** @@ -85,9 +94,10 @@ protected byte[] binaryParse() { * * @param days Number of days to validate. */ - private void validateRange(long days) { + private int validateRange(long days) { if (days > Integer.MAX_VALUE) { throw new IllegalArgumentException("Date is out of range, epoch day=" + days); } + return (int) days; } } diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParser.java index 1249cf8e0..d899c4e86 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/DoubleParser.java @@ -16,7 +16,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Types; +import org.postgresql.util.ByteConverter; /** Translate from wire protocol to double. */ public class DoubleParser extends Parser { @@ -29,8 +29,17 @@ public DoubleParser(Object item) { this.item = (Double) item; } - public DoubleParser(byte[] item) { - this.item = Double.valueOf(new String(item)); + public DoubleParser(byte[] item, FormatCode formatCode) { + switch (formatCode) { + case TEXT: + this.item = Double.valueOf(new String(item)); + break; + case BINARY: + this.item = ByteConverter.float8(item, 0); + break; + default: + throw new IllegalArgumentException("Unsupported format: " + formatCode); + } } @Override @@ -45,6 +54,8 @@ protected String stringParse() { @Override protected byte[] binaryParse() { - return toBinary(this.item, Types.DOUBLE); + byte[] result = new byte[8]; + ByteConverter.float8(result, 0, this.item); + return result; } } diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParser.java index bd3bb7ffd..d4ca90415 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/IntegerParser.java @@ -16,7 +16,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Types; +import org.postgresql.util.ByteConverter; /** Translate from wire protocol to int. */ public class IntegerParser extends Parser { @@ -29,8 +29,17 @@ public IntegerParser(Object item) { this.item = (Integer) item; } - public IntegerParser(byte[] item) { - this.item = Integer.valueOf(new String(item)); + public IntegerParser(byte[] item, FormatCode formatCode) { + switch (formatCode) { + case TEXT: + this.item = Integer.valueOf(new String(item)); + break; + case BINARY: + this.item = ByteConverter.int4(item, 0); + break; + default: + throw new IllegalArgumentException("Unsupported format: " + formatCode); + } } @Override @@ -45,6 +54,12 @@ protected String stringParse() { @Override protected byte[] binaryParse() { - return toBinary(this.item, Types.INTEGER); + return binaryParse(this.item); + } + + public static byte[] binaryParse(int value) { + byte[] result = new byte[4]; + ByteConverter.int4(result, 0, value); + return result; } } diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/LongParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/LongParser.java index 3ab8b6c44..3839efa49 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/LongParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/LongParser.java @@ -16,7 +16,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Types; +import org.postgresql.util.ByteConverter; /** Translate from wire protocol to long. */ public class LongParser extends Parser { @@ -29,8 +29,17 @@ public LongParser(Object item) { this.item = (Long) item; } - public LongParser(byte[] item) { - this.item = Long.valueOf(new String(item)); + public LongParser(byte[] item, FormatCode formatCode) { + switch (formatCode) { + case TEXT: + this.item = Long.valueOf(new String(item)); + break; + case BINARY: + this.item = ByteConverter.int8(item, 0); + break; + default: + throw new IllegalArgumentException("Unsupported format: " + formatCode); + } } @Override @@ -45,6 +54,8 @@ protected String stringParse() { @Override protected byte[] binaryParse() { - return toBinary(this.item, Types.BIGINT); + byte[] result = new byte[8]; + ByteConverter.int8(result, 0, this.item); + return result; } } diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/NumericParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/NumericParser.java new file mode 100644 index 000000000..0ce9706aa --- /dev/null +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/NumericParser.java @@ -0,0 +1,69 @@ +// 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.pgadapter.parsers; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.postgresql.util.ByteConverter; + +/** Translate from wire protocol to {@link Number}. */ +public class NumericParser extends Parser { + public NumericParser(ResultSet item, int position) throws SQLException { + // This should be either a BigDecimal value or a Double.NaN. + this.item = (Number) item.getObject(position); + } + + public NumericParser(Object item) { + this.item = (Number) item; + } + + public NumericParser(byte[] item, FormatCode formatCode) { + switch (formatCode) { + case TEXT: + String stringValue = new String(item); + if (stringValue.equalsIgnoreCase("NaN")) { + this.item = Double.NaN; + } else { + this.item = new BigDecimal(new String(item)); + } + break; + case BINARY: + this.item = ByteConverter.numeric(item, 0, item.length); + break; + default: + throw new IllegalArgumentException("Unsupported format: " + formatCode); + } + } + + @Override + public Number getItem() { + return this.item; + } + + @Override + protected String stringParse() { + return Double.isNaN(this.item.doubleValue()) ? "NaN" : ((BigDecimal) this.item).toPlainString(); + } + + @Override + protected byte[] binaryParse() { + if (Double.isNaN(this.item.doubleValue())) { + return "NaN".getBytes(StandardCharsets.UTF_8); + } + return ByteConverter.numeric((BigDecimal) this.item); + } +} diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java index e62a977a1..62c2ed3da 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/Parser.java @@ -21,7 +21,6 @@ import java.sql.SQLException; import java.sql.Types; import org.postgresql.core.Oid; -import org.postgresql.util.ByteConverter; /** * Parser is the parsing superclass, used to take wire format data types and convert them @@ -29,40 +28,88 @@ */ public abstract class Parser { + public enum FormatCode { + TEXT, + BINARY; + + public static FormatCode of(short code) { + FormatCode[] values = FormatCode.values(); + if (code < 0 || code > values.length) { + throw new IllegalArgumentException("Unknown format code: " + code); + } + return values[code]; + } + } + public static final long PG_EPOCH_SECONDS = 946684800L; public static final long PG_EPOCH_DAYS = PG_EPOCH_SECONDS / 86400L; protected static final Charset UTF8 = StandardCharsets.UTF_8; protected T item; + /** + * Untyped parameters are allowed in PostgreSQL, and some drivers use this deliberately to work + * around side effects regarding dates and timestamps with and without timezones. See for example + * https://github.com/pgjdbc/pgjdbc/blob/3af3b32cc5b77db3e7af1cbc217d6288fd0cf9b9/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java#L1322 + * + *

TODO: This method currently only checks whether the value could be a timestamp with + * timezone. Date and timestamp without timezone are also known to be sent with type code {@link + * Oid#UNSPECIFIED} by the JDBC driver, and must be implemented once Spanner supports these types. + * + * @param item The value to guess the type for + * @param formatCode The encoding that is used for the value + * @return The {@link Oid} type code that is guessed for the value or {@link Oid#UNSPECIFIED} if + * no type could be guessed. + */ + private static int guessType(byte[] item, FormatCode formatCode) { + if (formatCode == FormatCode.TEXT) { + String value = new String(item, StandardCharsets.UTF_8); + if (TimestampParser.isTimestamp(value)) { + return Oid.TIMESTAMPTZ; + } + } + return Oid.UNSPECIFIED; + } + /** * Factory method to create a Parser subtype with a designated type from a byte array. * - * @param item The data to be parsed. - * @param oidType The type of the designated data. + * @param item The data to be parsed + * @param oidType The type of the designated data + * @param formatCode The format of the data to be parsed * @return The parser object for the designated data type. */ - public static Parser create(byte[] item, int oidType) { + public static Parser create(byte[] item, int oidType, FormatCode formatCode) { switch (oidType) { case Oid.BOOL: - return new BooleanParser(item); + case Oid.BIT: + return new BooleanParser(item, formatCode); + case Oid.BYTEA: case Oid.BIT_ARRAY: - return new BinaryParser(item); + return new BinaryParser(item, formatCode); case Oid.DATE: - return new DateParser(item); + return new DateParser(item, formatCode); case Oid.FLOAT8: - return new DoubleParser(item); + return new DoubleParser(item, formatCode); case Oid.INT8: - return new LongParser(item); + return new LongParser(item, formatCode); case Oid.INT4: - return new IntegerParser(item); + return new IntegerParser(item, formatCode); case Oid.NUMERIC: - return new StringParser(item); + return new NumericParser(item, formatCode); case Oid.TEXT: - case Oid.UNSPECIFIED: case Oid.VARCHAR: - return new StringParser(item); + return new StringParser(item, formatCode); case Oid.TIMESTAMP: - return new TimestampParser(item); + case Oid.TIMESTAMPTZ: + return new TimestampParser(item, formatCode); + case Oid.UNSPECIFIED: + int type = guessType(item, formatCode); + if (type == Oid.UNSPECIFIED) { + throw new IllegalArgumentException( + String.format( + "Could not guess type of value %s", new String(item, StandardCharsets.UTF_8))); + } + return create(item, type, formatCode); default: throw new IllegalArgumentException("Illegal or unknown element type: " + oidType); } @@ -140,37 +187,6 @@ protected static Parser create(Object result, int oidType) { } } - /** - * Convert a specified candidate type from specified type to binary. - * - * @param candidate Specified object to convert. - * @param type Type of specified object. - * @return Object in binary representation. - */ - public static byte[] toBinary(Object candidate, int type) { - byte[] result; - switch (type) { - case Types.BOOLEAN: - result = new byte[1]; - ByteConverter.bool(result, 0, (Boolean) candidate); - return result; - case Types.INTEGER: - result = new byte[4]; - ByteConverter.int4(result, 0, (Integer) candidate); - return result; - case Types.DOUBLE: - result = new byte[8]; - ByteConverter.float8(result, 0, (Double) candidate); - return result; - case Types.BIGINT: - result = new byte[8]; - ByteConverter.int8(result, 0, (Long) candidate); - return result; - default: - throw new IllegalArgumentException("Type " + type + " is not valid!"); - } - } - public T getItem() { return this.item; } @@ -221,7 +237,5 @@ protected byte[] spannerBinaryParse() { } /** Used to parse data type onto binary. Override this to change binary representation. */ - protected byte[] binaryParse() { - return this.stringParse().getBytes(UTF8); - } + protected abstract byte[] binaryParse(); } diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/StringParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/StringParser.java index 4bf4c9f5a..2c4ea161d 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/StringParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/StringParser.java @@ -14,6 +14,7 @@ package com.google.cloud.spanner.pgadapter.parsers; +import java.nio.charset.StandardCharsets; import java.sql.ResultSet; import java.sql.SQLException; @@ -28,7 +29,7 @@ public StringParser(Object item) { this.item = (String) item; } - public StringParser(byte[] item) { + public StringParser(byte[] item, FormatCode formatCode) { this.item = new String(item, UTF8); } @@ -41,4 +42,9 @@ public String getItem() { protected String stringParse() { return this.item; } + + @Override + protected byte[] binaryParse() { + return this.item.getBytes(StandardCharsets.UTF_8); + } } diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java index 03c5102e9..f6096f225 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/parsers/TimestampParser.java @@ -15,10 +15,13 @@ package com.google.cloud.spanner.pgadapter.parsers; import com.google.common.base.Preconditions; +import java.nio.charset.StandardCharsets; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; -import java.sql.Types; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; import java.util.regex.Pattern; import org.postgresql.util.ByteConverter; @@ -43,6 +46,11 @@ public class TimestampParser extends Parser { private static final Pattern TIMESTAMP_PATTERN = Pattern.compile(TIMESTAMP_REGEX); + private static final DateTimeFormatter TIMESTAMP_WITHOUT_FRACTION_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX"); + private static final DateTimeFormatter TIMESTAMP_WITH_FRACTION_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSSSSSX"); + public TimestampParser(ResultSet item, int position) throws SQLException { this.item = item.getTimestamp(position); } @@ -51,9 +59,30 @@ public TimestampParser(Object item) { this.item = (Timestamp) item; } - public TimestampParser(byte[] item) { - long micros = ByteConverter.int8(item, 0); - this.item = new Timestamp(micros); + public TimestampParser(byte[] item, FormatCode formatCode) { + switch (formatCode) { + case TEXT: + String stringValue = new String(item, StandardCharsets.UTF_8); + TemporalAccessor temporalAccessor; + if (stringValue.contains(".")) { + temporalAccessor = TIMESTAMP_WITH_FRACTION_FORMATTER.parse(stringValue); + } else { + temporalAccessor = TIMESTAMP_WITHOUT_FRACTION_FORMATTER.parse(stringValue); + } + this.item = Timestamp.from(Instant.from(temporalAccessor)); + break; + case BINARY: + long pgMicros = ByteConverter.int8(item, 0); + com.google.cloud.Timestamp ts = com.google.cloud.Timestamp.ofTimeMicroseconds(pgMicros); + long javaSeconds = ts.getSeconds() + PG_EPOCH_SECONDS; + int javaNanos = ts.getNanos(); + this.item = + com.google.cloud.Timestamp.ofTimeSecondsAndNanos(javaSeconds, javaNanos) + .toSqlTimestamp(); + break; + default: + throw new IllegalArgumentException("Unsupported format: " + formatCode); + } } /** @@ -87,7 +116,9 @@ protected byte[] binaryParse() { long microseconds = ((this.item.getTime() / MILLISECONDS_IN_SECOND - PG_EPOCH_SECONDS) * MICROSECONDS_IN_SECOND) + (this.item.getNanos() / NANOSECONDS_IN_MICROSECONDS); - return toBinary(microseconds, Types.BIGINT); + byte[] result = new byte[8]; + ByteConverter.int8(result, 0, microseconds); + return result; } /** diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java index 50aba980a..880932371 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/statements/IntermediatePreparedStatement.java @@ -17,6 +17,7 @@ import com.google.cloud.spanner.pgadapter.metadata.DescribeMetadata; import com.google.cloud.spanner.pgadapter.metadata.SQLMetadata; import com.google.cloud.spanner.pgadapter.parsers.Parser; +import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode; import com.google.cloud.spanner.pgadapter.utils.Converter; import com.google.common.collect.SetMultimap; import java.nio.charset.Charset; @@ -140,13 +141,9 @@ public IntermediatePortalStatement bind( for (int index = 0; index < parameters.length; index++) { short formatCode = portal.getParameterFormatCode(index); int type = this.parseType(parameters, index); - if (formatCode == 0) { - for (Integer position : parameterIndexToPositions.get(index)) { - ((PreparedStatement) portal.statement) - .setObject(position, Parser.create(parameters[index], type).getItem()); - } - } else { - throw new IllegalStateException("Unimplemented"); + for (Integer position : parameterIndexToPositions.get(index)) { + Object value = Parser.create(parameters[index], type, FormatCode.of(formatCode)).getItem(); + ((PreparedStatement) portal.statement).setObject(position, value); } } return portal; diff --git a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java index 07ce7e852..b95a6d866 100644 --- a/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java +++ b/src/main/java/com/google/cloud/spanner/pgadapter/wireprotocol/ParseMessage.java @@ -40,9 +40,6 @@ public ParseMessage(ConnectionHandler connection) throws Exception { short numberOfParameters = this.inputStream.readShort(); for (int i = 0; i < numberOfParameters; i++) { int type = this.inputStream.readInt(); - if (type == 0) { - throw new IllegalArgumentException("PgAdapter does not support untyped parameters."); - } this.parameterDataTypes.add(type); } this.statement = new IntermediatePreparedStatement(queryString, connection.getJdbcConnection()); diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcTest.java index 0c0948735..29718731e 100644 --- a/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcTest.java +++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITJdbcTest.java @@ -52,6 +52,8 @@ public static void setup() throws Exception { // Make sure the PG JDBC driver is loaded. Class.forName("org.postgresql.Driver"); + // TODO: Refactor the integration tests to use a common subclass, as this is repeated in each + // class. testEnv.setUp(); if (testEnv.isUseExistingDb()) { database = testEnv.getExistingDatabase(); diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITParameterizedQueryTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITParameterizedQueryTest.java index 8efe634b7..9928a5ea3 100644 --- a/src/test/java/com/google/cloud/spanner/pgadapter/ITParameterizedQueryTest.java +++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITParameterizedQueryTest.java @@ -63,6 +63,8 @@ public void setUp() throws Exception { List values = new ArrayList<>(Arrays.asList("(1, 1, '1')", "(2, 20, 'Joe')", "(3, 23, 'Jack')")); String dml = "INSERT INTO users (id, age, name) VALUES " + String.join(", ", values); + // TODO: Refactor the integration tests to use a common subclass, as this is repeated in each + // class. testEnv.setUp(); Database db = testEnv.createDatabase(); testEnv.updateDdl(db.getId().getDatabase(), Arrays.asList(ddl)); diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ITQueryTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ITQueryTest.java index e9a4e8e11..0301b1b92 100644 --- a/src/test/java/com/google/cloud/spanner/pgadapter/ITQueryTest.java +++ b/src/test/java/com/google/cloud/spanner/pgadapter/ITQueryTest.java @@ -63,6 +63,8 @@ public void setUp() throws Exception { Arrays.asList( "(1, 1, '1', '12345.67890')", "(2, 20, 'ABCD', 'NaN')", "(3, 23, 'Jack', '22')")); String dml = "INSERT INTO users (id, age, name, data) VALUES " + String.join(", ", values); + // TODO: Refactor the integration tests to use a common subclass, as this is repeated in each + // class. testEnv.setUp(); Database db = testEnv.createDatabase(); testEnv.updateDdl(db.getId().getDatabase(), Arrays.asList(ddl)); diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ParserTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ParserTest.java index 3dcc6b0f9..07997c91e 100644 --- a/src/test/java/com/google/cloud/spanner/pgadapter/ParserTest.java +++ b/src/test/java/com/google/cloud/spanner/pgadapter/ParserTest.java @@ -17,6 +17,8 @@ import static com.google.cloud.spanner.pgadapter.parsers.copy.Copy.parse; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThrows; @@ -30,7 +32,9 @@ import com.google.cloud.spanner.pgadapter.parsers.DoubleParser; import com.google.cloud.spanner.pgadapter.parsers.IntegerParser; import com.google.cloud.spanner.pgadapter.parsers.LongParser; +import com.google.cloud.spanner.pgadapter.parsers.NumericParser; import com.google.cloud.spanner.pgadapter.parsers.Parser; +import com.google.cloud.spanner.pgadapter.parsers.Parser.FormatCode; import com.google.cloud.spanner.pgadapter.parsers.StringParser; import com.google.cloud.spanner.pgadapter.parsers.TimestampParser; import com.google.cloud.spanner.pgadapter.parsers.copy.CopyTreeParser; @@ -38,6 +42,7 @@ import com.google.cloud.spanner.pgadapter.parsers.copy.CopyTreeParser.CopyOptions.FromTo; import com.google.cloud.spanner.pgadapter.parsers.copy.ParseException; import com.google.cloud.spanner.pgadapter.parsers.copy.TokenMgrError; +import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.sql.Array; import java.sql.Date; @@ -50,6 +55,7 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.Mockito; +import org.postgresql.core.Oid; import org.postgresql.util.ByteConverter; /** @@ -70,6 +76,29 @@ private void validate( assertThat(parser.parse(DataFormat.SPANNER), is(equalTo(spannerResult))); } + private void validateCreateBinary(byte[] item, int oid, Object value) { + Parser binary = Parser.create(item, oid, FormatCode.BINARY); + + assertParserValueEqual(binary, value); + } + + private void validateCreateText(byte[] item, int oid, Object value) { + Parser text = Parser.create(item, oid, FormatCode.TEXT); + + assertParserValueEqual(text, value); + } + + private void assertParserValueEqual(Parser parser, Object value) { + if (value instanceof byte[]) { + assertArrayEquals((byte[]) value, (byte[]) parser.getItem()); + } else if (value instanceof Date) { + // To prevent false failures because dates are automatically appended with the local timezone. + assertEquals(value.toString(), parser.getItem().toString()); + } else { + assertEquals(value, parser.getItem()); + } + } + @Test public void testPositiveLongParsing() { long value = 1234567890L; @@ -79,6 +108,8 @@ public void testPositiveLongParsing() { Parser parsedValue = new LongParser(value); validate(parsedValue, byteResult, stringResult, stringResult); + validateCreateBinary(byteResult, Oid.INT8, value); + validateCreateText(stringResult, Oid.INT8, value); } @Test @@ -90,6 +121,8 @@ public void testNegativeLongParsing() { Parser parsedValue = new LongParser(value); validate(parsedValue, byteResult, stringResult, stringResult); + validateCreateBinary(byteResult, Oid.INT8, value); + validateCreateText(stringResult, Oid.INT8, value); } @Test @@ -101,6 +134,8 @@ public void testPositiveIntegerParsing() { Parser parsedValue = new IntegerParser(value); validate(parsedValue, byteResult, stringResult, stringResult); + validateCreateBinary(byteResult, Oid.INT4, value); + validateCreateText(stringResult, Oid.INT4, value); } @Test @@ -112,6 +147,8 @@ public void testNegativeIntegerParsing() { Parser parsedValue = new IntegerParser(value); validate(parsedValue, byteResult, stringResult, stringResult); + validateCreateBinary(byteResult, Oid.INT4, value); + validateCreateText(stringResult, Oid.INT4, value); } @Test @@ -123,6 +160,8 @@ public void testPositiveDoubleParsing() { Parser parsedValue = new DoubleParser(value); validate(parsedValue, byteResult, stringResult, stringResult); + validateCreateBinary(byteResult, Oid.FLOAT8, value); + validateCreateText(stringResult, Oid.FLOAT8, value); } @Test @@ -134,28 +173,36 @@ public void testNegativeDoubleParsing() { Parser parsedValue = new DoubleParser(value); validate(parsedValue, byteResult, stringResult, stringResult); + validateCreateBinary(byteResult, Oid.FLOAT8, value); + validateCreateText(stringResult, Oid.FLOAT8, value); } @Test public void testFalseBooleanParsing() { boolean value = false; + byte[] byteResult = {0}; byte[] stringResult = {'f'}; byte[] spannerResult = {'f', 'a', 'l', 's', 'e'}; Parser parsedValue = new BooleanParser(value); - validate(parsedValue, stringResult, stringResult, spannerResult); + validate(parsedValue, byteResult, stringResult, spannerResult); + validateCreateBinary(byteResult, Oid.BIT, value); + validateCreateText(stringResult, Oid.BIT, value); } @Test public void testTrueBooleanParsing() { boolean value = true; + byte[] byteResult = {1}; byte[] stringResult = {'t'}; byte[] spannerResult = {'t', 'r', 'u', 'e'}; Parser parsedValue = new BooleanParser(value); - validate(parsedValue, stringResult, stringResult, spannerResult); + validate(parsedValue, byteResult, stringResult, spannerResult); + validateCreateBinary(byteResult, Oid.BIT, value); + validateCreateText(stringResult, Oid.BIT, value); } @Test @@ -168,13 +215,15 @@ public void testDateParsing() { Parser parsedValue = new DateParser(value); validate(parsedValue, byteResult, stringResult, stringResult); + validateCreateBinary(byteResult, Oid.DATE, value); + validateCreateText(stringResult, Oid.DATE, value); } @Test(expected = IllegalArgumentException.class) public void testDateParsingRejectsInvalidDateTooLong() { byte[] result = new byte[4]; ByteConverter.int4(result, 0, Integer.MAX_VALUE); - new DateParser(result); + new DateParser(result, FormatCode.BINARY); } @Test @@ -202,6 +251,8 @@ public void testStringParsing() { Parser parsedValue = new StringParser(value); validate(parsedValue, stringResult, stringResult, stringResult); + validateCreateBinary(stringResult, Oid.VARCHAR, value); + validateCreateText(stringResult, Oid.VARCHAR, value); } @Test @@ -213,6 +264,7 @@ public void testTimestampParsingBytePart() { Parser parsedValue = new TimestampParser(value); assertThat(parsedValue.parse(DataFormat.POSTGRESQL_BINARY), is(equalTo(byteResult))); + validateCreateBinary(byteResult, Oid.TIMESTAMP, value); } @Test @@ -252,6 +304,8 @@ public void testBinaryParsing() { Parser parsedValue = new BinaryParser(value); validate(parsedValue, byteResult, stringResult, byteResult); + validateCreateBinary(byteResult, Oid.BYTEA, value); + validateCreateText(stringResult, Oid.BYTEA, value); } @Test @@ -318,29 +372,33 @@ public void testArrayArrayParsingFails() throws SQLException { @Test public void testNumericParsing() throws SQLException { - String value = "1234567890.1234567890"; + BigDecimal value = new BigDecimal("1234567890.1234567890"); + byte[] byteResult = ByteConverter.numeric(new BigDecimal("1234567890.1234567890")); byte[] stringResult = { '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '.', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' }; - Parser parser = new StringParser(value); + Parser parser = new NumericParser(value); - validate(parser, stringResult, stringResult, stringResult); + validate(parser, byteResult, stringResult, stringResult); assertThat(parser.getItem(), is(equalTo(value))); + validateCreateBinary(byteResult, Oid.NUMERIC, value); + validateCreateText(stringResult, Oid.NUMERIC, value); } @Test public void testNumericParsingNaN() throws SQLException { - String value = "NaN"; + Number value = Double.NaN; byte[] stringResult = {'N', 'a', 'N'}; - Parser parser = new StringParser(value); + NumericParser parser = new NumericParser(value); validate(parser, stringResult, stringResult, stringResult); assertThat(parser.getItem(), is(equalTo(value))); + validateCreateText(stringResult, Oid.NUMERIC, value); } @Test diff --git a/src/test/java/com/google/cloud/spanner/pgadapter/ProtocolTest.java b/src/test/java/com/google/cloud/spanner/pgadapter/ProtocolTest.java index 46a03a8a3..d1a0c1fc6 100644 --- a/src/test/java/com/google/cloud/spanner/pgadapter/ProtocolTest.java +++ b/src/test/java/com/google/cloud/spanner/pgadapter/ProtocolTest.java @@ -339,8 +339,8 @@ public void testParseMessage() throws Exception { Assert.assertEquals(outputResult.readInt(), 4); } - @Test(expected = IllegalArgumentException.class) - public void testParseMessageExceptsWithUntypedParameter() throws Exception { + @Test + public void testParseMessageAcceptsUntypedParameter() throws Exception { byte[] messageMetadata = {'P'}; String statementName = "some statement\0"; String payload = @@ -372,6 +372,8 @@ public void testParseMessageExceptsWithUntypedParameter() throws Exception { String expectedMessageName = "some statement"; DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(value)); + ByteArrayOutputStream result = new ByteArrayOutputStream(); + DataOutputStream outputStream = new DataOutputStream(result); Mockito.when(connectionHandler.getJdbcConnection()).thenReturn(connection); Mockito.when(connectionHandler.getConnectionMetadata()).thenReturn(connectionMetadata); @@ -388,6 +390,13 @@ public void testParseMessageExceptsWithUntypedParameter() throws Exception { Mockito.when(connectionHandler.hasStatement(anyString())).thenReturn(false); message.send(); + Mockito.verify(connectionHandler, Mockito.times(1)) + .registerStatement(expectedMessageName, ((ParseMessage) message).getStatement()); + + // ParseCompleteResponse + DataInputStream outputResult = inputStreamFromOutputStream(result); + Assert.assertEquals(outputResult.readByte(), '1'); + Assert.assertEquals(outputResult.readInt(), 4); } @Test(expected = IllegalArgumentException.class)