diff --git a/TestOnly/pom.xml b/TestOnly/pom.xml index 3188b89de..d8a594471 100644 --- a/TestOnly/pom.xml +++ b/TestOnly/pom.xml @@ -345,6 +345,8 @@ **/ResultSetArrow*.java **/PreparedStatementArrow*IT.java **/SFArrowResultSetIT.java + + **/structuredtypes/sqldata/*.java **/*LatestIT.java diff --git a/src/main/java/net/snowflake/client/core/ArrowSqlInput.java b/src/main/java/net/snowflake/client/core/ArrowSqlInput.java index 99ac133dc..61cf39674 100644 --- a/src/main/java/net/snowflake/client/core/ArrowSqlInput.java +++ b/src/main/java/net/snowflake/client/core/ArrowSqlInput.java @@ -14,6 +14,8 @@ import java.sql.SQLInput; import java.sql.Time; import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; @@ -21,6 +23,7 @@ import net.snowflake.client.core.structs.SQLDataCreationHelper; import net.snowflake.client.jdbc.FieldMetadata; import net.snowflake.client.util.ThrowingBiFunction; +import org.apache.arrow.vector.util.JsonStringArrayList; import org.apache.arrow.vector.util.JsonStringHashMap; @SnowflakeJdbcInternalApi @@ -45,27 +48,12 @@ public Map getInput() { @Override public String readString() throws SQLException { - return withNextValue( - ((value, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - int columnSubType = fieldMetadata.getType(); - int scale = fieldMetadata.getScale(); - return mapSFExceptionToSQLException( - () -> - converters - .getStringConverter() - .getString(value, columnType, columnSubType, scale)); - })); + return withNextValue((this::convertString)); } @Override public boolean readBoolean() throws SQLException { - return withNextValue( - (value, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getBooleanConverter().getBoolean(value, columnType)); - }); + return withNextValue(this::convertBoolean); } @Override @@ -77,77 +65,37 @@ public byte readByte() throws SQLException { @Override public short readShort() throws SQLException { - return withNextValue( - (value, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getShort(value, columnType)); - }); + return withNextValue(this::convertShort); } @Override public int readInt() throws SQLException { - return withNextValue( - (value, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getInt(value, columnType)); - }); + return withNextValue(this::convertInt); } @Override public long readLong() throws SQLException { - return withNextValue( - (value, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getLong(value, columnType)); - }); + return withNextValue(this::convertLong); } @Override public float readFloat() throws SQLException { - return withNextValue( - (value, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getFloat(value, columnType)); - }); + return withNextValue(this::convertFloat); } @Override public double readDouble() throws SQLException { - return withNextValue( - (value, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getDouble(value, columnType)); - }); + return withNextValue(this::convertDouble); } @Override public BigDecimal readBigDecimal() throws SQLException { - return withNextValue((value, fieldMetadata) -> convertToBigDecimal(value, fieldMetadata)); - } - - private BigDecimal convertToBigDecimal(Object value, FieldMetadata fieldMetadata) - throws SQLException { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getBigDecimal(value, columnType)); + return withNextValue(this::convertBigDecimal); } @Override public byte[] readBytes() throws SQLException { - return withNextValue((value, fieldMetadata) -> converToBytes(value, fieldMetadata)); - } - - private byte[] converToBytes(Object value, FieldMetadata fieldMetadata) throws SQLException { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - int columnSubType = fieldMetadata.getType(); - int scale = fieldMetadata.getScale(); - return mapSFExceptionToSQLException( - () -> converters.getBytesConverter().getBytes(value, columnType, columnSubType, scale)); + return withNextValue(this::convertBytes); } @Override @@ -157,11 +105,11 @@ public Date readDate() throws SQLException { if (value == null) { return null; } - return formatDate((int) value); + return convertDate((int) value); }); } - private Date formatDate(int value) throws SQLException { + private Date convertDate(int value) throws SQLException { return mapSFExceptionToSQLException( () -> converters.getStructuredTypeDateTimeConverter().getDate(value, TimeZone.getDefault())); @@ -174,11 +122,11 @@ public Time readTime() throws SQLException { if (value == null) { return null; } - return formatTime((long) value, fieldMetadata); + return convertTime((long) value, fieldMetadata); }); } - private Time formatTime(long value, FieldMetadata fieldMetadata) throws SQLException { + private Time convertTime(long value, FieldMetadata fieldMetadata) throws SQLException { return mapSFExceptionToSQLException( () -> { int scale = fieldMetadata.getScale(); @@ -188,10 +136,10 @@ private Time formatTime(long value, FieldMetadata fieldMetadata) throws SQLExcep @Override public Timestamp readTimestamp(TimeZone tz) throws SQLException { - return withNextValue((value, fieldMetadata) -> formatTimestamp(tz, value, fieldMetadata)); + return withNextValue((value, fieldMetadata) -> convertTimestamp(tz, value, fieldMetadata)); } - private Timestamp formatTimestamp(TimeZone tz, Object value, FieldMetadata fieldMetadata) + private Timestamp convertTimestamp(TimeZone tz, Object value, FieldMetadata fieldMetadata) throws SQLException { if (value == null) { return null; @@ -220,54 +168,117 @@ public Object readObject() throws SQLException { @Override public T readObject(Class type) throws SQLException { + return readObject(type, TimeZone.getDefault()); + } + + @Override + public T readObject(Class type, TimeZone tz) throws SQLException { + return withNextValue((value, fieldMetadata) -> convertObject(type, tz, value, fieldMetadata)); + } + + private T convertObject(Class type, TimeZone tz, Object value, FieldMetadata fieldMetadata) + throws SQLException { + if (value == null) { + return null; + } else if (SQLData.class.isAssignableFrom(type)) { + ArrowSqlInput sqlInput = + new ArrowSqlInput( + (Map) value, session, converters, fieldMetadata.getFields()); + SQLData instance = (SQLData) SQLDataCreationHelper.create(type); + instance.readSQL(sqlInput, null); + return (T) instance; + } else if (Map.class.isAssignableFrom(type)) { + return (T) convertSqlInputToMap((SQLInput) value); + } else if (String.class.isAssignableFrom(type)) { + return (T) convertString(value, fieldMetadata); + } else if (Boolean.class.isAssignableFrom(type)) { + return (T) convertBoolean(value, fieldMetadata); + } else if (Byte.class.isAssignableFrom(type)) { + return (T) convertBytes(value, fieldMetadata); + } else if (Short.class.isAssignableFrom(type)) { + return (T) convertShort(value, fieldMetadata); + } else if (Integer.class.isAssignableFrom(type)) { + return (T) convertInt(value, fieldMetadata); + } else if (Long.class.isAssignableFrom(type)) { + return (T) convertLong(value, fieldMetadata); + } else if (Float.class.isAssignableFrom(type)) { + return (T) convertFloat(value, fieldMetadata); + } else if (Double.class.isAssignableFrom(type)) { + return (T) convertDouble(value, fieldMetadata); + } else if (Date.class.isAssignableFrom(type)) { + return (T) convertDate((int) value); + } else if (Time.class.isAssignableFrom(type)) { + return (T) convertTime((long) value, fieldMetadata); + } else if (Timestamp.class.isAssignableFrom(type)) { + return (T) convertTimestamp(tz, value, fieldMetadata); + } else if (BigDecimal.class.isAssignableFrom(type)) { + return (T) convertBigDecimal(value, fieldMetadata); + } else if (byte[].class.isAssignableFrom(type)) { + return (T) convertBytes(value, fieldMetadata); + } else { + logger.debug( + "Unsupported type passed to readObject(int columnIndex,Class type): " + + type.getName()); + throw new SQLException( + "Type passed to 'getObject(int columnIndex,Class type)' is unsupported. Type: " + + type.getName()); + } + } + + @Override + public List readList(Class type) throws SQLException { return withNextValue( (value, fieldMetadata) -> { - if (SQLData.class.isAssignableFrom(type)) { - if (value == null) { - return null; - } else { - ArrowSqlInput sqlInput = - new ArrowSqlInput( - (Map) value, session, converters, fieldMetadata.getFields()); - SQLData instance = (SQLData) SQLDataCreationHelper.create(type); - instance.readSQL(sqlInput, null); - return (T) instance; - } - } else if (value == null) { + if (value == null) { return null; - } else if (Map.class.isAssignableFrom(type)) { - if (value == null) { - return null; - } else { - return (T) convertSqlInputToMap((SQLInput) value); - } - } else if (String.class.isAssignableFrom(type) - || Boolean.class.isAssignableFrom(type) - || Byte.class.isAssignableFrom(type) - || Short.class.isAssignableFrom(type) - || Integer.class.isAssignableFrom(type) - || Long.class.isAssignableFrom(type) - || Float.class.isAssignableFrom(type) - || Double.class.isAssignableFrom(type)) { - return (T) value; - } else if (Date.class.isAssignableFrom(type)) { - return (T) formatDate((int) value); - } else if (Time.class.isAssignableFrom(type)) { - return (T) formatTime((long) value, fieldMetadata); - } else if (Timestamp.class.isAssignableFrom(type)) { - return (T) formatTimestamp(TimeZone.getDefault(), value, fieldMetadata); - } else if (BigDecimal.class.isAssignableFrom(type)) { - return (T) convertToBigDecimal(value, fieldMetadata); - } else if (byte[].class.isAssignableFrom(type)) { - return (T) converToBytes(value, fieldMetadata); - } else { - logger.debug( - "Unsupported type passed to readObject(int columnIndex,Class type): " - + type.getName()); - throw new SQLException( - "Type passed to 'getObject(int columnIndex,Class type)' is unsupported. Type: " - + type.getName()); } + List result = new ArrayList(); + JsonStringArrayList maps = (JsonStringArrayList) value; + for (Object ob : maps) { + result.add( + convertObject(type, TimeZone.getDefault(), ob, fieldMetadata.getFields().get(0))); + } + return result; + }); + } + + @Override + public T[] readArray(Class type) throws SQLException { + return withNextValue( + (value, fieldMetadata) -> { + if (value == null) { + return null; + } + JsonStringArrayList internalValues = (JsonStringArrayList) value; + T[] array = (T[]) java.lang.reflect.Array.newInstance(type, internalValues.size()); + int counter = 0; + for (Object ob : internalValues) { + array[counter++] = + convertObject(type, TimeZone.getDefault(), ob, fieldMetadata.getFields().get(0)); + } + return array; + }); + } + + @Override + public Map readMap(Class type) throws SQLException { + return withNextValue( + (value, fieldMetadata) -> { + if (value == null) { + return null; + } + Map result = new HashMap(); + JsonStringArrayList maps = (JsonStringArrayList) value; + for (Map map : maps) { + result.put( + map.get("key").toString(), + convertObject( + type, + TimeZone.getDefault(), + map.get("value"), + fieldMetadata.getFields().get(1))); + } + return result; }); } diff --git a/src/main/java/net/snowflake/client/core/BaseSqlInput.java b/src/main/java/net/snowflake/client/core/BaseSqlInput.java index 85f0ca7ee..d622d9bcd 100644 --- a/src/main/java/net/snowflake/client/core/BaseSqlInput.java +++ b/src/main/java/net/snowflake/client/core/BaseSqlInput.java @@ -4,8 +4,11 @@ package net.snowflake.client.core; +import static net.snowflake.client.jdbc.SnowflakeUtil.mapSFExceptionToSQLException; + import java.io.InputStream; import java.io.Reader; +import java.math.BigDecimal; import java.net.URL; import java.sql.Array; import java.sql.Blob; @@ -107,4 +110,63 @@ public RowId readRowId() throws SQLException { } abstract Map convertSqlInputToMap(SQLInput sqlInput); + + protected String convertString(Object value, FieldMetadata fieldMetadata) throws SQLException { + int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); + int columnSubType = fieldMetadata.getType(); + int scale = fieldMetadata.getScale(); + return mapSFExceptionToSQLException( + () -> converters.getStringConverter().getString(value, columnType, columnSubType, scale)); + } + + protected Boolean convertBoolean(Object value, FieldMetadata fieldMetadata) throws SQLException { + int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); + return mapSFExceptionToSQLException( + () -> converters.getBooleanConverter().getBoolean(value, columnType)); + } + + protected Short convertShort(Object value, FieldMetadata fieldMetadata) throws SQLException { + int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); + return mapSFExceptionToSQLException( + () -> converters.getNumberConverter().getShort(value, columnType)); + } + + protected Integer convertInt(Object value, FieldMetadata fieldMetadata) throws SQLException { + int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); + return mapSFExceptionToSQLException( + () -> converters.getNumberConverter().getInt(value, columnType)); + } + + protected Long convertLong(Object value, FieldMetadata fieldMetadata) throws SQLException { + int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); + return mapSFExceptionToSQLException( + () -> converters.getNumberConverter().getLong(value, columnType)); + } + + protected Float convertFloat(Object value, FieldMetadata fieldMetadata) throws SQLException { + int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); + return mapSFExceptionToSQLException( + () -> converters.getNumberConverter().getFloat(value, columnType)); + } + + protected Double convertDouble(Object value, FieldMetadata fieldMetadata) throws SQLException { + int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); + return mapSFExceptionToSQLException( + () -> converters.getNumberConverter().getDouble(value, columnType)); + } + + protected BigDecimal convertBigDecimal(Object value, FieldMetadata fieldMetadata) + throws SQLException { + int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); + return mapSFExceptionToSQLException( + () -> converters.getNumberConverter().getBigDecimal(value, columnType)); + } + + protected byte[] convertBytes(Object value, FieldMetadata fieldMetadata) throws SQLException { + int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); + int columnSubType = fieldMetadata.getType(); + int scale = fieldMetadata.getScale(); + return mapSFExceptionToSQLException( + () -> converters.getBytesConverter().getBytes(value, columnType, columnSubType, scale)); + } } diff --git a/src/main/java/net/snowflake/client/core/JsonSqlInput.java b/src/main/java/net/snowflake/client/core/JsonSqlInput.java index 0ff641321..6b0e6e34e 100644 --- a/src/main/java/net/snowflake/client/core/JsonSqlInput.java +++ b/src/main/java/net/snowflake/client/core/JsonSqlInput.java @@ -9,6 +9,8 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.math.BigDecimal; import java.sql.Date; import java.sql.SQLData; @@ -18,6 +20,8 @@ import java.sql.Timestamp; import java.time.Instant; import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -25,7 +29,7 @@ import net.snowflake.client.core.json.Converters; import net.snowflake.client.core.structs.SQLDataCreationHelper; import net.snowflake.client.jdbc.FieldMetadata; -import net.snowflake.client.util.ThrowingTriFunction; +import net.snowflake.client.util.ThrowingBiFunction; import net.snowflake.common.core.SFTimestamp; import net.snowflake.common.core.SnowflakeDateTimeFormat; @@ -55,124 +59,68 @@ public JsonNode getInput() { @Override public String readString() throws SQLException { - return withNextValue( - ((value, jsonNode, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - int columnSubType = fieldMetadata.getType(); - int scale = fieldMetadata.getScale(); - return mapSFExceptionToSQLException( - () -> - converters - .getStringConverter() - .getString(value, columnType, columnSubType, scale)); - })); + return withNextValue((this::convertString)); } @Override public boolean readBoolean() throws SQLException { - return withNextValue( - (value, jsonNode, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getBooleanConverter().getBoolean(value, columnType)); - }); + return withNextValue(this::convertBoolean); } @Override public byte readByte() throws SQLException { return withNextValue( - (value, jsonNode, fieldMetadata) -> + (value, fieldMetadata) -> mapSFExceptionToSQLException(() -> converters.getNumberConverter().getByte(value))); } @Override public short readShort() throws SQLException { - return withNextValue( - (value, jsonNode, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getShort(value, columnType)); - }); + return withNextValue(this::convertShort); } @Override public int readInt() throws SQLException { - return withNextValue( - (value, jsonNode, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getInt(value, columnType)); - }); + return withNextValue(this::convertInt); } @Override public long readLong() throws SQLException { - return withNextValue( - (value, jsonNode, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getLong(value, columnType)); - }); + return withNextValue(this::convertLong); } @Override public float readFloat() throws SQLException { - return withNextValue( - (value, jsonNode, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getFloat(value, columnType)); - }); + return withNextValue(this::convertFloat); } @Override public double readDouble() throws SQLException { - return withNextValue( - (value, jsonNode, fieldMetadata) -> { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getDouble(value, columnType)); - }); + return withNextValue(this::convertDouble); } @Override public BigDecimal readBigDecimal() throws SQLException { - return withNextValue( - (value, jsonNode, fieldMetadata) -> convertBigDecimal(value, fieldMetadata)); - } - - private BigDecimal convertBigDecimal(Object value, FieldMetadata fieldMetadata) - throws SQLException { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - return mapSFExceptionToSQLException( - () -> converters.getNumberConverter().getBigDecimal(value, columnType)); + return withNextValue(this::convertBigDecimal); } @Override public byte[] readBytes() throws SQLException { - return withNextValue((value, jsonNode, fieldMetadata) -> convertToBytes(value, fieldMetadata)); - } - - private byte[] convertToBytes(Object value, FieldMetadata fieldMetadata) throws SQLException { - int columnType = ColumnTypeHelper.getColumnType(fieldMetadata.getType(), session); - int columnSubType = fieldMetadata.getType(); - int scale = fieldMetadata.getScale(); - return mapSFExceptionToSQLException( - () -> converters.getBytesConverter().getBytes(value, columnType, columnSubType, scale)); + return withNextValue(this::convertBytes); } @Override public Date readDate() throws SQLException { return withNextValue( - (value, jsonNode, fieldMetadata) -> { + (value, fieldMetadata) -> { if (value == null) { return null; } - return formatDate((String) value); + return convertDate((String) value); }); } - private Date formatDate(String value) { + private Date convertDate(String value) { SnowflakeDateTimeFormat formatter = getFormat(session, "DATE_OUTPUT_FORMAT"); SFTimestamp timestamp = formatter.parse(value); return Date.valueOf( @@ -182,15 +130,15 @@ private Date formatDate(String value) { @Override public Time readTime() throws SQLException { return withNextValue( - (value, jsonNode, fieldMetadata) -> { + (value, fieldMetadata) -> { if (value == null) { return null; } - return formatTime((String) value); + return convertTime((String) value); }); } - private Time formatTime(String value) { + private Time convertTime(String value) { SnowflakeDateTimeFormat formatter = getFormat(session, "TIME_OUTPUT_FORMAT"); SFTimestamp timestamp = formatter.parse(value); return Time.valueOf( @@ -205,68 +153,151 @@ public Timestamp readTimestamp() throws SQLException { @Override public Timestamp readTimestamp(TimeZone tz) throws SQLException { return withNextValue( - (value, jsonNode, fieldMetadata) -> { + (value, fieldMetadata) -> { if (value == null) { return null; } - return formatTimestamp(tz, value, fieldMetadata); + return convertTimestamp(tz, value, fieldMetadata); }); } @Override - public T readObject(Class type) throws SQLException { + public T readObject(Class type, TimeZone tz) throws SQLException { + return withNextValue((value, fieldMetadata) -> convertObject(type, tz, value, fieldMetadata)); + } + + private T convertObject(Class type, TimeZone tz, Object value, FieldMetadata fieldMetadata) + throws SQLException { + if (value == null) { + return null; + } else if (SQLData.class.isAssignableFrom(type)) { + if (!JsonNode.class.isAssignableFrom(value.getClass())) { + logger.error("Object of class JsonNode is expected to convert to SqlData"); + return null; + } + JsonNode jsonNode = (JsonNode) value; + SQLInput sqlInput = + new JsonSqlInput( + jsonNode, session, converters, fieldMetadata.getFields(), sessionTimeZone); + SQLData instance = (SQLData) SQLDataCreationHelper.create(type); + instance.readSQL(sqlInput, null); + return (T) instance; + } else if (Map.class.isAssignableFrom(type)) { + if (value == null) { + return null; + } else { + return (T) convertSqlInputToMap((SQLInput) value); + } + } else if (String.class.isAssignableFrom(type)) { + return (T) convertString(value, fieldMetadata); + } else if (Boolean.class.isAssignableFrom(type)) { + return (T) convertBoolean(value, fieldMetadata); + } else if (Byte.class.isAssignableFrom(type)) { + return (T) convertString(value, fieldMetadata); + } else if (Short.class.isAssignableFrom(type)) { + return (T) convertShort(value, fieldMetadata); + } else if (Integer.class.isAssignableFrom(type)) { + return (T) convertInt(value, fieldMetadata); + } else if (Long.class.isAssignableFrom(type)) { + return (T) convertLong(value, fieldMetadata); + } else if (Float.class.isAssignableFrom(type)) { + return (T) convertFloat(value, fieldMetadata); + } else if (Double.class.isAssignableFrom(type)) { + return (T) convertFloat(value, fieldMetadata); + } else if (Date.class.isAssignableFrom(type)) { + return (T) convertDate((String) value); + } else if (Time.class.isAssignableFrom(type)) { + return (T) convertTime((String) value); + } else if (Timestamp.class.isAssignableFrom(type)) { + return (T) convertTimestamp(tz, value, fieldMetadata); + } else if (BigDecimal.class.isAssignableFrom(type)) { + return (T) convertBigDecimal(value, fieldMetadata); + } else if (byte[].class.isAssignableFrom(type)) { + return (T) convertBytes(value, fieldMetadata); + } else { + logger.debug( + "Unsupported type passed to readObject(int columnIndex,Class type): " + + type.getName()); + throw new SQLException( + "Type passed to 'getObject(int columnIndex,Class type)' is unsupported. Type: " + + type.getName()); + } + } + + @Override + public List readList(Class type) throws SQLException { return withNextValue( - (value, jsonNode, fieldMetadata) -> { - if (SQLData.class.isAssignableFrom(type)) { - if (jsonNode.isNull()) { - return null; - } else { - SQLInput sqlInput = - new JsonSqlInput( - jsonNode, session, converters, fieldMetadata.getFields(), sessionTimeZone); - SQLData instance = (SQLData) SQLDataCreationHelper.create(type); - instance.readSQL(sqlInput, null); - return (T) instance; + (value, fieldMetadata) -> { + if (value == null) { + return null; + } + List result = new ArrayList(); + if (ArrayNode.class.isAssignableFrom(value.getClass())) { + for (JsonNode node : (ArrayNode) value) { + + result.add(convertObject(type, TimeZone.getDefault(), getValue(node), fieldMetadata)); } - } else if (Map.class.isAssignableFrom(type)) { - if (value == null) { - return null; - } else { - return (T) convertSqlInputToMap((SQLInput) value); + return result; + } else { + logger.debug("Given object could not be converted to List of type: " + type.getName()); + throw new SQLException( + "Given object could not be converted to List of type: " + type.getName()); + } + }); + } + + @Override + public T[] readArray(Class type) throws SQLException { + return withNextValue( + (value, fieldMetadata) -> { + if (value == null) { + return null; + } + if (ArrayNode.class.isAssignableFrom(value.getClass())) { + ArrayNode valueNodes = (ArrayNode) value; + T[] array = (T[]) java.lang.reflect.Array.newInstance(type, valueNodes.size()); + int counter = 0; + for (JsonNode node : valueNodes) { + array[counter++] = + convertObject(type, TimeZone.getDefault(), getValue(node), fieldMetadata); } - } else if (value == null) { + return array; + } else { + logger.debug("Given object could not be converted to Array of type: " + type.getName()); + throw new SQLException( + "Given object could not be converted to List of type: " + type.getName()); + } + }); + } + + @Override + public Map readMap(Class type) throws SQLException { + return withNextValue( + (value, fieldMetadata) -> { + if (value == null) { return null; - } else if (String.class.isAssignableFrom(type) - || Boolean.class.isAssignableFrom(type) - || Byte.class.isAssignableFrom(type) - || Short.class.isAssignableFrom(type) - || Integer.class.isAssignableFrom(type) - || Long.class.isAssignableFrom(type) - || Float.class.isAssignableFrom(type) - || Double.class.isAssignableFrom(type)) { - return (T) value; - } else if (Date.class.isAssignableFrom(type)) { - return (T) formatDate((String) value); - } else if (Time.class.isAssignableFrom(type)) { - return (T) formatTime((String) value); - } else if (Timestamp.class.isAssignableFrom(type)) { - return (T) formatTimestamp(sessionTimeZone, value, fieldMetadata); - } else if (Byte[].class.isAssignableFrom(type)) { - return (T) convertToBytes(value, fieldMetadata); - } else if (BigDecimal.class.isAssignableFrom(type)) { - return (T) convertBigDecimal(value, fieldMetadata); + } + if (ObjectNode.class.isAssignableFrom(value.getClass())) { + Map result = new HashMap<>(); + ObjectNode arrayNode = (ObjectNode) value; + for (Iterator it = arrayNode.fieldNames(); it.hasNext(); ) { + String key = it.next(); + result.put( + key, + convertObject( + type, TimeZone.getDefault(), getValue(arrayNode.get(key)), fieldMetadata)); + } + return result; } else { logger.debug( - "Unsupported type passed to readObject(int columnIndex,Class type): " - + type.getName()); + "Given object could not be converted to Map of String and type: " + type.getName()); throw new SQLException( - "Type passed to 'getObject(int columnIndex,Class type)' is unsupported. Type: " - + type.getName()); + "Given object could not be converted to Map of String and type: " + type.getName()); } }); } - private Timestamp formatTimestamp(TimeZone tz, Object value, FieldMetadata fieldMetadata) + private Timestamp convertTimestamp(TimeZone tz, Object value, FieldMetadata fieldMetadata) throws SQLException { if (value == null) { return null; @@ -289,7 +320,12 @@ private Timestamp formatTimestamp(TimeZone tz, Object value, FieldMetadata field @Override public Object readObject() throws SQLException { - return withNextValue((value, jsonNode, fieldMetadata) -> value); + return withNextValue((value, fieldMetadata) -> value); + } + + @Override + public T readObject(Class type) throws SQLException { + return readObject(type, sessionTimeZone); } public boolean wasNull() { @@ -302,13 +338,12 @@ Map convertSqlInputToMap(SQLInput sqlInput) { ((JsonSqlInput) sqlInput).getInput(), new TypeReference>() {}); } - private T withNextValue( - ThrowingTriFunction action) + private T withNextValue(ThrowingBiFunction action) throws SQLException { JsonNode jsonNode = elements.next(); Object value = getValue(jsonNode); wasNull = value == null; - return action.apply(value, jsonNode, fields.get(currentIndex++)); + return action.apply(value, fields.get(currentIndex++)); } private Object getValue(JsonNode jsonNode) { @@ -318,6 +353,8 @@ private Object getValue(JsonNode jsonNode) { return jsonNode.booleanValue(); } else if (jsonNode.isNumber()) { return jsonNode.numberValue(); + } else if (jsonNode.isObject() || jsonNode.isArray()) { + return jsonNode; } return null; } diff --git a/src/main/java/net/snowflake/client/core/SFSqlInput.java b/src/main/java/net/snowflake/client/core/SFSqlInput.java index ea75fe251..b3efa6893 100644 --- a/src/main/java/net/snowflake/client/core/SFSqlInput.java +++ b/src/main/java/net/snowflake/client/core/SFSqlInput.java @@ -6,6 +6,8 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLInput; +import java.util.List; +import java.util.Map; import java.util.TimeZone; /** This interface extends the standard {@link SQLInput} interface to provide additional methods. */ @@ -33,4 +35,53 @@ static SFSqlInput unwrap(SQLInput sqlInput) { * @since 1.2 */ java.sql.Timestamp readTimestamp(TimeZone tz) throws SQLException; + /** + * Reads the next attribute in the stream and returns it as a Object object. + * + * @param the type of the class modeled by this Class object + * @param type Class representing the Java data type to convert the attribute to. + * @return the attribute at the head of the stream as an {@code Object} in the Java programming + * language;{@code null} if the attribute is SQL {@code NULL} + * @exception SQLException if a database access error occurs + * @exception SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.8 + */ + T readObject(Class type, TimeZone tz) throws SQLException; + /** + * Reads the next attribute in the stream and returns it as a List object. + * + * @param the type of the class modeled by this Class object + * @param type Class representing the Java data type to convert the attribute to. + * @return the attribute at the head of the stream as an {@code List} in the Java programming + * language;{@code null} if the attribute is SQL {@code NULL} + * @exception SQLException if a database access error occurs + * @exception SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.8 + */ + List readList(Class type) throws SQLException; + + /** + * Reads the next attribute in the stream and returns it as a Map object. + * + * @param the type of the class modeled by this Class object + * @param type Class representing the Java data type to convert the attribute to. + * @return the attribute at the head of the stream as an {@code Map} in the Java programming + * language;{@code null} if the attribute is SQL {@code NULL} + * @exception SQLException if a database access error occurs + * @exception SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.8 + */ + Map readMap(Class type) throws SQLException; + /** + * Reads the next attribute in the stream and returns it as a Array object. + * + * @param the type of the class modeled by this Class object + * @param type Class representing the Java data type to convert the attribute to. + * @return the attribute at the head of the stream as an {@code Array} in the Java programming + * language;{@code null} if the attribute is SQL {@code NULL} + * @exception SQLException if a database access error occurs + * @exception SQLFeatureNotSupportedException if the JDBC driver does not support this method + * @since 1.8 + */ + T[] readArray(Class type) throws SQLException; } diff --git a/src/test/java/net/snowflake/client/jdbc/structuredtypes/ResultSetStructuredTypesLatestIT.java b/src/test/java/net/snowflake/client/jdbc/structuredtypes/ResultSetStructuredTypesLatestIT.java index 1724f932c..2eaaa5e39 100644 --- a/src/test/java/net/snowflake/client/jdbc/structuredtypes/ResultSetStructuredTypesLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/structuredtypes/ResultSetStructuredTypesLatestIT.java @@ -32,6 +32,7 @@ import net.snowflake.client.jdbc.SnowflakeBaseResultSet; import net.snowflake.client.jdbc.SnowflakeResultSetMetaData; import net.snowflake.client.jdbc.structuredtypes.sqldata.AllTypesClass; +import net.snowflake.client.jdbc.structuredtypes.sqldata.NestedStructSqlData; import net.snowflake.client.jdbc.structuredtypes.sqldata.NullableFieldsSqlData; import net.snowflake.client.jdbc.structuredtypes.sqldata.SimpleClass; import org.junit.Assume; @@ -731,6 +732,56 @@ public void testMapArrayOfArrays() throws SQLException { }); } + @Test + @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) + public void testMapNestedStructures() throws SQLException { + withFirstRow( + "SELECT {'simpleClass': {'string': 'a'}, " + + "'simpleClasses': ARRAY_CONSTRUCT({'string': 'a'}, {'string': 'b'}), " + + "'arrayOfSimpleClasses': ARRAY_CONSTRUCT({'string': 'a'}, {'string': 'b'}), " + + "'mapOfSimpleClasses':{'x':{'string': 'c'}, 'y':{'string': 'd'}}," + + "'texts': ARRAY_CONSTRUCT('string', 'a'), " + + "'arrayOfDates': ARRAY_CONSTRUCT(to_date('2023-12-24', 'YYYY-MM-DD'), to_date('2023-12-25', 'YYYY-MM-DD')), " + + "'mapOfIntegers':{'x':3, 'y':4}}" + + "::OBJECT(simpleClass OBJECT(string VARCHAR), " + + "simpleClasses ARRAY(OBJECT(string VARCHAR))," + + "arrayOfSimpleClasses ARRAY(OBJECT(string VARCHAR))," + + "mapOfSimpleClasses MAP(VARCHAR, OBJECT(string VARCHAR))," + + "texts ARRAY(VARCHAR)," + + "arrayOfDates ARRAY(DATE)," + + "mapOfIntegers MAP(VARCHAR, INTEGER))", + (resultSet) -> { + NestedStructSqlData nestedStructSqlData = + resultSet.getObject(1, NestedStructSqlData.class); + ; + assertEquals("a", nestedStructSqlData.getSimpleClass().getString()); + + assertEquals("a", nestedStructSqlData.getSimpleClassses().get(0).getString()); + assertEquals("b", nestedStructSqlData.getSimpleClassses().get(1).getString()); + + assertEquals("a", nestedStructSqlData.getArrayOfSimpleClasses()[0].getString()); + assertEquals("b", nestedStructSqlData.getArrayOfSimpleClasses()[1].getString()); + + assertEquals("c", nestedStructSqlData.getMapOfSimpleClasses().get("x").getString()); + assertEquals("d", nestedStructSqlData.getMapOfSimpleClasses().get("y").getString()); + + assertEquals("string", nestedStructSqlData.getTexts().get(0)); + assertEquals("a", nestedStructSqlData.getTexts().get(1)); + + // TODO uncomment after merge SNOW-928973: Date field is returning one day less when + // getting + // assertEquals( + // Date.valueOf(LocalDate.of(2023, 12, 24)).toString(), + // nestedStructSqlData.getArrayOfDates()[0].toString()); + // assertEquals( + // Date.valueOf(LocalDate.of(2023, 12, 25)).toString(), + // nestedStructSqlData.getArrayOfDates()[1].toString()); + + assertEquals(Integer.valueOf(3), nestedStructSqlData.getMapOfIntegers().get("x")); + assertEquals(Integer.valueOf(4), nestedStructSqlData.getMapOfIntegers().get("y")); + }); + } + @Test @ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubAction.class) public void testColumnTypeWhenStructureTypeIsDisabled() throws Exception { diff --git a/src/test/java/net/snowflake/client/jdbc/structuredtypes/sqldata/NestedStructSqlData.java b/src/test/java/net/snowflake/client/jdbc/structuredtypes/sqldata/NestedStructSqlData.java new file mode 100644 index 000000000..6c39e6b26 --- /dev/null +++ b/src/test/java/net/snowflake/client/jdbc/structuredtypes/sqldata/NestedStructSqlData.java @@ -0,0 +1,68 @@ +package net.snowflake.client.jdbc.structuredtypes.sqldata; + +import java.sql.Date; +import java.sql.SQLData; +import java.sql.SQLException; +import java.sql.SQLInput; +import java.sql.SQLOutput; +import java.util.List; +import java.util.Map; +import net.snowflake.client.core.SFSqlInput; + +public class NestedStructSqlData implements SQLData { + + private SimpleClass simpleClass; + private List simpleClassses; + private SimpleClass[] arrayOfSimpleClasses; + private Map mapOfSimpleClasses; + private List texts; + private Date[] arrayOfDates; + private Map mapOfIntegers; + + @Override + public String getSQLTypeName() throws SQLException { + return null; + } + + @Override + public void readSQL(SQLInput sqlInput, String typeName) throws SQLException { + simpleClass = sqlInput.readObject(SimpleClass.class); + simpleClassses = SFSqlInput.unwrap(sqlInput).readList(SimpleClass.class); + arrayOfSimpleClasses = SFSqlInput.unwrap(sqlInput).readArray(SimpleClass.class); + mapOfSimpleClasses = SFSqlInput.unwrap(sqlInput).readMap(SimpleClass.class); + texts = SFSqlInput.unwrap(sqlInput).readList(String.class); + arrayOfDates = SFSqlInput.unwrap(sqlInput).readArray(Date.class); + mapOfIntegers = SFSqlInput.unwrap(sqlInput).readMap(Integer.class); + } + + @Override + public void writeSQL(SQLOutput stream) throws SQLException {} + + public SimpleClass getSimpleClass() { + return simpleClass; + } + + public List getSimpleClassses() { + return simpleClassses; + } + + public Map getMapOfSimpleClasses() { + return mapOfSimpleClasses; + } + + public List getTexts() { + return texts; + } + + public Map getMapOfIntegers() { + return mapOfIntegers; + } + + public SimpleClass[] getArrayOfSimpleClasses() { + return arrayOfSimpleClasses; + } + + public Date[] getArrayOfDates() { + return arrayOfDates; + } +}