From fad4f121b395a8a8d8459f796c062180c6c41bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Kubik?= Date: Sun, 1 Dec 2024 23:01:31 +0100 Subject: [PATCH 1/3] SNOW-1812949 add get object and get bytes support for native arrow structured types (#1968) Co-authored-by: Przemyslaw Motacki --- .../client/core/SFArrowResultSet.java | 54 +++++++++++-------- .../client/core/SFBaseResultSet.java | 19 ++++++- .../client/core/SFJsonResultSet.java | 3 +- .../net/snowflake/client/core/SfSqlArray.java | 25 ++++++++- .../client/core/arrow/ArrayConverter.java | 7 ++- .../client/core/arrow/MapConverter.java | 20 +++++-- .../client/core/arrow/StructConverter.java | 19 +++++-- .../core/arrow/StructObjectWrapper.java | 25 +++++++++ .../core/arrow/VectorTypeConverter.java | 11 +++- .../ArrowStringRepresentationBuilderBase.java | 4 +- .../client/core/json/BytesConverter.java | 6 ++- .../client/jdbc/SnowflakeBaseResultSet.java | 38 ++++++++++--- .../client/jdbc/SnowflakeResultSetV1.java | 6 +++ .../ResultSetStructuredTypesLatestIT.java | 35 +++++------- ...dTypesArrowJsonCompatibilityLatestIT.java} | 8 +-- .../StructuredTypesGetStringBaseIT.java | 14 ++++- 16 files changed, 224 insertions(+), 70 deletions(-) create mode 100644 src/main/java/net/snowflake/client/core/arrow/StructObjectWrapper.java rename src/test/java/net/snowflake/client/jdbc/structuredtypes/{StructuredTypesGetStringArrowJsonCompatibilityIT.java => StructuredTypesArrowJsonCompatibilityLatestIT.java} (95%) diff --git a/src/main/java/net/snowflake/client/core/SFArrowResultSet.java b/src/main/java/net/snowflake/client/core/SFArrowResultSet.java index cbb92bcde..69195e8a4 100644 --- a/src/main/java/net/snowflake/client/core/SFArrowResultSet.java +++ b/src/main/java/net/snowflake/client/core/SFArrowResultSet.java @@ -26,7 +26,7 @@ import java.util.stream.Stream; import net.snowflake.client.core.arrow.ArrayConverter; import net.snowflake.client.core.arrow.ArrowVectorConverter; -import net.snowflake.client.core.arrow.StructConverter; +import net.snowflake.client.core.arrow.StructObjectWrapper; import net.snowflake.client.core.arrow.VarCharConverter; import net.snowflake.client.core.arrow.VectorTypeConverter; import net.snowflake.client.core.json.Converters; @@ -576,16 +576,19 @@ public Object getObject(int columnIndex) throws SFException { converter.setSessionTimeZone(sessionTimeZone); Object obj = converter.toObject(index); boolean isStructuredType = resultSetMetaData.isStructuredTypeColumn(columnIndex); - if (type == Types.STRUCT && isStructuredType) { - if (converter instanceof VarCharConverter) { - return createJsonSqlInput(columnIndex, obj); - } else if (converter instanceof StructConverter) { - return createArrowSqlInput(columnIndex, (Map) obj); + if (isVarcharConvertedStruct(type, isStructuredType, converter)) { + if (obj != null) { + return new StructObjectWrapper((String) obj, createJsonSqlInput(columnIndex, obj)); } } return obj; } + private boolean isVarcharConvertedStruct( + int type, boolean isStructuredType, ArrowVectorConverter converter) { + return type == Types.STRUCT && isStructuredType && converter instanceof VarCharConverter; + } + private Object createJsonSqlInput(int columnIndex, Object obj) throws SFException { try { if (obj == null) { @@ -605,15 +608,6 @@ private Object createJsonSqlInput(int columnIndex, Object obj) throws SFExceptio } } - private Object createArrowSqlInput(int columnIndex, Map input) - throws SFException { - if (input == null) { - return null; - } - return new ArrowSqlInput( - input, session, converters, resultSetMetaData.getColumnFields(columnIndex)); - } - @Override public Array getArray(int columnIndex) throws SFException { ArrowVectorConverter converter = currentChunkIterator.getCurrentConverter(columnIndex - 1); @@ -625,16 +619,19 @@ public Array getArray(int columnIndex) throws SFException { } if (converter instanceof VarCharConverter) { return getJsonArray((String) obj, columnIndex); - } else if (converter instanceof ArrayConverter) { - return getArrowArray((List) obj, columnIndex); - } else if (converter instanceof VectorTypeConverter) { - return getArrowArray((List) obj, columnIndex); + } else if (converter instanceof ArrayConverter || converter instanceof VectorTypeConverter) { + StructObjectWrapper structObjectWrapper = (StructObjectWrapper) obj; + return getArrowArray( + structObjectWrapper.getJsonString(), + (List) structObjectWrapper.getObject(), + columnIndex); } else { throw new SFException(queryId, ErrorCode.INVALID_STRUCT_DATA); } } - private SfSqlArray getArrowArray(List elements, int columnIndex) throws SFException { + private SfSqlArray getArrowArray(String text, List elements, int columnIndex) + throws SFException { try { List fieldMetadataList = resultSetMetaData.getColumnFields(columnIndex); if (fieldMetadataList.size() != 1) { @@ -651,26 +648,31 @@ private SfSqlArray getArrowArray(List elements, int columnIndex) throws switch (columnType) { case Types.INTEGER: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.integerConverter(columnType)) .toArray(Integer[]::new)); case Types.SMALLINT: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.smallIntConverter(columnType)) .toArray(Short[]::new)); case Types.TINYINT: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.tinyIntConverter(columnType)) .toArray(Byte[]::new)); case Types.BIGINT: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.bigIntConverter(columnType)).toArray(Long[]::new)); case Types.DECIMAL: case Types.NUMERIC: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.bigDecimalConverter(columnType)) .toArray(BigDecimal[]::new)); @@ -678,35 +680,42 @@ private SfSqlArray getArrowArray(List elements, int columnIndex) throws case Types.VARCHAR: case Types.LONGNVARCHAR: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.varcharConverter(columnType, columnSubType, scale)) .toArray(String[]::new)); case Types.BINARY: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.bytesConverter(columnType, scale)) .toArray(Byte[][]::new)); case Types.FLOAT: case Types.REAL: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.floatConverter(columnType)).toArray(Float[]::new)); case Types.DOUBLE: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.doubleConverter(columnType)) .toArray(Double[]::new)); case Types.DATE: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.dateFromIntConverter(sessionTimeZone)) .toArray(Date[]::new)); case Types.TIME: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.timeFromIntConverter(scale)).toArray(Time[]::new)); case Types.TIMESTAMP: return new SfSqlArray( + text, columnSubType, mapAndConvert( elements, @@ -715,13 +724,16 @@ private SfSqlArray getArrowArray(List elements, int columnIndex) throws .toArray(Timestamp[]::new)); case Types.BOOLEAN: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, converters.booleanConverter(columnType)) .toArray(Boolean[]::new)); case Types.STRUCT: - return new SfSqlArray(columnSubType, mapAndConvert(elements, e -> e).toArray(Map[]::new)); + return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, e -> e).toArray(Map[]::new)); case Types.ARRAY: return new SfSqlArray( + text, columnSubType, mapAndConvert(elements, e -> ((List) e).stream().toArray(Map[]::new)) .toArray(Map[][]::new)); diff --git a/src/main/java/net/snowflake/client/core/SFBaseResultSet.java b/src/main/java/net/snowflake/client/core/SFBaseResultSet.java index 71e56a515..c0b6256ad 100644 --- a/src/main/java/net/snowflake/client/core/SFBaseResultSet.java +++ b/src/main/java/net/snowflake/client/core/SFBaseResultSet.java @@ -273,7 +273,7 @@ protected SQLInput createJsonSqlInputForColumn( } @SnowflakeJdbcInternalApi - protected SfSqlArray getJsonArray(String obj, int columnIndex) throws SFException { + protected SfSqlArray getJsonArray(String arrayString, int columnIndex) throws SFException { try { List fieldMetadataList = resultSetMetaData.getColumnFields(columnIndex); if (fieldMetadataList.size() != 1) { @@ -288,33 +288,38 @@ protected SfSqlArray getJsonArray(String obj, int columnIndex) throws SFExceptio int columnType = ColumnTypeHelper.getColumnType(columnSubType, session); int scale = fieldMetadata.getScale(); - ArrayNode arrayNode = (ArrayNode) OBJECT_MAPPER.readTree(obj); + ArrayNode arrayNode = (ArrayNode) OBJECT_MAPPER.readTree(arrayString); Iterator nodeElements = arrayNode.elements(); switch (columnType) { case Types.INTEGER: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().integerConverter(columnType)) .toArray(Integer[]::new)); case Types.SMALLINT: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().smallIntConverter(columnType)) .toArray(Short[]::new)); case Types.TINYINT: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().tinyIntConverter(columnType)) .toArray(Byte[]::new)); case Types.BIGINT: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().bigIntConverter(columnType)) .toArray(Long[]::new)); case Types.DECIMAL: case Types.NUMERIC: return new SfSqlArray( + arrayString, columnSubType, convertToFixedArray( getStream(nodeElements, getConverters().bigDecimalConverter(columnType)))); @@ -322,6 +327,7 @@ protected SfSqlArray getJsonArray(String obj, int columnIndex) throws SFExceptio case Types.VARCHAR: case Types.LONGNVARCHAR: return new SfSqlArray( + arrayString, columnSubType, getStream( nodeElements, @@ -329,32 +335,38 @@ protected SfSqlArray getJsonArray(String obj, int columnIndex) throws SFExceptio .toArray(String[]::new)); case Types.BINARY: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().bytesConverter(columnType, scale)) .toArray(Byte[][]::new)); case Types.FLOAT: case Types.REAL: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().floatConverter(columnType)) .toArray(Float[]::new)); case Types.DOUBLE: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().doubleConverter(columnType)) .toArray(Double[]::new)); case Types.DATE: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().dateStringConverter(session)) .toArray(Date[]::new)); case Types.TIME: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().timeFromStringConverter(session)) .toArray(Time[]::new)); case Types.TIMESTAMP: return new SfSqlArray( + arrayString, columnSubType, getStream( nodeElements, @@ -364,16 +376,19 @@ protected SfSqlArray getJsonArray(String obj, int columnIndex) throws SFExceptio .toArray(Timestamp[]::new)); case Types.BOOLEAN: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().booleanConverter(columnType)) .toArray(Boolean[]::new)); case Types.STRUCT: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().structConverter(OBJECT_MAPPER)) .toArray(Map[]::new)); case Types.ARRAY: return new SfSqlArray( + arrayString, columnSubType, getStream(nodeElements, getConverters().arrayConverter(OBJECT_MAPPER)) .toArray(Map[][]::new)); diff --git a/src/main/java/net/snowflake/client/core/SFJsonResultSet.java b/src/main/java/net/snowflake/client/core/SFJsonResultSet.java index c32a16424..04e8d3fba 100644 --- a/src/main/java/net/snowflake/client/core/SFJsonResultSet.java +++ b/src/main/java/net/snowflake/client/core/SFJsonResultSet.java @@ -15,6 +15,7 @@ import java.sql.Types; import java.util.List; import java.util.TimeZone; +import net.snowflake.client.core.arrow.StructObjectWrapper; import net.snowflake.client.core.json.Converters; import net.snowflake.client.jdbc.ErrorCode; import net.snowflake.client.jdbc.FieldMetadata; @@ -87,7 +88,7 @@ public Object getObject(int columnIndex) throws SFException { case Types.STRUCT: if (resultSetMetaData.isStructuredTypeColumn(columnIndex)) { - return getSqlInput((String) obj, columnIndex); + return new StructObjectWrapper((String) obj, getSqlInput((String) obj, columnIndex)); } else { throw new SFException(ErrorCode.FEATURE_UNSUPPORTED, "data type: " + type); } diff --git a/src/main/java/net/snowflake/client/core/SfSqlArray.java b/src/main/java/net/snowflake/client/core/SfSqlArray.java index 70682b4f4..4966d7ab8 100644 --- a/src/main/java/net/snowflake/client/core/SfSqlArray.java +++ b/src/main/java/net/snowflake/client/core/SfSqlArray.java @@ -1,6 +1,7 @@ package net.snowflake.client.core; import static net.snowflake.client.core.FieldSchemaCreator.buildBindingSchemaForType; +import static net.snowflake.client.core.FieldSchemaCreator.logger; import com.fasterxml.jackson.core.JsonProcessingException; import java.sql.Array; @@ -16,14 +17,21 @@ @SnowflakeJdbcInternalApi public class SfSqlArray implements Array { + private String text; private int baseType; private Object elements; + private String jsonStringFromElements; - public SfSqlArray(int baseType, Object elements) { + public SfSqlArray(String text, int baseType, Object elements) { + this.text = text; this.baseType = baseType; this.elements = elements; } + public SfSqlArray(int baseType, Object elements) { + this(null, baseType, elements); + } + @Override public String getBaseTypeName() throws SQLException { return JDBCType.valueOf(baseType).getName(); @@ -81,7 +89,22 @@ public ResultSet getResultSet(long index, int count, Map> map) @Override public void free() throws SQLException {} + public String getText() { + if (text == null) { + logger.warn("Text field wasn't initialized. Should never happen."); + } + return text; + } + public String getJsonString() throws SQLException { + if (jsonStringFromElements == null) { + jsonStringFromElements = buildJsonStringFromElements(elements); + } + + return jsonStringFromElements; + } + + private static String buildJsonStringFromElements(Object elements) throws SQLException { try { return SnowflakeUtil.mapJson(elements); } catch (JsonProcessingException e) { diff --git a/src/main/java/net/snowflake/client/core/arrow/ArrayConverter.java b/src/main/java/net/snowflake/client/core/arrow/ArrayConverter.java index ad9926eac..96b683151 100644 --- a/src/main/java/net/snowflake/client/core/arrow/ArrayConverter.java +++ b/src/main/java/net/snowflake/client/core/arrow/ArrayConverter.java @@ -25,7 +25,12 @@ public ArrayConverter(ListVector valueVector, int vectorIndex, DataConversionCon @Override public Object toObject(int index) throws SFException { - return vector.getObject(index); + return isNull(index) ? null : new StructObjectWrapper(toString(index), vector.getObject(index)); + } + + @Override + public byte[] toBytes(int index) throws SFException { + return isNull(index) ? null : toString(index).getBytes(); } @Override diff --git a/src/main/java/net/snowflake/client/core/arrow/MapConverter.java b/src/main/java/net/snowflake/client/core/arrow/MapConverter.java index 4099cd5fb..0c6ca072e 100644 --- a/src/main/java/net/snowflake/client/core/arrow/MapConverter.java +++ b/src/main/java/net/snowflake/client/core/arrow/MapConverter.java @@ -1,6 +1,8 @@ package net.snowflake.client.core.arrow; +import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import net.snowflake.client.core.DataConversionContext; import net.snowflake.client.core.SFException; @@ -28,11 +30,23 @@ public MapConverter(MapVector valueVector, int columnIndex, DataConversionContex @Override public Object toObject(int index) throws SFException { + if (isNull(index)) { + return null; + } + List> entriesList = (List>) vector.getObject(index); - return entriesList.stream() - .collect( - Collectors.toMap(entry -> entry.get("key").toString(), entry -> entry.get("value"))); + Map map = + entriesList.stream() + .collect( + Collectors.toMap( + entry -> entry.get("key").toString(), entry -> entry.get("value"))); + return new StructObjectWrapper(toString(index), map); + } + + @Override + public byte[] toBytes(int index) throws SFException { + return isNull(index) ? null : toString(index).getBytes(StandardCharsets.UTF_8); } @Override diff --git a/src/main/java/net/snowflake/client/core/arrow/StructConverter.java b/src/main/java/net/snowflake/client/core/arrow/StructConverter.java index 4c0516c51..ab7d20382 100644 --- a/src/main/java/net/snowflake/client/core/arrow/StructConverter.java +++ b/src/main/java/net/snowflake/client/core/arrow/StructConverter.java @@ -21,7 +21,14 @@ public StructConverter(StructVector vector, int columnIndex, DataConversionConte @Override public Object toObject(int index) throws SFException { - return structVector.getObject(index); + return isNull(index) + ? null + : new StructObjectWrapper(toString(index), structVector.getObject(index)); + } + + @Override + public byte[] toBytes(int index) throws SFException { + return isNull(index) ? null : toString(index).getBytes(); } @Override @@ -32,9 +39,13 @@ public String toString(int index) throws SFException { SnowflakeType logicalType = ArrowVectorConverterUtil.getSnowflakeTypeFromFieldMetadata(fieldVector.getField()); try { - ArrowVectorConverter converter = - ArrowVectorConverterUtil.initConverter(fieldVector, context, columnIndex); - builder.appendKeyValue(childName, converter.toString(index), logicalType); + if (fieldVector.isNull(index)) { + builder.appendKeyValue(childName, null, logicalType); + } else { + ArrowVectorConverter converter = + ArrowVectorConverterUtil.initConverter(fieldVector, context, columnIndex); + builder.appendKeyValue(childName, converter.toString(index), logicalType); + } } catch (SnowflakeSQLException e) { return structVector.getObject(index).toString(); } diff --git a/src/main/java/net/snowflake/client/core/arrow/StructObjectWrapper.java b/src/main/java/net/snowflake/client/core/arrow/StructObjectWrapper.java new file mode 100644 index 000000000..8219c110a --- /dev/null +++ b/src/main/java/net/snowflake/client/core/arrow/StructObjectWrapper.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ +package net.snowflake.client.core.arrow; + +import net.snowflake.client.core.SnowflakeJdbcInternalApi; + +@SnowflakeJdbcInternalApi +public class StructObjectWrapper { + private final String jsonString; + private final Object object; + + public StructObjectWrapper(String jsonString, Object object) { + this.jsonString = jsonString; + this.object = object; + } + + public String getJsonString() { + return jsonString; + } + + public Object getObject() { + return object; + } +} diff --git a/src/main/java/net/snowflake/client/core/arrow/VectorTypeConverter.java b/src/main/java/net/snowflake/client/core/arrow/VectorTypeConverter.java index 8d1ae2942..cb9dcad73 100644 --- a/src/main/java/net/snowflake/client/core/arrow/VectorTypeConverter.java +++ b/src/main/java/net/snowflake/client/core/arrow/VectorTypeConverter.java @@ -24,7 +24,16 @@ public VectorTypeConverter( @Override public Object toObject(int index) throws SFException { - return vector.getObject(index); + if (isNull(index)) { + return null; + } + Object object = vector.getObject(index); + return new StructObjectWrapper(object.toString(), object); + } + + @Override + public byte[] toBytes(int index) throws SFException { + return isNull(index) ? null : toString(index).getBytes(); } @Override diff --git a/src/main/java/net/snowflake/client/core/arrow/tostringhelpers/ArrowStringRepresentationBuilderBase.java b/src/main/java/net/snowflake/client/core/arrow/tostringhelpers/ArrowStringRepresentationBuilderBase.java index cc25bb7e0..f60daa0cc 100644 --- a/src/main/java/net/snowflake/client/core/arrow/tostringhelpers/ArrowStringRepresentationBuilderBase.java +++ b/src/main/java/net/snowflake/client/core/arrow/tostringhelpers/ArrowStringRepresentationBuilderBase.java @@ -21,7 +21,6 @@ public abstract class ArrowStringRepresentationBuilderBase { quotableTypes.add(SnowflakeType.ANY); quotableTypes.add(SnowflakeType.CHAR); quotableTypes.add(SnowflakeType.TEXT); - quotableTypes.add(SnowflakeType.VARIANT); quotableTypes.add(SnowflakeType.BINARY); quotableTypes.add(SnowflakeType.DATE); quotableTypes.add(SnowflakeType.TIME); @@ -44,6 +43,9 @@ private boolean shouldQuoteValue(SnowflakeType type) { } protected String quoteIfNeeded(String string, SnowflakeType type) { + if (string == null) { + return null; + } // Turn Boolean string representations lowercase to make the output JSON-compatible // this should be changed on the converter level, but it would be a breaking change thus // for now only structured types will be valid JSONs while in NATIVE ARROW mode diff --git a/src/main/java/net/snowflake/client/core/json/BytesConverter.java b/src/main/java/net/snowflake/client/core/json/BytesConverter.java index 8212e5830..eb1f80a5a 100644 --- a/src/main/java/net/snowflake/client/core/json/BytesConverter.java +++ b/src/main/java/net/snowflake/client/core/json/BytesConverter.java @@ -1,6 +1,7 @@ package net.snowflake.client.core.json; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.sql.Types; import net.snowflake.client.core.SFException; import net.snowflake.client.jdbc.ErrorCode; @@ -46,10 +47,13 @@ public byte[] getBytes(Object obj, int columnType, int columnSubType, Integer sc .toByteArray(); case Types.VARCHAR: case Types.CHAR: + case Types.STRUCT: + case Types.ARRAY: + case SnowflakeUtil.EXTRA_TYPES_VECTOR: return converters .getStringConverter() .getString(obj, columnType, columnSubType, scale) - .getBytes(); + .getBytes(StandardCharsets.UTF_8); case Types.BOOLEAN: return converters.getBooleanConverter().getBoolean(obj, columnType) ? new byte[] {1} diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeBaseResultSet.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeBaseResultSet.java index ced00e325..633083391 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeBaseResultSet.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeBaseResultSet.java @@ -45,6 +45,7 @@ import net.snowflake.client.core.SFBaseResultSet; import net.snowflake.client.core.SFBaseSession; import net.snowflake.client.core.SFException; +import net.snowflake.client.core.arrow.StructObjectWrapper; import net.snowflake.client.core.structs.SQLDataCreationHelper; import net.snowflake.client.log.SFLogger; import net.snowflake.client.log.SFLoggerFactory; @@ -1395,7 +1396,11 @@ public T getObject(int columnIndex, Class type) throws SQLException { if (SQLData.class.isAssignableFrom(type)) { SQLInput sqlInput = SnowflakeUtil.mapSFExceptionToSQLException( - () -> (SQLInput) sfBaseResultSet.getObject(columnIndex)); + () -> { + StructObjectWrapper structObjectWrapper = + (StructObjectWrapper) sfBaseResultSet.getObject(columnIndex); + return (SQLInput) createJsonSqlInput(columnIndex, structObjectWrapper); + }); if (sqlInput == null) { return null; } else { @@ -1631,13 +1636,16 @@ public Map getMap(int columnIndex, Class type) throws SQLExcep int columnType = ColumnTypeHelper.getColumnType(valueFieldMetadata.getType(), session); int scale = valueFieldMetadata.getScale(); TimeZone tz = sfBaseResultSet.getSessionTimeZone(); - Object object = - SnowflakeUtil.mapSFExceptionToSQLException(() -> sfBaseResultSet.getObject(columnIndex)); - if (object == null) { + StructObjectWrapper structObjectWrapper = + (StructObjectWrapper) + SnowflakeUtil.mapSFExceptionToSQLException( + () -> sfBaseResultSet.getObject(columnIndex)); + if (structObjectWrapper == null) { return null; } Map map = - mapSFExceptionToSQLException(() -> prepareMapWithValues(object, type)); + mapSFExceptionToSQLException( + () -> prepareMapWithValues(structObjectWrapper.getObject(), type)); Map resultMap = new HashMap<>(); for (Map.Entry entry : map.entrySet()) { if (SQLData.class.isAssignableFrom(type)) { @@ -1645,7 +1653,7 @@ public Map getMap(int columnIndex, Class type) throws SQLExcep SQLInput sqlInput = sfBaseResultSet.createSqlInputForColumn( entry.getValue(), - object.getClass(), + structObjectWrapper.getObject().getClass(), columnIndex, session, valueFieldMetadata.getFields()); @@ -1824,4 +1832,22 @@ private Map prepareMapWithValues(Object object, Class typ throw new SFException(ErrorCode.INVALID_STRUCT_DATA, "Object couldn't be converted to map"); } } + + private Object createJsonSqlInput(int columnIndex, StructObjectWrapper obj) throws SFException { + try { + if (obj == null) { + return null; + } + JsonNode jsonNode = OBJECT_MAPPER.readTree(obj.getJsonString()); + return new JsonSqlInput( + obj.getJsonString(), + jsonNode, + session, + sfBaseResultSet.getConverters(), + sfBaseResultSet.getMetaData().getColumnFields(columnIndex), + sfBaseResultSet.getSessionTimeZone()); + } catch (JsonProcessingException e) { + throw new SFException(sfBaseResultSet.getQueryId(), e, ErrorCode.INVALID_STRUCT_DATA); + } + } } diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetV1.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetV1.java index 4f73b4c18..e9db5ec71 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetV1.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeResultSetV1.java @@ -33,6 +33,8 @@ import net.snowflake.client.core.QueryStatus; import net.snowflake.client.core.SFBaseResultSet; import net.snowflake.client.core.SFException; +import net.snowflake.client.core.SfSqlArray; +import net.snowflake.client.core.arrow.StructObjectWrapper; import net.snowflake.client.log.SFLogger; import net.snowflake.client.log.SFLoggerFactory; @@ -274,6 +276,10 @@ public Object getObject(int columnIndex) throws SQLException { return null; } else if (object instanceof JsonSqlInput) { return ((JsonSqlInput) object).getText(); + } else if (object instanceof StructObjectWrapper) { + return ((StructObjectWrapper) object).getJsonString(); + } else if (object instanceof SfSqlArray) { + return ((SfSqlArray) object).getText(); } else if (object instanceof ArrowSqlInput) { throw new SQLException( "Arrow native struct couldn't be converted to String. To map to SqlData the method getObject(int columnIndex, Class type) should be used"); 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 1660ea57d..f9b3368d8 100644 --- a/src/test/java/net/snowflake/client/jdbc/structuredtypes/ResultSetStructuredTypesLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/structuredtypes/ResultSetStructuredTypesLatestIT.java @@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; @@ -39,6 +38,7 @@ import net.snowflake.client.jdbc.structuredtypes.sqldata.SimpleClass; import net.snowflake.client.jdbc.structuredtypes.sqldata.StringClass; import net.snowflake.client.providers.ResultFormatProvider; +import org.junit.Assert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; @@ -201,7 +201,7 @@ public void testReturnStructAsStringIfTypeWasNotIndicated(ResultSetFormatType fo try (ResultSet resultSet = statement.executeQuery(AllTypesClass.ALL_TYPES_QUERY); ) { resultSet.next(); String object = (String) resultSet.getObject(1); - String expected = + String expectedJson = "{\n" + " \"string\": \"a\",\n" + " \"b\": 1,\n" @@ -223,31 +223,20 @@ public void testReturnStructAsStringIfTypeWasNotIndicated(ResultSetFormatType fo + " \"intValue\": 2\n" + " }\n" + "}"; - assertEquals(expected, object); + String expectedJsonFromArrow = + "{\"string\": \"a\",\"b\": 1,\"s\": 2,\"i\": 3,\"l\": 4,\"f\": 1.1,\"d\": 2.2,\"bd\": 3.3," + + "\"bool\": true,\"timestamp_ltz\": \"2021-12-22 09:43:44.000 +0100\",\"timestamp_ntz\": \"2021-12-23 09:44:44.000\"," + + "\"timestamp_tz\": \"2021-12-24 09:45:45.000 +0800\",\"date\": \"2023-12-24\",\"time\": \"12:34:56\",\"binary\": \"616263\"," + + "\"simpleClass\": {\"string\": \"b\",\"intValue\": 2}}"; + if (format == ResultSetFormatType.NATIVE_ARROW) { + Assert.assertEquals(expectedJsonFromArrow, object); + } else { + Assert.assertEquals(expectedJson, object); + } } } } - @ParameterizedTest - @ArgumentsSource(ResultFormatProvider.class) - @DontRunOnGithubActions - public void testThrowingGettingObjectIfTypeWasNotIndicatedAndFormatNativeArrow( - ResultSetFormatType format) throws SQLException { - Assumptions.assumeTrue(format == ResultSetFormatType.NATIVE_ARROW); - withFirstRow( - "select {'string':'a'}::OBJECT(string VARCHAR)", - (resultSet) -> { - assertThrows(SQLException.class, () -> resultSet.getObject(1)); - }, - format); - withFirstRow( - "select {'x':{'string':'one'},'y':{'string':'two'},'z':{'string':'three'}}::MAP(VARCHAR, OBJECT(string VARCHAR));", - (resultSet) -> { - assertThrows(SQLException.class, () -> resultSet.getObject(1, Map.class)); - }, - format); - } - @ParameterizedTest @ArgumentsSource(ResultFormatProvider.class) @DontRunOnGithubActions diff --git a/src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesGetStringArrowJsonCompatibilityIT.java b/src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesArrowJsonCompatibilityLatestIT.java similarity index 95% rename from src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesGetStringArrowJsonCompatibilityIT.java rename to src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesArrowJsonCompatibilityLatestIT.java index c3ae5fdd8..77bfca52d 100644 --- a/src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesGetStringArrowJsonCompatibilityIT.java +++ b/src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesArrowJsonCompatibilityLatestIT.java @@ -21,8 +21,7 @@ import org.junit.jupiter.params.provider.ArgumentsSource; @Tag(TestTags.RESULT_SET) -public class StructuredTypesGetStringArrowJsonCompatibilityIT - extends StructuredTypesGetStringBaseIT { +public class StructuredTypesArrowJsonCompatibilityLatestIT extends StructuredTypesGetStringBaseIT { private static Map connections = new HashMap<>(); @BeforeAll @@ -44,7 +43,7 @@ public static void closeConnections() throws SQLException { @ParameterizedTest @DontRunOnGithubActions @ArgumentsSource(DataProvider.class) - public void testRunAsGetString( + public void testArrowJsonCompatibility( ResultSetFormatType queryResultFormat, String selectSql, String expectedStructureTypeRepresentation) @@ -52,7 +51,7 @@ public void testRunAsGetString( withFirstRow( connections.get(queryResultFormat), selectSql, - (resultSet) -> assertGetStringIsCompatible(resultSet, expectedStructureTypeRepresentation)); + (resultSet) -> assertResultSetIsCompatible(resultSet, expectedStructureTypeRepresentation)); } public static class SampleProvider extends SnowflakeArgumentsProvider { @@ -146,6 +145,7 @@ protected List rawArguments(ExtensionContext context) { "{\"binary\":\"616263\"}")); samples.add(Arguments.of("select [1,2,3]::VECTOR(INT, 3)", "[1,2,3]")); samples.add(Arguments.of("select ['a','b','c']::ARRAY(varchar)", "[\"a\",\"b\",\"c\"]")); + samples.add(Arguments.of("select ['a','b','c']::ARRAY(variant)", "[\"a\",\"b\",\"c\"]")); return samples; } diff --git a/src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesGetStringBaseIT.java b/src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesGetStringBaseIT.java index 35d10c4b1..767e64e18 100644 --- a/src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesGetStringBaseIT.java +++ b/src/test/java/net/snowflake/client/jdbc/structuredtypes/StructuredTypesGetStringBaseIT.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; @@ -45,10 +46,21 @@ protected static Connection initConnection(ResultSetFormatType queryResultFormat return conn; } - protected void assertGetStringIsCompatible(ResultSet resultSet, String expected) + protected void assertResultSetIsCompatible(ResultSet resultSet, String expected) throws SQLException { + // Test getString String result = resultSet.getString(1); TestUtil.assertEqualsIgnoringWhitespace(expected, result); + + // Test getObject + result = resultSet.getObject(1, String.class); + String resultCasted = (String) resultSet.getObject(1); + TestUtil.assertEqualsIgnoringWhitespace(expected, result); + TestUtil.assertEqualsIgnoringWhitespace(expected, resultCasted); + + // Test getBytes + TestUtil.assertEqualsIgnoringWhitespace( + expected, new String(resultSet.getBytes(1), StandardCharsets.UTF_8)); } protected void withFirstRow( From 4faff346cc7cb21f80519bcc69ce07ac5ef1a776 Mon Sep 17 00:00:00 2001 From: Dominik Przybysz <132913826+sfc-gh-dprzybysz@users.noreply.github.com> Date: Tue, 3 Dec 2024 08:39:45 +0100 Subject: [PATCH 2/3] SNOW-1548083: Remove experimental label from thin driver (#1970) --- README.rst | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index a9d3cacb2..faac38fd1 100644 --- a/README.rst +++ b/README.rst @@ -9,12 +9,22 @@ Snowflake JDBC Driver .. image:: http://img.shields.io/:license-Apache%202-brightgreen.svg :target: http://www.apache.org/licenses/LICENSE-2.0.txt - -.. image:: https://maven-badges.herokuapp.com/maven-central/net.snowflake/snowflake-jdbc/badge.svg?style=plastic - :target: https://repo1.maven.org/maven2/net/snowflake/snowflake-jdbc/ - + Snowflake provides a JDBC type 4 driver that supports core functionality, allowing Java program to connect to Snowflake. +.. |maven-snowflake-jdbc| image:: https://maven-badges.herokuapp.com/maven-central/net.snowflake/snowflake-jdbc/badge.svg?style=plastic + :target: https://repo1.maven.org/maven2/net/snowflake/snowflake-jdbc/ + +.. |maven-snowflake-jdbc-fips| image:: https://maven-badges.herokuapp.com/maven-central/net.snowflake/snowflake-jdbc-fips/badge.svg?style=plastic + :target: https://repo1.maven.org/maven2/net/snowflake/snowflake-jdbc-fips/ + +.. |maven-snowflake-jdbc-thin| image:: https://maven-badges.herokuapp.com/maven-central/net.snowflake/snowflake-jdbc-thin/badge.svg?style=plastic + :target: https://repo1.maven.org/maven2/net/snowflake/snowflake-jdbc-thin/ + +- snowflake-jdbc (fat-jar): |maven-snowflake-jdbc| +- snowflake-jdbc-fips (FIPS compliant fat-jar): |maven-snowflake-jdbc-fips| +- snowflake-jdbc-thin (thin-jar): |maven-snowflake-jdbc-thin| + Prerequisites ============= @@ -45,7 +55,7 @@ or for FIPS compliant fat-jar {version} -or for experimental thin-jar +or for thin-jar .. code-block:: xml @@ -79,7 +89,7 @@ Build from Source Code ../mvnw org.apache.maven.plugins:maven-install-plugin:3.1.1:install-file -Dfile=target/snowflake-jdbc-fips.jar -DpomFile=./public_pom.xml cd - -4. Build the experimental thin-jar and install it in local maven repository by running: +4. Build the thin-jar and install it in local maven repository by running: .. code-block:: bash From 852b2d9e4c6342c3b6e46eeb44dcc8cf96c1603c Mon Sep 17 00:00:00 2001 From: Dominik Przybysz <132913826+sfc-gh-dprzybysz@users.noreply.github.com> Date: Wed, 4 Dec 2024 13:36:42 +0100 Subject: [PATCH 3/3] SNOW-1652680: Add option to override j.u.l.ConsoleHandler to write to stdout (#1981) --- .../net/snowflake/client/core/SFSession.java | 6 +++ .../client/core/SFSessionProperty.java | 4 +- .../net/snowflake/client/log/JDK14Logger.java | 24 ++++++++++++ .../client/log/StdOutConsoleHandler.java | 26 +++++++++++++ ...4LoggerConsoleHandlerOverrideLatestIT.java | 39 +++++++++++++++++++ 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/snowflake/client/log/StdOutConsoleHandler.java create mode 100644 src/test/java/net/snowflake/client/log/JDK14LoggerConsoleHandlerOverrideLatestIT.java diff --git a/src/main/java/net/snowflake/client/core/SFSession.java b/src/main/java/net/snowflake/client/core/SFSession.java index 59aac9d5b..d26da7828 100644 --- a/src/main/java/net/snowflake/client/core/SFSession.java +++ b/src/main/java/net/snowflake/client/core/SFSession.java @@ -46,6 +46,7 @@ import net.snowflake.client.jdbc.telemetry.Telemetry; import net.snowflake.client.jdbc.telemetry.TelemetryClient; import net.snowflake.client.jdbc.telemetryOOB.TelemetryService; +import net.snowflake.client.log.JDK14Logger; import net.snowflake.client.log.SFLogger; import net.snowflake.client.log.SFLoggerFactory; import net.snowflake.client.log.SFLoggerUtil; @@ -435,6 +436,11 @@ public void addSFSessionProperty(String propertyName, Object propertyValue) thro tracingLevel = Level.parse(((String) propertyValue).toUpperCase()); } break; + case JAVA_LOGGING_CONSOLE_STD_OUT: + if (propertyValue != null && (Boolean) propertyValue) { + JDK14Logger.useStdOutConsoleHandler(); + } + break; case DISABLE_SOCKS_PROXY: // note: if any session has this parameter, it will be used for all diff --git a/src/main/java/net/snowflake/client/core/SFSessionProperty.java b/src/main/java/net/snowflake/client/core/SFSessionProperty.java index 97c0adbc2..6a6730c49 100644 --- a/src/main/java/net/snowflake/client/core/SFSessionProperty.java +++ b/src/main/java/net/snowflake/client/core/SFSessionProperty.java @@ -112,7 +112,9 @@ public enum SFSessionProperty { HTTP_CLIENT_CONNECTION_TIMEOUT("HTTP_CLIENT_CONNECTION_TIMEOUT", false, Integer.class), - HTTP_CLIENT_SOCKET_TIMEOUT("HTTP_CLIENT_SOCKET_TIMEOUT", false, Integer.class); + HTTP_CLIENT_SOCKET_TIMEOUT("HTTP_CLIENT_SOCKET_TIMEOUT", false, Integer.class), + + JAVA_LOGGING_CONSOLE_STD_OUT("JAVA_LOGGING_CONSOLE_STD_OUT", false, Boolean.class); // property key in string private String propertyKey; diff --git a/src/main/java/net/snowflake/client/log/JDK14Logger.java b/src/main/java/net/snowflake/client/log/JDK14Logger.java index e9ae25696..466031af2 100644 --- a/src/main/java/net/snowflake/client/log/JDK14Logger.java +++ b/src/main/java/net/snowflake/client/log/JDK14Logger.java @@ -18,6 +18,8 @@ import java.util.logging.SimpleFormatter; import net.snowflake.client.core.EventHandler; import net.snowflake.client.core.EventUtil; +import net.snowflake.client.core.SFSessionProperty; +import net.snowflake.client.core.SnowflakeJdbcInternalApi; import net.snowflake.client.util.SecretDetector; /** @@ -38,10 +40,32 @@ public class JDK14Logger implements SFLogger { public static String STDOUT = "STDOUT"; + private static final StdOutConsoleHandler STD_OUT_CONSOLE_HANDLER = new StdOutConsoleHandler(); + public JDK14Logger(String name) { this.jdkLogger = Logger.getLogger(name); } + static { + String javaLoggingConsoleStdOut = + System.getProperty(SFSessionProperty.JAVA_LOGGING_CONSOLE_STD_OUT.getPropertyKey()); + if ("true".equalsIgnoreCase(javaLoggingConsoleStdOut)) { + useStdOutConsoleHandler(); + } + } + + @SnowflakeJdbcInternalApi + public static void useStdOutConsoleHandler() { + Logger rootLogger = Logger.getLogger(""); + for (Handler handler : rootLogger.getHandlers()) { + if (handler instanceof ConsoleHandler) { + rootLogger.removeHandler(handler); + rootLogger.addHandler(STD_OUT_CONSOLE_HANDLER); + break; + } + } + } + public boolean isDebugEnabled() { return this.jdkLogger.isLoggable(Level.FINE); } diff --git a/src/main/java/net/snowflake/client/log/StdOutConsoleHandler.java b/src/main/java/net/snowflake/client/log/StdOutConsoleHandler.java new file mode 100644 index 000000000..b41659edb --- /dev/null +++ b/src/main/java/net/snowflake/client/log/StdOutConsoleHandler.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2012-2019 Snowflake Computing Inc. All rights reserved. + */ +package net.snowflake.client.log; + +import java.util.logging.LogRecord; +import java.util.logging.SimpleFormatter; +import java.util.logging.StreamHandler; + +class StdOutConsoleHandler extends StreamHandler { + public StdOutConsoleHandler() { + // configure with specific defaults for ConsoleHandler + super(System.out, new SimpleFormatter()); + } + + @Override + public void publish(LogRecord record) { + super.publish(record); + flush(); + } + + @Override + public void close() { + flush(); + } +} diff --git a/src/test/java/net/snowflake/client/log/JDK14LoggerConsoleHandlerOverrideLatestIT.java b/src/test/java/net/snowflake/client/log/JDK14LoggerConsoleHandlerOverrideLatestIT.java new file mode 100644 index 000000000..2e91e069d --- /dev/null +++ b/src/test/java/net/snowflake/client/log/JDK14LoggerConsoleHandlerOverrideLatestIT.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved. + */ +package net.snowflake.client.log; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Properties; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Logger; +import net.snowflake.client.category.TestTags; +import net.snowflake.client.jdbc.BaseJDBCTest; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag(TestTags.CORE) +public class JDK14LoggerConsoleHandlerOverrideLatestIT extends BaseJDBCTest { + /** Added in > 3.20.0 */ + @Test + public void shouldOverrideConsoleLogger() throws Exception { + Properties paramProperties = new Properties(); + paramProperties.put("JAVA_LOGGING_CONSOLE_STD_OUT", true); + try (Connection connection = getConnection(paramProperties); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("select 1")) { + assertTrue(resultSet.next()); + Handler[] handlers = Logger.getLogger("").getHandlers(); + assertTrue(handlers.length > 0); + assertFalse(Arrays.stream(handlers).anyMatch(h -> h instanceof ConsoleHandler)); + assertTrue(Arrays.stream(handlers).anyMatch(h -> h instanceof StdOutConsoleHandler)); + } + } +}