Skip to content

Add support for prepared statements in JDBC #3116

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

Merged
merged 14 commits into from
Feb 13, 2025
Merged
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 @@ -24,6 +24,8 @@

import com.apple.foundationdb.relational.util.ExcludeFromJacocoGeneratedReport;

import java.sql.Array;
import java.sql.Struct;
import java.sql.Types;

/**
Expand Down Expand Up @@ -95,4 +97,30 @@ public static int getSqlTypeCode(String sqlTypeName) {
throw new IllegalStateException("Unexpected sql type name:" + sqlTypeName);
}
}

public static int getSqlTypeCodeFromObject(Object obj) {
if (obj == null) {
return Types.NULL;
} else if (obj instanceof Long) {
return Types.BIGINT;
} else if (obj instanceof Integer) {
return Types.INTEGER;
} else if (obj instanceof Boolean) {
return Types.BOOLEAN;
} else if (obj instanceof byte[]) {
return Types.BINARY;
} else if (obj instanceof Float) {
return Types.FLOAT;
} else if (obj instanceof Double) {
return Types.DOUBLE;
} else if (obj instanceof String) {
return Types.VARCHAR;
} else if (obj instanceof Array) {
return Types.ARRAY;
} else if (obj instanceof Struct) {
return Types.STRUCT;
} else {
throw new IllegalStateException("Unexpected object type: " + obj.getClass().getName());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public enum ErrorCode {
CANNOT_CONVERT_TYPE("22000"),
INVALID_ROW_COUNT_IN_LIMIT_CLAUSE("2201W"),
INVALID_PARAMETER("22023"),
ARRAY_ELEMENT_ERROR("2202E"),
INVALID_BINARY_REPRESENTATION("22F03"),
INVALID_ARGUMENT_FOR_FUNCTION("22F00"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ public boolean wasNull() throws SQLException {

private Column getColumnInternal(int oneBasedColumn) {
Column c = this.delegate.getColumns().getColumn(PositionalIndex.toProtobuf(oneBasedColumn));
wasNull = c.hasNull() || c.getKindCase().equals(Column.KindCase.KIND_NOT_SET);
wasNull = c.hasNullType() || c.getKindCase().equals(Column.KindCase.KIND_NOT_SET);
return c;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import com.apple.foundationdb.relational.api.ArrayMetaData;
import com.apple.foundationdb.relational.api.Continuation;
import com.apple.foundationdb.relational.api.SqlTypeNamesSupport;
import com.apple.foundationdb.relational.api.StructMetaData;
import com.apple.foundationdb.relational.api.RelationalArray;
import com.apple.foundationdb.relational.api.RelationalResultSet;
Expand Down Expand Up @@ -241,6 +242,129 @@ private static Struct toStruct(RelationalStruct relationalStruct) throws SQLExce
return Struct.newBuilder().setColumns(listColumnBuilder.build()).build();
}

/**
* Return the Java object stored within the proto.
* @param columnType the type of object in the column
* @param column the column to process
* @return the Java object from the Column representation
* @throws SQLException in case of an error
*/
public static Object fromColumn(int columnType, Column column) throws SQLException {
switch (columnType) {
case Types.ARRAY:
checkColumnType(columnType, column.hasArray());
return fromArray(column.getArray());
case Types.BIGINT:
checkColumnType(columnType, column.hasLong());
return column.getLong();
case Types.INTEGER:
checkColumnType(columnType, column.hasInteger());
return column.getInteger();
case Types.BOOLEAN:
checkColumnType(columnType, column.hasBoolean());
return column.getBoolean();
case Types.VARCHAR:
checkColumnType(columnType, column.hasString());
return column.getString();
case Types.BINARY:
checkColumnType(columnType, column.hasBinary());
return column.getBinary().toByteArray();
case Types.DOUBLE:
checkColumnType(columnType, column.hasDouble());
return column.getDouble();
default:
// NULL (java.sql.Types value 0) is not a valid column type for an array and is likely the result of a default value for the
// (optional) array.getElementType() protobuf field.
throw new SQLException("java.sql.Type=" + columnType + " not supported", ErrorCode.ARRAY_ELEMENT_ERROR.getErrorCode());
}
}

