Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support JDBC PreparedStatement #23

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public BinaryParser(Object item) {
this.item = (byte[]) item;
}

public BinaryParser(byte[] item, FormatCode formatCode) {
this.item = item;
}

@Override
protected String stringParse() {
return PGbytea.toPGString(this.item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,6 +35,24 @@ 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 = Boolean.valueOf(stringValue);
break;
case BINARY:
if (!(item instanceof byte[])) {
throw new IllegalArgumentException(
"Unsupported input value for binary encoding: " + item.getClass());
}
this.item = ByteConverter.bool((byte[]) item, 0);
break;
default:
throw new IllegalArgumentException("Unsupported format: " + formatCode);
}
}

@Override
public Boolean getItem() {
return this.item;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,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);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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<Double> {
Expand All @@ -29,8 +30,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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<Integer> {
Expand All @@ -29,8 +30,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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<Long> {
Expand All @@ -29,8 +30,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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;

/** Translate from wire protocol to {@link BigDecimal}. */
public class NumericParser extends Parser<BigDecimal> {

public NumericParser(ResultSet item, int position) throws SQLException {
this.item = item.getBigDecimal(position);
}

public NumericParser(Object item) {
this.item = (BigDecimal) item;
}

public NumericParser(byte[] item, FormatCode formatCode) {
switch (formatCode) {
case TEXT:
case BINARY:
this.item = new BigDecimal(new String(item));
break;
default:
throw new IllegalArgumentException("Unsupported format: " + formatCode);
}
}

@Override
public BigDecimal getItem() {
return this.item;
}

@Override
protected String stringParse() {
return this.item.toPlainString();
}

@Override
protected byte[] binaryParse() {
return toBinary(this.item, Types.NUMERIC);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,40 +29,86 @@
*/
public abstract class Parser<T> {

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
*
* <p>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);
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);
case Oid.TIMESTAMP:
return new TimestampParser(item);
return new StringParser(item, formatCode);
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);
}
Expand Down Expand Up @@ -140,6 +186,30 @@ protected static Parser create(Object result, int oidType) {
}
}

/**
* Converts a binary wire value to a Java type.
*
* @param value The value to convert.
* @param type Type of the value. The type should be an {@link Oid} constant.
* @return Object as a Java wrapper class of a primitive type, for example {@link Boolean}, {@link
* String} or {@link Integer}.
*/
public static Object fromBinary(byte[] value, int type) {
byte[] result;
switch (type) {
case Oid.BOOL:
return ByteConverter.bool(value, 0);
case Oid.INT4:
return ByteConverter.int4(value, 0);
case Oid.FLOAT8:
return ByteConverter.float8(value, 0);
case Oid.INT8:
return ByteConverter.int8(value, 0);
default:
throw new IllegalArgumentException("Type " + type + " is not valid!");
}
}

/**
* Convert a specified candidate type from specified type to binary.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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);
}

Expand Down
Loading