diff --git a/src/main/java/org/opensearch/jdbc/ConnectionImpl.java b/src/main/java/org/opensearch/jdbc/ConnectionImpl.java index e6a8587..6ad7e47 100644 --- a/src/main/java/org/opensearch/jdbc/ConnectionImpl.java +++ b/src/main/java/org/opensearch/jdbc/ConnectionImpl.java @@ -436,7 +436,7 @@ public Array createArrayOf(String typeName, Object[] elements) throws SQLExcepti @Override public Struct createStruct(String typeName, Object[] attributes) throws SQLException { - throw new SQLFeatureNotSupportedException("Struct is not supported."); + return new StructImpl(typeName, attributes); } @Override diff --git a/src/main/java/org/opensearch/jdbc/StructImpl.java b/src/main/java/org/opensearch/jdbc/StructImpl.java new file mode 100644 index 0000000..6ae2831 --- /dev/null +++ b/src/main/java/org/opensearch/jdbc/StructImpl.java @@ -0,0 +1,98 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.opensearch.jdbc; + +import java.sql.SQLException; +import java.sql.Struct; +import java.util.Arrays; +import java.util.Map; +import java.util.List; + + +/** + * This class implements the {@link java.sql.Struct} interface. + *

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

+ */ +public class StructImpl implements Struct { + private final String typeName; + private final Object[] attributes; + + /** + * Constructs a new {@code StructImpl} object with the specified parameter values. + * + * @param typeName the SQL type name of the struct + * @param attributes the attributes of the struct, each attribute is a {@code Map.Entry}(key-value pair) + */ + public StructImpl(String typeName, Object[] attributes) { + this.typeName = typeName; + this.attributes = attributes; + } + + /** + * Returns the SQL type name of the struct. + * + * @return the SQL type name of the struct + * @throws SQLException if a database access error occurs + */ + @Override + public String getSQLTypeName() throws SQLException { + return this.typeName; + } + + /** + * Returns an array containing the attributes of the struct. + * + * @return an array containing the attribute values of the struct + * @throws SQLException if a database access error occurs + */ + @Override + public Object[] getAttributes() throws SQLException { + return attributes; + } + + /** + * @throws java.lang.UnsupportedOperationException because functionality is not supported yet + */ + @Override + public Object[] getAttributes(Map> map) throws SQLException { + throw new java.lang.UnsupportedOperationException("Not supported yet."); + } + + /** + * Compares this StructImpl object with the specified object for equality. + * + *

+ * Two StructImpl objects are considered equal if they have the same typeName, same number of attributes, + * and contain the same attributes. + *

+ * + * @param obj the object to compare with this StructImpl object for equality. + * @return {@code true} if the specified object is equal to this StructImpl object, {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Struct)) { + return false; + } + if (obj == this) { + return true; + } + Struct other = (Struct) obj; + try { + if (!typeName.equals(other.getSQLTypeName()) || attributes.length != other.getAttributes().length) { + return false; + } + List otherAttributes = Arrays.asList(other.getAttributes()); + return otherAttributes.containsAll(Arrays.asList(attributes)); + } + catch (SQLException e) { + return false; + } + } +} diff --git a/src/main/java/org/opensearch/jdbc/types/BaseTypeConverter.java b/src/main/java/org/opensearch/jdbc/types/BaseTypeConverter.java index bbc114a..7e286ed 100644 --- a/src/main/java/org/opensearch/jdbc/types/BaseTypeConverter.java +++ b/src/main/java/org/opensearch/jdbc/types/BaseTypeConverter.java @@ -8,6 +8,7 @@ import java.sql.Date; import java.sql.SQLException; +import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; import java.util.HashMap; @@ -35,6 +36,8 @@ public abstract class BaseTypeConverter implements TypeConverter { typeHandlerMap.put(Date.class, DateType.INSTANCE); typeHandlerMap.put(Time.class, TimeType.INSTANCE); + typeHandlerMap.put(Struct.class, StructType.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 e7329a7..36f6623 100644 --- a/src/main/java/org/opensearch/jdbc/types/OpenSearchType.java +++ b/src/main/java/org/opensearch/jdbc/types/OpenSearchType.java @@ -90,6 +90,7 @@ public enum OpenSearchType { jdbcTypeToOpenSearchTypeMap.put(JDBCType.TIME, TIME); jdbcTypeToOpenSearchTypeMap.put(JDBCType.DATE, DATE); jdbcTypeToOpenSearchTypeMap.put(JDBCType.VARBINARY, BINARY); + jdbcTypeToOpenSearchTypeMap.put(JDBCType.STRUCT, OBJECT); } /** diff --git a/src/main/java/org/opensearch/jdbc/types/StructType.java b/src/main/java/org/opensearch/jdbc/types/StructType.java new file mode 100644 index 0000000..faee44b --- /dev/null +++ b/src/main/java/org/opensearch/jdbc/types/StructType.java @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.jdbc.types; + +import java.sql.Struct; +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.StructImpl; + +public class StructType implements TypeHelper { + + public static final StructType INSTANCE = new StructType(); + + private StructType() { + + } + + @Override + public String getTypeName() { + return "Struct"; + } + + @Override + public Struct fromValue(Object value, Map conversionParams) { + if (value == null || !(value instanceof Map)) { + return null; + } + Map structKeyValues = (Map) value; + return new StructImpl(getTypeName(), structKeyValues.entrySet().toArray()); + } + } diff --git a/src/main/java/org/opensearch/jdbc/types/TypeConverters.java b/src/main/java/org/opensearch/jdbc/types/TypeConverters.java index 189e970..847e264 100644 --- a/src/main/java/org/opensearch/jdbc/types/TypeConverters.java +++ b/src/main/java/org/opensearch/jdbc/types/TypeConverters.java @@ -9,6 +9,7 @@ import java.sql.Date; import java.sql.JDBCType; import java.sql.SQLException; +import java.sql.Struct; import java.sql.Time; import java.sql.Timestamp; import java.util.Arrays; @@ -56,12 +57,34 @@ public class TypeConverters { tcMap.put(JDBCType.BINARY, new BinaryTypeConverter()); tcMap.put(JDBCType.NULL, new NullTypeConverter()); + + // Adding Struct Support + tcMap.put(JDBCType.STRUCT, new StructTypeConverter()); } public static TypeConverter getInstance(JDBCType jdbcType) { return tcMap.get(jdbcType); } + public static class StructTypeConverter extends BaseTypeConverter { + + private static final Set supportedJavaClasses = Collections.singleton(Struct.class); + + StructTypeConverter() { + + } + + @Override + public Class getDefaultJavaClass() { + return Struct.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 0ef1753..60733db 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.StructType; import org.opensearch.jdbc.test.PerTestWireMockServerExtension; import org.opensearch.jdbc.test.WireMockServerHelpers; import org.opensearch.jdbc.test.mocks.MockResultSet; @@ -32,6 +33,8 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; import java.util.stream.Stream; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; @@ -176,6 +179,18 @@ void testNullableFieldsQuery(WireMockServer mockServer) throws SQLException, IOE Statement st = con.createStatement(); ResultSet rs = assertDoesNotThrow(() -> st.executeQuery(queryMock.getSql())); + Map attributes = new HashMap() {{ + put("attribute1", "value1"); + put("attribute2", 2); + put("attribute3", 15.0); + }}; + + Map nestedAttributes = new HashMap() {{ + put("struct", attributes); + put("string", "hello"); + put("int", 1); + }}; + assertNotNull(rs); MockResultSetMetaData mockResultSetMetaData = MockResultSetMetaData.builder() @@ -191,6 +206,7 @@ void testNullableFieldsQuery(WireMockServer mockServer) throws SQLException, IOE .column("testKeyword", OpenSearchType.KEYWORD) .column("testText", OpenSearchType.TEXT) .column("testDouble", OpenSearchType.DOUBLE) + .column("testStruct", OpenSearchType.OBJECT) .build(); MockResultSetRows mockResultSetRows = MockResultSetRows.builder() @@ -207,6 +223,21 @@ 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) + .row() + .column(true, false) + .column("1", false) + .column((byte) 126, false) + .column((float) 0, true) + .column((long) 32000320003200030L, false) + .column((short) 29000, false) + .column((float) 0, true) + .column(null, true) + .column((double) 0, true) + .column(null, true) + .column(null, true) + .column((double) 22.312423148903218, false) + .column(null, true) .row() .column(true, false) .column("1", false) @@ -220,6 +251,7 @@ void testNullableFieldsQuery(WireMockServer mockServer) throws SQLException, IOE .column(null, true) .column(null, true) .column((double) 22.312423148903218, false) + .column(StructType.INSTANCE.fromValue(nestedAttributes, null), false) .build(); MockResultSet mockResultSet = new MockResultSet(mockResultSetMetaData, mockResultSetRows); diff --git a/src/test/java/org/opensearch/jdbc/types/StructTypeTests.java b/src/test/java/org/opensearch/jdbc/types/StructTypeTests.java new file mode 100644 index 0000000..6280358 --- /dev/null +++ b/src/test/java/org/opensearch/jdbc/types/StructTypeTests.java @@ -0,0 +1,57 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + + package org.opensearch.jdbc.types; + + import static org.junit.jupiter.api.Assertions.*; + import org.junit.jupiter.api.Test; + import org.opensearch.jdbc.types.StructType; + import org.opensearch.jdbc.StructImpl; + + import java.sql.Struct; + import java.sql.SQLException; + import java.util.Arrays; + import java.util.Map; + import java.util.HashMap; + + public class StructTypeTests { + + @Test + public void testStructTypeFromValue() throws SQLException { + Map attributes = new HashMap() {{ + put("attribute1", "value1"); + put("attribute2", 2); + put("attribute3", 15.0); + }}; + + Map attributes2 = new HashMap() {{ + put("attribute1", "value1"); + put("attribute2", 2); + put("attribute3", 15.0); + put("attribute4", "value4"); + }}; + + Struct actualStruct = StructType.INSTANCE.fromValue(attributes, null); + Struct actualStruct2 = StructType.INSTANCE.fromValue(attributes2, null); + + assertTrue(Arrays.equals(actualStruct.getAttributes(), attributes.entrySet().toArray())); + assertEquals(actualStruct.getAttributes().length, 3); + assertEquals(actualStruct2.getAttributes().length, 4); + assertEquals(actualStruct, new StructImpl(StructType.INSTANCE.getTypeName(), attributes.entrySet().toArray())); + assertNotEquals(actualStruct, actualStruct2); + + Map nestedAttributes = new HashMap() {{ + put("struct", attributes); + put("string", "hello"); + put("int", 1); + }}; + + Struct actualNestedStruct = StructType.INSTANCE.fromValue(nestedAttributes, null); + assertTrue(Arrays.equals(actualNestedStruct.getAttributes(), nestedAttributes.entrySet().toArray())); + assertEquals(actualNestedStruct, new StructImpl(StructType.INSTANCE.getTypeName(), nestedAttributes.entrySet().toArray())); + assertNotEquals(actualStruct, actualNestedStruct); + } + } diff --git a/src/test/resources/mock/protocol/json/queryresponse_nullablefields.json b/src/test/resources/mock/protocol/json/queryresponse_nullablefields.json index 1068e6c..932ab8d 100644 --- a/src/test/resources/mock/protocol/json/queryresponse_nullablefields.json +++ b/src/test/resources/mock/protocol/json/queryresponse_nullablefields.json @@ -47,6 +47,10 @@ { "name": "testDouble", "type": "double" + }, + { + "name": "testStruct", + "type": "object" } ], "total": 2, @@ -63,6 +67,26 @@ 24.324234543532153, "Test String", "document3", + null, + { + "attribute1": "value1", + "attribute2": 2, + "attribute3": 15.0 + } + ], + [ + true, + "1", + 126, + null, + 32000320003200030, + 29000, + null, + null, + null, + null, + null, + 22.312423148903218, null ], [ @@ -77,9 +101,14 @@ null, null, null, - 22.312423148903218 + 22.312423148903218, + { + "struct": {"attribute1": "value1", "attribute2": 2, "attribute3": 15.0}, + "string": "hello", + "int": 1 + } ] ], "size": 2, "status": 200 -} \ No newline at end of file +}