private static void checkColumnType(final int expectedColumnType, final boolean columnHasType) throws SQLException {
if (!columnHasType) {
throw new SQLException("Column has wrong type (expected " + expectedColumnType + ")", ErrorCode.WRONG_OBJECT_TYPE.getErrorCode());
}
}

/**
* Return the Java array stored within the proto.
* @param array the array to process
* @return the Java array from the proto representation
* @throws SQLException in case of an error
*/
public static Object[] fromArray(Array array) throws SQLException {
Object[] result = new Object[array.getElementCount()];
final List<Column> elements = array.getElementList();
for (int i = 0 ; i < elements.size() ; i++) {
result[i] = fromColumn(array.getElementType(), elements.get(i));
}
return result;
}

/**
* Return the protobuf {@link Array} for a SQL {@link java.sql.Array}.
* @param array the SQL array
* @return the resulting protobuf array
*/
public static Array toArray(@Nonnull java.sql.Array array) throws SQLException {
Array.Builder builder = Array.newBuilder();
builder.setElementType(array.getBaseType());
for (Object o: (Object[])array.getArray()) {
builder.addElement(toColumn(array.getBaseType(), o));
}
return builder.build();
}

/**
* Create {@link Column} from a Java object.
* Note: In case the column is of a composite type (array) then the actual type has to be a SQL flavor
* ({@link java.sql.Array}.
* Note: In case {@code columnType} is of value {@link Types#NULL}, the {@code obj} parameter is expected to be the
* type of null. That is, the {@code obj} will represent the {@link Types} constant for the type of variable whose
* value is null.
* @param columnType the SQL type to create (from {@link Types})
* @param obj the value to use for the column
* @return the created column
* @throws SQLException in case of error
*/
public static Column toColumn(int columnType, @Nonnull Object obj) throws SQLException {
if (columnType != SqlTypeNamesSupport.getSqlTypeCodeFromObject(obj)) {
throw new SQLException("Column element type does not match object type: " + columnType + " / " + obj.getClass().getSimpleName(),
ErrorCode.WRONG_OBJECT_TYPE.getErrorCode());
}

Column.Builder builder = Column.newBuilder();
switch (columnType) {
case Types.BIGINT:
builder = builder.setLong((Long)obj);
break;
case Types.INTEGER:
builder = builder.setInteger((Integer)obj);
break;
case Types.BOOLEAN:
builder = builder.setBoolean((Boolean)obj);
break;
case Types.VARCHAR:
builder = builder.setString((String)obj);
break;
case Types.BINARY:
builder = builder.setBinary((ByteString)obj);
break;
case Types.DOUBLE:
builder = builder.setDouble((Double)obj);
break;
case Types.ARRAY:
builder = builder.setArray(toArray((java.sql.Array)obj));
break;
case Types.NULL:
builder = builder.setNullType((Integer)obj);
break;
default:
throw new SQLException("java.sql.Type=" + columnType + " not supported",
ErrorCode.UNSUPPORTED_OPERATION.getErrorCode());
}
return builder.build();
}

