diff --git a/src/main/java/org/opensearch/jdbc/ArrayImpl.java b/src/main/java/org/opensearch/jdbc/ArrayImpl.java new file mode 100644 index 0000000..555c618 --- /dev/null +++ b/src/main/java/org/opensearch/jdbc/ArrayImpl.java @@ -0,0 +1,118 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + + package org.opensearch.jdbc; + + import java.sql.JDBCType; + import java.sql.SQLException; + import java.sql.SQLFeatureNotSupportedException; + import java.sql.ResultSet; + import java.sql.Struct; + import java.sql.Array; + import java.util.Arrays; + import java.util.Map; + import java.util.List; + import java.util.ArrayList; + + + /** + * This class implements the {@link java.sql.Struct} interface. + *

+ * {@code StructImpl} provides a simple implementation of a struct data type. + *

+ */ + public class ArrayImpl implements Array { + private ArrayList arrayData; + private JDBCType baseTypeName; + + public ArrayImpl(ArrayList arrayData, JDBCType baseTypeName) { + this.arrayData = arrayData; + this.baseTypeName = baseTypeName; + } + + @Override + public String getBaseTypeName() throws SQLException { + return this.baseTypeName.toString(); + } + + @Override + public int getBaseType() throws SQLException { + throw new SQLFeatureNotSupportedException("getBaseType() is not supported"); + } + + @Override + public Object getArray() throws SQLException { + return arrayData.toArray(); + } + + @Override + public Object getArray(Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getArray(map) is not supported"); + } + + @Override + public Object getArray(long index, int count) throws SQLException { + if (index < 1 || index > arrayData.size() || index + count - 1 > arrayData.size() || count <= 0) { + throw new SQLException("Invalid index or count"); + } + int fromIndex = (int) index - 1; + int toIndex = fromIndex + count; + return arrayData.subList(fromIndex, toIndex).toArray(); + } + + @Override + public Object getArray(long index, int count, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getArray(index, count, map) is not supported"); + } + + @Override + public ResultSet getResultSet() throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet() is not supported"); + } + + @Override + public ResultSet getResultSet(Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet(map) is not supported"); + } + + @Override + public ResultSet getResultSet(long index, int count) throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet(index, count) is not supported"); + } + + @Override + public ResultSet getResultSet(long index, int count, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet(index, count, map) is not supported"); + } + + @Override + public void free() throws SQLException { + arrayData = null; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Array)) { + return false; + } + if (obj == this) { + return true; + } + Array other = (Array) obj; + try { + Object[] myArray = (Object[]) this.getArray(); + Object[] otherArray = (Object[]) this.getArray(); + + if (!(this.getBaseTypeName().equals(other.getBaseTypeName())) || myArray.length != otherArray.length) { + return false; + } + return Arrays.equals(myArray, otherArray); + } + catch (SQLException e) { + return false; + } + } +} diff --git a/src/main/java/org/opensearch/jdbc/ResultSetImpl.java b/src/main/java/org/opensearch/jdbc/ResultSetImpl.java index e34a42d..220a236 100644 --- a/src/main/java/org/opensearch/jdbc/ResultSetImpl.java +++ b/src/main/java/org/opensearch/jdbc/ResultSetImpl.java @@ -49,6 +49,7 @@ import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; +import java.sql.JDBCType; import java.util.Calendar; import java.util.HashMap; import java.util.List; @@ -579,10 +580,24 @@ protected T getObjectX(int columnIndex, Class javaClass) throws SQLExcept protected T getObjectX(int columnIndex, Class javaClass, Map conversionParams) throws SQLException { final Object value = getColumn(columnIndex); - final TypeConverter tc = TypeConverters.getInstance(getColumnMetaData(columnIndex).getOpenSearchType().getJdbcType()); + // Change made to identify if value is of type array since it isn't an official supported field type + + JDBCType trueJdbcType = getColumnMetaData(columnIndex).getOpenSearchType().getJdbcType(); + + if (javaClass == Array.class) { + trueJdbcType = JDBCType.ARRAY; + if (conversionParams == null) { + conversionParams = new HashMap(); + } + conversionParams.put("baseType", getColumnMetaData(columnIndex).getOpenSearchType().getJdbcType()); + } + + final TypeConverter tc = TypeConverters.getInstance(trueJdbcType); + if (null == tc) { throw new SQLException("Conversion from " + getColumnMetaData(columnIndex).getOpenSearchType() + " not supported."); } + return tc.convert(value, javaClass, conversionParams); } @@ -1007,7 +1022,10 @@ public Clob getClob(int columnIndex) throws SQLException { @Override public Array getArray(int columnIndex) throws SQLException { - throw new SQLFeatureNotSupportedException("Array is not supported"); + log.debug(() -> logEntry("getArray (%s, %s)", columnIndex, Array.class)); + Array value = getObjectX(columnIndex, Array.class); + log.debug(() -> logExit("getArray", value)); + return value; } @Override @@ -1050,7 +1068,10 @@ public Clob getClob(String columnLabel) throws SQLException { @Override public Array getArray(String columnLabel) throws SQLException { - throw new SQLFeatureNotSupportedException("Array is not supported"); + log.debug(() -> logEntry("getArray (%s, %s)", columnLabel, Array.class)); + Array value = getObjectX(getColumnIndex(columnLabel), Array.class); + log.debug(() -> logExit("getArray", value)); + return value; } @Override diff --git a/src/main/java/org/opensearch/jdbc/types/ArrayType.java b/src/main/java/org/opensearch/jdbc/types/ArrayType.java new file mode 100644 index 0000000..3cd67c5 --- /dev/null +++ b/src/main/java/org/opensearch/jdbc/types/ArrayType.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.jdbc.types; + +import java.sql.Array; +import java.sql.JDBCType; +import java.util.Map; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.LinkedHashMap; +import java.util.Collections; + + +import org.opensearch.jdbc.ArrayImpl; + +public class ArrayType implements TypeHelper { + + public static final ArrayType INSTANCE = new ArrayType(); + + private ArrayType() { + + } + + @Override + public String getTypeName() { + return "Array"; + } + + @Override + public Array fromValue(Object value, Map conversionParams) { + if (value == null || !(value instanceof ArrayList)) { + return null; + } + + JDBCType baseType = conversionParams != null ? (JDBCType) conversionParams.get("baseType") : JDBCType.OTHER; + + return new ArrayImpl((ArrayList) value, baseType); + } + } diff --git a/src/main/java/org/opensearch/jdbc/types/BaseTypeConverter.java b/src/main/java/org/opensearch/jdbc/types/BaseTypeConverter.java index 7e286ed..880d78d 100644 --- a/src/main/java/org/opensearch/jdbc/types/BaseTypeConverter.java +++ b/src/main/java/org/opensearch/jdbc/types/BaseTypeConverter.java @@ -6,6 +6,7 @@ package org.opensearch.jdbc.types; +import java.sql.Array; import java.sql.Date; import java.sql.SQLException; import java.sql.Struct; @@ -38,6 +39,7 @@ public abstract class BaseTypeConverter implements TypeConverter { typeHandlerMap.put(Struct.class, StructType.INSTANCE); + typeHandlerMap.put(Array.class, ArrayType.INSTANCE); } @Override diff --git a/src/main/java/org/opensearch/jdbc/types/OpenSearchType.java b/src/main/java/org/opensearch/jdbc/types/OpenSearchType.java index 36f6623..8c118e1 100644 --- a/src/main/java/org/opensearch/jdbc/types/OpenSearchType.java +++ b/src/main/java/org/opensearch/jdbc/types/OpenSearchType.java @@ -6,8 +6,10 @@ package org.opensearch.jdbc.types; +import java.sql.Array; import java.sql.Date; import java.sql.JDBCType; +import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; import java.util.HashMap; @@ -61,7 +63,7 @@ public enum OpenSearchType { STRING(JDBCType.VARCHAR, String.class, Integer.MAX_VALUE, 0, false), IP(JDBCType.VARCHAR, String.class, 15, 0, false), NESTED(JDBCType.STRUCT, null, 0, 0, false), - OBJECT(JDBCType.STRUCT, null, 0, 0, false), + OBJECT(JDBCType.STRUCT, Struct.class, 0, 0, false), DATE(JDBCType.DATE, Date.class, 10, 10, false), TIME(JDBCType.TIME, Time.class, 8, 8, false), DATETIME(JDBCType.TIMESTAMP, Timestamp.class, 29, 29, false), @@ -69,7 +71,9 @@ public enum OpenSearchType { BINARY(JDBCType.VARBINARY, String.class, Integer.MAX_VALUE, 0, false), NULL(JDBCType.NULL, null, 0, 0, false), UNDEFINED(JDBCType.NULL, null, 0, 0, false), - UNSUPPORTED(JDBCType.OTHER, null, 0, 0, false); + UNSUPPORTED(JDBCType.OTHER, null, 0, 0, false), + ARRAY(JDBCType.ARRAY, Array.class, 0, 0, false); + private static final Map jdbcTypeToOpenSearchTypeMap; @@ -91,6 +95,7 @@ public enum OpenSearchType { jdbcTypeToOpenSearchTypeMap.put(JDBCType.DATE, DATE); jdbcTypeToOpenSearchTypeMap.put(JDBCType.VARBINARY, BINARY); jdbcTypeToOpenSearchTypeMap.put(JDBCType.STRUCT, OBJECT); + jdbcTypeToOpenSearchTypeMap.put(JDBCType.ARRAY, ARRAY); } /** diff --git a/src/main/java/org/opensearch/jdbc/types/TypeConverters.java b/src/main/java/org/opensearch/jdbc/types/TypeConverters.java index 847e264..9825084 100644 --- a/src/main/java/org/opensearch/jdbc/types/TypeConverters.java +++ b/src/main/java/org/opensearch/jdbc/types/TypeConverters.java @@ -6,6 +6,7 @@ package org.opensearch.jdbc.types; +import java.sql.Array; import java.sql.Date; import java.sql.JDBCType; import java.sql.SQLException; @@ -58,8 +59,9 @@ public class TypeConverters { tcMap.put(JDBCType.NULL, new NullTypeConverter()); - // Adding Struct Support tcMap.put(JDBCType.STRUCT, new StructTypeConverter()); + + tcMap.put(JDBCType.ARRAY, new ArrayTypeConverter()); } public static TypeConverter getInstance(JDBCType jdbcType) { @@ -85,6 +87,25 @@ public Set getSupportedJavaClasses() { } } + public static class ArrayTypeConverter extends BaseTypeConverter { + + private static final Set supportedJavaClasses = Collections.singleton(Array.class); + + ArrayTypeConverter() { + + } + + @Override + public Class getDefaultJavaClass() { + return Array.class; + } + + @Override + public Set getSupportedJavaClasses() { + return supportedJavaClasses; + } + } + public static class TimestampTypeConverter extends BaseTypeConverter { private static final Set supportedJavaClasses = Collections.unmodifiableSet( diff --git a/src/test/java/org/opensearch/jdbc/ResultSetTests.java b/src/test/java/org/opensearch/jdbc/ResultSetTests.java index 60733db..1e90b64 100644 --- a/src/test/java/org/opensearch/jdbc/ResultSetTests.java +++ b/src/test/java/org/opensearch/jdbc/ResultSetTests.java @@ -13,6 +13,7 @@ import org.opensearch.jdbc.test.TestResources; import org.opensearch.jdbc.test.mocks.MockOpenSearch; import org.opensearch.jdbc.types.OpenSearchType; +import org.opensearch.jdbc.types.ArrayType; import org.opensearch.jdbc.types.StructType; import org.opensearch.jdbc.test.PerTestWireMockServerExtension; import org.opensearch.jdbc.test.WireMockServerHelpers; @@ -33,6 +34,8 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; +import java.util.Arrays; +import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -179,18 +182,28 @@ void testNullableFieldsQuery(WireMockServer mockServer) throws SQLException, IOE Statement st = con.createStatement(); ResultSet rs = assertDoesNotThrow(() -> st.executeQuery(queryMock.getSql())); - Map attributes = new HashMap() {{ + Map simpleAttributes = new HashMap() {{ put("attribute1", "value1"); put("attribute2", 2); put("attribute3", 15.0); }}; + Map simpleAttributes2 = new HashMap() {{ + put("attribute1", "value2"); + put("attribute2", 100); + put("attribute3", 75.5); + }}; + Map nestedAttributes = new HashMap() {{ - put("struct", attributes); + put("struct", simpleAttributes); put("string", "hello"); put("int", 1); }}; + ArrayList elements = new ArrayList( Arrays.asList("item1", "item2", "item3") ); + ArrayList elementsComplex = new ArrayList( Arrays.asList(simpleAttributes, simpleAttributes2)); + + assertNotNull(rs); MockResultSetMetaData mockResultSetMetaData = MockResultSetMetaData.builder() @@ -207,6 +220,7 @@ void testNullableFieldsQuery(WireMockServer mockServer) throws SQLException, IOE .column("testText", OpenSearchType.TEXT) .column("testDouble", OpenSearchType.DOUBLE) .column("testStruct", OpenSearchType.OBJECT) + .column("testArray", OpenSearchType.ARRAY) .build(); MockResultSetRows mockResultSetRows = MockResultSetRows.builder() @@ -223,7 +237,8 @@ void testNullableFieldsQuery(WireMockServer mockServer) throws SQLException, IOE .column("Test String", false) .column("document3", false) .column((double) 0, true) - .column(StructType.INSTANCE.fromValue(attributes, null), false) + .column(StructType.INSTANCE.fromValue(simpleAttributes, null), false) + .column(ArrayType.INSTANCE.fromValue(elements, null), false) .row() .column(true, false) .column("1", false) @@ -238,6 +253,7 @@ void testNullableFieldsQuery(WireMockServer mockServer) throws SQLException, IOE .column(null, true) .column((double) 22.312423148903218, false) .column(null, true) + .column(null, true) .row() .column(true, false) .column("1", false) @@ -252,6 +268,7 @@ void testNullableFieldsQuery(WireMockServer mockServer) throws SQLException, IOE .column(null, true) .column((double) 22.312423148903218, false) .column(StructType.INSTANCE.fromValue(nestedAttributes, null), false) + .column(ArrayType.INSTANCE.fromValue(elementsComplex, null), false) .build(); MockResultSet mockResultSet = new MockResultSet(mockResultSetMetaData, mockResultSetRows); diff --git a/src/test/resources/mock/protocol/json/queryresponse_nullablefields.json b/src/test/resources/mock/protocol/json/queryresponse_nullablefields.json index 932ab8d..57a60b1 100644 --- a/src/test/resources/mock/protocol/json/queryresponse_nullablefields.json +++ b/src/test/resources/mock/protocol/json/queryresponse_nullablefields.json @@ -51,9 +51,13 @@ { "name": "testStruct", "type": "object" + }, + { + "name": "testArray", + "type": "array" } ], - "total": 2, + "total": 3, "datarows": [ [ null, @@ -72,7 +76,8 @@ "attribute1": "value1", "attribute2": 2, "attribute3": 15.0 - } + }, + ["item1", "item2", "item3"] ], [ true, @@ -87,6 +92,7 @@ null, null, 22.312423148903218, + null, null ], [ @@ -106,9 +112,21 @@ "struct": {"attribute1": "value1", "attribute2": 2, "attribute3": 15.0}, "string": "hello", "int": 1 - } + }, + [ + { + "attribute1": "value1", + "attribute2": 2, + "attribute3": 15.0 + }, + { + "attribute1": "value2", + "attribute2": 100, + "attribute3": 75.5 + } + ] ] ], - "size": 2, + "size": 3, "status": 200 }