private static Column toColumn(RelationalStruct relationalStruct, int oneBasedIndex) throws SQLException {
int columnType = relationalStruct.getMetaData().getColumnType(oneBasedIndex);
Column column;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ message Struct {
// Relational Array.
message Array {
repeated Column element = 1;
// The java.sql.Types of the elements of the array. This is somewhat redundant with the type of the
// columns, but it makes it easier to verify correctness.
int32 elementType = 2;
}

// `Column` represents a dynamically typed column which can be either
Expand All @@ -59,7 +62,7 @@ message Array {
message Column {
// The kind/type of column.
oneof kind {
// Represents a null column.
// Deprecated
NullColumn null = 1;
// Represents a double column.
double double = 2;
Expand All @@ -75,15 +78,13 @@ message Column {
Struct struct = 7;
Array array = 8;
bytes binary = 9;

float float = 10;
// Represents a null value. These can be typed, so the value is the java.sql.Types of the parameter
int32 nullType = 11;
}
}

// `NullValue` is a singleton enumeration to represent the null value for the
// `Column` type union.
//
// The JSON representation for `NullValue` is JSON `null`.
// Deprecated
enum NullColumn {
// Null value.
NULL_COLUMN = 0;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* JDBCArrayImpl.java
*
* This source file is part of the FoundationDB open source project
*
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
*
* 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.apple.foundationdb.relational.jdbc;

import com.apple.foundationdb.relational.api.SqlTypeNamesSupport;

import javax.annotation.Nonnull;
import java.sql.Array;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Map;

/**
* A simplistic implementation of a {@link Array} that wraps around a
* {@link com.apple.foundationdb.relational.jdbc.grpc.v1.column.Array}.
* TODO: We can't use the other implementation of array in {@link RelationalArrayFacade} as it makes several assumptions
* that would be incompatible with the need to transfer an array across the wire (e.g. the type of element is missing,
* {@link RelationalArrayFacade#getArray()} returns a type other than a Java array).
*/
public class JDBCArrayImpl implements Array {
@Nonnull
private final com.apple.foundationdb.relational.jdbc.grpc.v1.column.Array underlying;

public JDBCArrayImpl(@Nonnull final com.apple.foundationdb.relational.jdbc.grpc.v1.column.Array underlying) {
this.underlying = underlying;
}

@Override
public String getBaseTypeName() throws SQLException {
return SqlTypeNamesSupport.getSqlTypeName(getBaseType());
}

@Override
public int getBaseType() throws SQLException {
return underlying.getElementType();
}

@Override
public Object getArray() throws SQLException {
return TypeConversion.fromArray(underlying);
}

@Override
public Object getArray(final Map<String, Class<?>> map) throws SQLException {
throw new SQLFeatureNotSupportedException("Custom type mapping is not supported");
}

@Override
public Object getArray(final long index, final int count) throws SQLException {
throw new SQLFeatureNotSupportedException("Array slicing is not supported");
}

@Override
public Object getArray(final long index, final int count, final Map<String, Class<?>> map) throws SQLException {
throw new SQLFeatureNotSupportedException("Custom type mapping is not supported");
}

@Override
public ResultSet getResultSet() throws SQLException {
throw new SQLFeatureNotSupportedException("Array as result set is not supported");
}

@Override
public ResultSet getResultSet(final Map<String, Class<?>> map) throws SQLException {
throw new SQLFeatureNotSupportedException("Array as result set is not supported");
}

@Override
public ResultSet getResultSet(final long index, final int count) throws SQLException {
throw new SQLFeatureNotSupportedException("Array as result set is not supported");
}

@Override
public ResultSet getResultSet(final long index, final int count, final Map<String, Class<?>> map) throws SQLException {
throw new SQLFeatureNotSupportedException("Array as result set is not supported");
}

@Override
public void free() throws SQLException {
}

/**
* Package protected getter.
* @return the underlying protobuf struct
*/
@Nonnull
com.apple.foundationdb.relational.jdbc.grpc.v1.column.Array getUnderlying() {
return underlying;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.apple.foundationdb.relational.api.RelationalConnection;
import com.apple.foundationdb.relational.api.RelationalPreparedStatement;
import com.apple.foundationdb.relational.api.RelationalStatement;
import com.apple.foundationdb.relational.api.SqlTypeNamesSupport;
import com.apple.foundationdb.relational.api.exceptions.ErrorCode;
import com.apple.foundationdb.relational.jdbc.grpc.GrpcConstants;
import com.apple.foundationdb.relational.jdbc.grpc.v1.DatabaseMetaDataRequest;
Expand Down Expand Up @@ -228,8 +229,13 @@ public void clearWarnings() throws SQLException {

@Override
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
// TODO: Implement
return null;
int elementType = SqlTypeNamesSupport.getSqlTypeCode(typeName);
final com.apple.foundationdb.relational.jdbc.grpc.v1.column.Array.Builder builder = com.apple.foundationdb.relational.jdbc.grpc.v1.column.Array.newBuilder();
builder.setElementType(elementType);
for (Object element: elements) {
builder.addElement(TypeConversion.toColumn(elementType, element));
}
return new JDBCArrayImpl(builder.build());
}

@Override
Expand Down
Loading
Loading