Repository: snowflakedb/gosnowflake
Pull request #1082 (2024-03-25 14:35:05)

SNOW-1256908 Implement structured objects
### Description

SNOW-1256908 Implementation of structured objects

### Checklist
- [ ] Created tests which fail without the change (if possible)
- [ ] Extended the README / documentation, if necessary


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) Report
Attention: Patch coverage is `88.06818%` with `42 lines` in your changes are missing coverage. Please review.
> Project coverage is 82.87%. Comparing base [(`bcb26f9`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/bcb26f9b7ff621db8b03ea6810b955d99f19b048?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) to head [(`946e6a2`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb).

| [Files](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) | Patch % | Lines |
|---|---|---|
| [structured\_type.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-c3RydWN0dXJlZF90eXBlLmdv) | 73.95% | [25 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [converter.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-Y29udmVydGVyLmdv) | 93.66% | [14 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [datetime.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-ZGF0ZXRpbWUuZ28=) | 89.28% | [3 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1082      +/-   ##
==========================================
- Coverage   84.00%   82.87%   -1.13%     
==========================================
  Files          51       51              
  Lines        9975    10220     +245     
==========================================
+ Hits         8379     8470      +91     
- Misses       1596     1750     +154     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1082?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb).


Patch:
diff --git a/arrow_chunk.go b/arrow_chunk.go
index b690674..24a3664 100644
--- a/arrow_chunk.go
+++ b/arrow_chunk.go
@@ -19,7 +19,7 @@ type arrowResultChunk struct {
 	allocator memory.Allocator
 }
 
-func (arc *arrowResultChunk) decodeArrowChunk(rowType []execResponseRowType, highPrec bool) ([]chunkRowType, error) {
+func (arc *arrowResultChunk) decodeArrowChunk(rowType []execResponseRowType, highPrec bool, params map[string]*string) ([]chunkRowType, error) {
 	logger.Debug("Arrow Decoder")
 	var chunkRows []chunkRowType
 
@@ -36,7 +36,7 @@ func (arc *arrowResultChunk) decodeArrowChunk(rowType []execResponseRowType, hig
 
 		for colIdx, col := range columns {
 			values := make([]snowflakeValue, numRows)
-			if err := arrowToValue(values, rowType[colIdx], col, arc.loc, highPrec); err != nil {
+			if err := arrowToValues(values, rowType[colIdx], col, arc.loc, highPrec, params); err != nil {
 				return nil, err
 			}
 
diff --git a/chunk_downloader.go b/chunk_downloader.go
index eaeec18..4fcc15a 100644
--- a/chunk_downloader.go
+++ b/chunk_downloader.go
@@ -114,7 +114,7 @@ func (scd *snowflakeChunkDownloader) start() error {
 			return err
 		}
 		higherPrecision := higherPrecisionEnabled(scd.ctx)
-		scd.CurrentChunk, err = firstArrowChunk.decodeArrowChunk(scd.RowSet.RowType, higherPrecision)
+		scd.CurrentChunk, err = firstArrowChunk.decodeArrowChunk(scd.RowSet.RowType, higherPrecision, scd.sc.cfg.Params)
 		scd.CurrentChunkSize = firstArrowChunk.rowCount
 		if err != nil {
 			return err
@@ -458,7 +458,7 @@ func decodeChunk(scd *snowflakeChunkDownloader, idx int, bufStream *bufio.Reader
 			return nil
 		}
 		highPrec := higherPrecisionEnabled(scd.ctx)
-		respd, err = arc.decodeArrowChunk(scd.RowSet.RowType, highPrec)
+		respd, err = arc.decodeArrowChunk(scd.RowSet.RowType, highPrec, scd.sc.cfg.Params)
 		if err != nil {
 			return err
 		}
diff --git a/converter.go b/converter.go
index d84484d..019587e 100644
--- a/converter.go
+++ b/converter.go
@@ -7,6 +7,7 @@ import (
 	"database/sql"
 	"database/sql/driver"
 	"encoding/hex"
+	"encoding/json"
 	"fmt"
 	"math"
 	"math/big"
@@ -107,7 +108,7 @@ func goTypeToSnowflake(v driver.Value, tsmode snowflakeType) snowflakeType {
 }
 
 // snowflakeTypeToGo translates Snowflake data type to Go data type.
-func snowflakeTypeToGo(dbtype snowflakeType, scale int64) reflect.Type {
+func snowflakeTypeToGo(dbtype snowflakeType, scale int64, fields []fieldMetadata) reflect.Type {
 	switch dbtype {
 	case fixedType:
 		if scale == 0 {
@@ -116,7 +117,7 @@ func snowflakeTypeToGo(dbtype snowflakeType, scale int64) reflect.Type {
 		return reflect.TypeOf(float64(0))
 	case realType:
 		return reflect.TypeOf(float64(0))
-	case textType, variantType, objectType, arrayType:
+	case textType, variantType, arrayType:
 		return reflect.TypeOf("")
 	case dateType, timeType, timestampLtzType, timestampNtzType, timestampTzType:
 		return reflect.TypeOf(time.Now())
@@ -124,6 +125,11 @@ func snowflakeTypeToGo(dbtype snowflakeType, scale int64) reflect.Type {
 		return reflect.TypeOf([]byte{})
 	case booleanType:
 		return reflect.TypeOf(true)
+	case objectType:
+		if len(fields) > 0 {
+			return reflect.TypeOf(ObjectType{})
+		}
+		return reflect.TypeOf("")
 	}
 	logger.Errorf("unsupported dbtype is specified. %v", dbtype)
 	return reflect.TypeOf("")
@@ -259,12 +265,7 @@ func extractTimestamp(srcValue *string) (sec int64, nsec int64, err error) {
 
 // stringToValue converts a pointer of string data to an arbitrary golang variable
 // This is mainly used in fetching data.
-func stringToValue(
-	dest *driver.Value,
-	srcColumnMeta execResponseRowType,
-	srcValue *string,
-	loc *time.Location,
-) error {
+func stringToValue(dest *driver.Value, srcColumnMeta execResponseRowType, srcValue *string, loc *time.Location, params map[string]*string) error {
 	if srcValue == nil {
 		logger.Debugf("snowflake data type: %v, raw value: nil", srcColumnMeta.Type)
 		*dest = nil
@@ -272,7 +273,20 @@ func stringToValue(
 	}
 	logger.Debugf("snowflake data type: %v, raw value: %v", srcColumnMeta.Type, *srcValue)
 	switch srcColumnMeta.Type {
-	case "text", "fixed", "real", "variant", "object":
+	case "object":
+		if len(srcColumnMeta.Fields) == 0 {
+			// semistructured type without schema
+			*dest = *srcValue
+			return nil
+		}
+		m := make(map[string]any)
+		err := json.Unmarshal([]byte(*srcValue), &m)
+		if err != nil {
+			return err
+		}
+		*dest = buildStructuredTypeRecursive(m, srcColumnMeta.Fields, params)
+		return nil
+	case "text", "fixed", "real", "variant":
 		*dest = *srcValue
 		return nil
 	case "date":
@@ -350,6 +364,19 @@ func stringToValue(
 	return nil
 }
 
+func buildStructuredTypeRecursive(m map[string]any, fields []fieldMetadata, params map[string]*string) *structuredType {
+	for _, fm := range fields {
+		if fm.Type == "object" {
+			m[fm.Name] = buildStructuredTypeRecursive(m[fm.Name].(map[string]any), fm.Fields, params)
+		}
+	}
+	return &structuredType{
+		values:        m,
+		fieldMetadata: fields,
+		params:        params,
+	}
+}
+
 var decimalShift = new(big.Int).Exp(big.NewInt(2), big.NewInt(64), nil)
 
 func intToBigFloat(val int64, scale int64) *big.Float {
@@ -445,190 +472,251 @@ func extractFraction(value int64, scale int) int64 {
 
 // Arrow Interface (Column) converter. This is called when Arrow chunks are
 // downloaded to convert to the corresponding row type.
-func arrowToValue(
+func arrowToValues(
 	destcol []snowflakeValue,
 	srcColumnMeta execResponseRowType,
 	srcValue arrow.Array,
 	loc *time.Location,
-	higherPrecision bool) error {
+	higherPrecision bool,
+	params map[string]*string) error {
 
-	var err error
 	if len(destcol) != srcValue.Len() {
-		err = fmt.Errorf("array interface length mismatch")
+		return fmt.Errorf("array interface length mismatch")
 	}
 	logger.Debugf("snowflake data type: %v, arrow data type: %v", srcColumnMeta.Type, srcValue.DataType())
 
+	var err error
+	for i := range destcol {
+		if destcol[i], err = arrowToValue(i, srcColumnMeta, srcValue, loc, higherPrecision, params); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func arrowToValue(rowIdx int, srcColumnMeta execResponseRowType, srcValue arrow.Array, loc *time.Location, higherPrecision bool, params map[string]*string) (snowflakeValue, error) {
 	snowflakeType := getSnowflakeType(srcColumnMeta.Type)
 	switch snowflakeType {
 	case fixedType:
 		// Snowflake data types that are fixed-point numbers will fall into this category
 		// e.g. NUMBER, DECIMAL/NUMERIC, INT/INTEGER
-		switch data := srcValue.(type) {
+		switch numericValue := srcValue.(type) {
 		case *array.Decimal128:
-			for i, num := range data.Values() {
-				if !srcValue.IsNull(i) {
-					if srcColumnMeta.Scale == 0 {
-						if higherPrecision {
-							destcol[i] = num.BigInt()
-						} else {
-							destcol[i] = num.ToString(0)
-						}
-					} else {
-						f := decimalToBigFloat(num, srcColumnMeta.Scale)
-						if higherPrecision {
-							destcol[i] = f
-						} else {
-							destcol[i] = fmt.Sprintf("%.*f", srcColumnMeta.Scale, f)
-						}
-					}
-				}
-			}
+			return arrowDecimal128ToValue(numericValue, rowIdx, higherPrecision, int(srcColumnMeta.Scale)), nil
 		case *array.Int64:
-			for i, val := range data.Int64Values() {
-				if !srcValue.IsNull(i) {
-					if srcColumnMeta.Scale == 0 {
-						if higherPrecision {
-							destcol[i] = val
-						} else {
-							destcol[i] = fmt.Sprintf("%d", val)
-						}
-					} else {
-						if higherPrecision {
-							f := intToBigFloat(val, srcColumnMeta.Scale)
-							destcol[i] = f
-						} else {
-							destcol[i] = fmt.Sprintf("%.*f", srcColumnMeta.Scale, float64(val)/math.Pow10(int(srcColumnMeta.Scale)))
-						}
-					}
-				}
-			}
+			return arrowInt64ToValue(numericValue, rowIdx, higherPrecision, int(srcColumnMeta.Scale)), nil
 		case *array.Int32:
-			for i, val := range data.Int32Values() {
-				if !srcValue.IsNull(i) {
-					if srcColumnMeta.Scale == 0 {
-						if higherPrecision {
-							destcol[i] = int64(val)
-						} else {
-							destcol[i] = fmt.Sprintf("%d", val)
-						}
-					} else {
-						if higherPrecision {
-							f := intToBigFloat(int64(val), srcColumnMeta.Scale)
-							destcol[i] = f
-						} else {
-							destcol[i] = fmt.Sprintf("%.*f", srcColumnMeta.Scale, float64(val)/math.Pow10(int(srcColumnMeta.Scale)))
-						}
-					}
-				}
-			}
+			return arrowInt32ToValue(numericValue, rowIdx, higherPrecision, int(srcColumnMeta.Scale)), nil
 		case *array.Int16:
-			for i, val := range data.Int16Values() {
-				if !srcValue.IsNull(i) {
-					if srcColumnMeta.Scale == 0 {
-						if higherPrecision {
-							destcol[i] = int64(val)
-						} else {
-							destcol[i] = fmt.Sprintf("%d", val)
-						}
-					} else {
-						if higherPrecision {
-							f := intToBigFloat(int64(val), srcColumnMeta.Scale)
-							destcol[i] = f
-						} else {
-							destcol[i] = fmt.Sprintf("%.*f", srcColumnMeta.Scale, float64(val)/math.Pow10(int(srcColumnMeta.Scale)))
-						}
-					}
-				}
-			}
+			return arrowInt16ToValue(numericValue, rowIdx, higherPrecision, int(srcColumnMeta.Scale)), nil
 		case *array.Int8:
-			for i, val := range data.Int8Values() {
-				if !srcValue.IsNull(i) {
-					if srcColumnMeta.Scale == 0 {
-						if higherPrecision {
-							destcol[i] = int64(val)
-						} else {
-							destcol[i] = fmt.Sprintf("%d", val)
-						}
-					} else {
-						if higherPrecision {
-							f := intToBigFloat(int64(val), srcColumnMeta.Scale)
-							destcol[i] = f
-						} else {
-							destcol[i] = fmt.Sprintf("%.*f", srcColumnMeta.Scale, float64(val)/math.Pow10(int(srcColumnMeta.Scale)))
-						}
-					}
-				}
-			}
+			return arrowInt8ToValue(numericValue, rowIdx, higherPrecision, int(srcColumnMeta.Scale)), nil
 		}
-		return err
+		return nil, fmt.Errorf("unsupported data type")
 	case booleanType:
-		boolData := srcValue.(*array.Boolean)
-		for i := range destcol {
-			if !srcValue.IsNull(i) {
-				destcol[i] = boolData.Value(i)
-			}
-		}
-		return err
+		return arrowBoolToValue(srcValue.(*array.Boolean), rowIdx), nil
 	case realType:
 		// Snowflake data types that are floating-point numbers will fall in this category
 		// e.g. FLOAT/REAL/DOUBLE
-		for i, flt64 := range srcValue.(*array.Float64).Float64Values() {
-			if !srcValue.IsNull(i) {
-				destcol[i] = flt64
-			}
-		}
-		return err
-	case textType, arrayType, variantType, objectType:
+		return arrowRealToValue(srcValue.(*array.Float64), rowIdx), nil
+	case textType, arrayType, variantType:
 		strings := srcValue.(*array.String)
-		for i := range destcol {
-			if !srcValue.IsNull(i) {
-				destcol[i] = strings.Value(i)
-			}
+		if !srcValue.IsNull(rowIdx) {
+			return strings.Value(rowIdx), nil
 		}
-		return err
-	case binaryType:
-		binaryData := srcValue.(*array.Binary)
-		for i := range destcol {
-			if !srcValue.IsNull(i) {
-				destcol[i] = binaryData.Value(i)
+		return nil, nil
+	case objectType:
+		if len(srcColumnMeta.Fields) == 0 {
+			// semistructured type without schema
+			strings := srcValue.(*array.String)
+			if !srcValue.IsNull(rowIdx) {
+				return strings.Value(rowIdx), nil
 			}
+			return nil, nil
 		}
-		return err
-	case dateType:
-		for i, date32 := range srcValue.(*array.Date32).Date32Values() {
-			if !srcValue.IsNull(i) {
-				t0 := time.Unix(int64(date32)*86400, 0).UTC()
-				destcol[i] = t0
+		strings, ok := srcValue.(*array.String)
+		if ok {
+			// structured objects as json
+			if !srcValue.IsNull(rowIdx) {
+				m := make(map[string]any)
+				err := json.Unmarshal([]byte(strings.Value(rowIdx)), &m)
+				if err != nil {
+					return nil, err
+				}
+				return buildStructuredTypeRecursive(m, srcColumnMeta.Fields, params), nil
 			}
+			return nil, nil
 		}
-		return err
+		// structured objects as native arrow
+		structs := srcValue.(*array.Struct)
+		return arrowToStructuredType(structs, srcColumnMeta.Fields, loc, rowIdx, higherPrecision, params), nil
+	case binaryType:
+		return arrowBinaryToValue(srcValue.(*array.Binary), rowIdx), nil
+	case dateType:
+		return arrowDateToValue(srcValue.(*array.Date32), rowIdx), nil
 	case timeType:
-		t0 := time.Time{}
-		if srcValue.DataType().ID() == arrow.INT64 {
-			for i, i64 := range srcValue.(*array.Int64).Int64Values() {
-				if !srcValue.IsNull(i) {
-					destcol[i] = t0.Add(time.Duration(i64 * int64(math.Pow10(9-int(srcColumnMeta.Scale)))))
-				}
-			}
-		} else {
-			for i, i32 := range srcValue.(*array.Int32).Int32Values() {
-				if !srcValue.IsNull(i) {
-					destcol[i] = t0.Add(time.Duration(int64(i32) * int64(math.Pow10(9-int(srcColumnMeta.Scale)))))
-				}
+		return arrowTimeToValue(srcValue, rowIdx, int(srcColumnMeta.Scale)), nil
+	case timestampNtzType, timestampLtzType, timestampTzType:
+		v := arrowSnowflakeTimestampToTime(srcValue, snowflakeType, int(srcColumnMeta.Scale), rowIdx, loc)
+		if v != nil {
+			return *v, nil
+		}
+		return nil, nil
+	}
+
+	return nil, fmt.Errorf("unsupported data type")
+}
+
+func arrowToStructuredType(structs *array.Struct, fieldMetadata []fieldMetadata, loc *time.Location, rowIdx int, higherPrecision bool, params map[string]*string) *structuredType {
+	m := make(map[string]any)
+	for colIdx := 0; colIdx < structs.NumField(); colIdx++ {
+		var v any
+		switch getSnowflakeType(fieldMetadata[colIdx].Type) {
+		case fixedType:
+			v = structs.Field(colIdx).ValueStr(rowIdx)
+			switch structs.Field(colIdx).(type) {
+			case *array.Decimal128:
+				v = arrowDecimal128ToValue(structs.Field(colIdx).(*array.Decimal128), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
+			case *array.Int64:
+				v = arrowInt64ToValue(structs.Field(colIdx).(*array.Int64), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
+			case *array.Int32:
+				v = arrowInt32ToValue(structs.Field(colIdx).(*array.Int32), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
+			case *array.Int16:
+				v = arrowInt16ToValue(structs.Field(colIdx).(*array.Int16), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
+			case *array.Int8:
+				v = arrowInt8ToValue(structs.Field(colIdx).(*array.Int8), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
 			}
+		case booleanType:
+			v = arrowBoolToValue(structs.Field(colIdx).(*array.Boolean), rowIdx)
+		case realType:
+			v = arrowRealToValue(structs.Field(colIdx).(*array.Float64), rowIdx)
+		case binaryType:
+			v = arrowBinaryToValue(structs.Field(colIdx).(*array.Binary), rowIdx)
+		case dateType:
+			v = arrowDateToValue(structs.Field(colIdx).(*array.Date32), rowIdx)
+		case timeType:
+			v = arrowTimeToValue(structs.Field(colIdx), rowIdx, fieldMetadata[colIdx].Scale)
+		case textType:
+			v = structs.Field(colIdx).(*array.String).Value(rowIdx)
+		case timestampLtzType, timestampTzType, timestampNtzType:
+			ptr := arrowSnowflakeTimestampToTime(structs.Field(colIdx), getSnowflakeType(fieldMetadata[colIdx].Type), fieldMetadata[colIdx].Scale, rowIdx, loc)
+			v = *ptr
+		case objectType:
+			v = arrowToStructuredType(structs.Field(colIdx).(*array.Struct), fieldMetadata[colIdx].Fields, loc, rowIdx, higherPrecision, params)
 		}
-		return err
-	case timestampNtzType, timestampLtzType, timestampTzType:
-		for i := range destcol {
-			var ts = arrowSnowflakeTimestampToTime(srcValue, snowflakeType, int(srcColumnMeta.Scale), i, loc)
-			if ts != nil {
-				destcol[i] = *ts
+		m[fieldMetadata[colIdx].Name] = v
+	}
+	return &structuredType{
+		values:        m,
+		fieldMetadata: fieldMetadata,
+		params:        params,
+	}
+}
+
+func arrowDecimal128ToValue(srcValue *array.Decimal128, rowIdx int, higherPrecision bool, scale int) snowflakeValue {
+	if !srcValue.IsNull(rowIdx) {
+		num := srcValue.Value(rowIdx)
+		if scale == 0 {
+			if higherPrecision {
+				return num.BigInt()
 			}
+			return num.ToString(0)
+		}
+		f := decimalToBigFloat(num, int64(scale))
+		if higherPrecision {
+			return f
+		}
+		return fmt.Sprintf("%.*f", scale, f)
+	}
+	return nil
+}
+
+func arrowInt64ToValue(srcValue *array.Int64, rowIdx int, higherPrecision bool, scale int) snowflakeValue {
+	if !srcValue.IsNull(rowIdx) {
+		val := srcValue.Value(rowIdx)
+		return arrowIntToValue(scale, higherPrecision, val)
+	}
+	return nil
+}
+
+func arrowInt32ToValue(srcValue *array.Int32, rowIdx int, higherPrecision bool, scale int) snowflakeValue {
+	if !srcValue.IsNull(rowIdx) {
+		val := srcValue.Value(rowIdx)
+		return arrowIntToValue(scale, higherPrecision, int64(val))
+	}
+	return nil
+}
+
+func arrowInt16ToValue(srcValue *array.Int16, rowIdx int, higherPrecision bool, scale int) snowflakeValue {
+	if !srcValue.IsNull(rowIdx) {
+		val := srcValue.Value(rowIdx)
+		return arrowIntToValue(scale, higherPrecision, int64(val))
+	}
+	return nil
+}
+
+func arrowInt8ToValue(srcValue *array.Int8, rowIdx int, higherPrecision bool, scale int) snowflakeValue {
+	if !srcValue.IsNull(rowIdx) {
+		val := srcValue.Value(rowIdx)
+		return arrowIntToValue(scale, higherPrecision, int64(val))
+	}
+	return nil
+}
+
+func arrowIntToValue(scale int, higherPrecision bool, val int64) snowflakeValue {
+	if scale == 0 {
+		if higherPrecision {
+			return int64(val)
 		}
-		return err
+		return fmt.Sprintf("%d", val)
+	}
+	if higherPrecision {
+		f := intToBigFloat(int64(val), int64(scale))
+		return f
+	}
+	return fmt.Sprintf("%.*f", scale, float64(val)/math.Pow10(int(scale)))
+}
+
+func arrowRealToValue(srcValue *array.Float64, rowIdx int) snowflakeValue {
+	if !srcValue.IsNull(rowIdx) {
+		return srcValue.Value(rowIdx)
 	}
+	return nil
+}
 
-	return fmt.Errorf("unsupported data type")
+func arrowBoolToValue(srcValue *array.Boolean, rowIdx int) snowflakeValue {
+	if !srcValue.IsNull(rowIdx) {
+		return srcValue.Value(rowIdx)
+	}
+	return nil
+}
+
+func arrowBinaryToValue(srcValue *array.Binary, rowIdx int) snowflakeValue {
+	if !srcValue.IsNull(rowIdx) {
+		return srcValue.Value(rowIdx)
+	}
+	return nil
+}
+
+func arrowDateToValue(srcValue *array.Date32, rowID int) snowflakeValue {
+	if !srcValue.IsNull(rowID) {
+		return time.Unix(int64(srcValue.Value(rowID))*86400, 0).UTC()
+	}
+	return nil
+}
+
+func arrowTimeToValue(srcValue arrow.Array, rowIdx int, scale int) snowflakeValue {
+	t0 := time.Time{}
+	if srcValue.DataType().ID() == arrow.INT64 {
+		if !srcValue.IsNull(rowIdx) {
+			return t0.Add(time.Duration(srcValue.(*array.Int64).Value(rowIdx) * int64(math.Pow10(9-scale))))
+		}
+	} else {
+		if !srcValue.IsNull(rowIdx) {
+			return t0.Add(time.Duration(int64(srcValue.(*array.Int32).Value(rowIdx)) * int64(math.Pow10(9-scale))))
+		}
+	}
+	return nil
 }
 
 type (
diff --git a/datetime.go b/datetime.go
index 222bae5..476bc2d 100644
--- a/datetime.go
+++ b/datetime.go
@@ -60,3 +60,34 @@ func snowflakeFormatToGoFormat(sfFormat string) (string, error) {
 	}
 	return res, nil
 }
+
+func dateTimeFormatByType(dateTimeType string, params map[string]*string) (string, error) {
+	paramsMutex.Lock()
+	defer paramsMutex.Unlock()
+	var format *string
+	switch dateTimeType {
+	case "date":
+		format = params["date_output_format"]
+	case "time":
+		format = params["time_output_format"]
+	case "timestamp_ltz":
+		format = params["timestamp_ltz_output_format"]
+		if format == nil || *format == "" {
+			format = params["timestamp_output_format"]
+		}
+	case "timestamp_tz":
+		format = params["timestamp_tz_output_format"]
+		if format == nil || *format == "" {
+			format = params["timestamp_output_format"]
+		}
+	case "timestamp_ntz":
+		format = params["timestamp_ntz_output_format"]
+		if format == nil || *format == "" {
+			format = params["timestamp_output_format"]
+		}
+	}
+	if format != nil {
+		return *format, nil
+	}
+	return "", errors.New("not known format parameter for " + dateTimeType)
+}
diff --git a/query.go b/query.go
index 162b452..d81f5c0 100644
--- a/query.go
+++ b/query.go
@@ -47,13 +47,21 @@ type contextData struct {
 }
 
 type execResponseRowType struct {
-	Name       string `json:"name"`
-	ByteLength int64  `json:"byteLength"`
-	Length     int64  `json:"length"`
-	Type       string `json:"type"`
-	Precision  int64  `json:"precision"`
-	Scale      int64  `json:"scale"`
-	Nullable   bool   `json:"nullable"`
+	Name       string          `json:"name"`
+	Fields     []fieldMetadata `json:"fields"`
+	ByteLength int64           `json:"byteLength"`
+	Length     int64           `json:"length"`
+	Type       string          `json:"type"`
+	Precision  int64           `json:"precision"`
+	Scale      int64           `json:"scale"`
+	Nullable   bool            `json:"nullable"`
+}
+
+type fieldMetadata struct {
+	Name   string          `json:"name"`
+	Type   string          `json:"type"`
+	Scale  int             `json:"scale"`
+	Fields []fieldMetadata `json:"fields"`
 }
 
 type execResponseChunk struct {
diff --git a/rows.go b/rows.go
index f8e5d50..95a0ea8 100644
--- a/rows.go
+++ b/rows.go
@@ -149,7 +149,8 @@ func (rows *snowflakeRows) ColumnTypeScanType(index int) reflect.Type {
 	}
 	return snowflakeTypeToGo(
 		getSnowflakeType(rows.ChunkDownloader.getRowType()[index].Type),
-		rows.ChunkDownloader.getRowType()[index].Scale)
+		rows.ChunkDownloader.getRowType()[index].Scale,
+		rows.ChunkDownloader.getRowType()[index].Fields)
 }
 
 func (rows *snowflakeRows) GetQueryID() string {
@@ -192,7 +193,7 @@ func (rows *snowflakeRows) Next(dest []driver.Value) (err error) {
 		for i, n := 0, len(row.RowSet); i < n; i++ {
 			// could move to chunk downloader so that each go routine
 			// can convert data
-			err = stringToValue(&dest[i], rows.ChunkDownloader.getRowType()[i], row.RowSet[i], rows.getLocation())
+			err = stringToValue(&dest[i], rows.ChunkDownloader.getRowType()[i], row.RowSet[i], rows.getLocation(), rows.sc.cfg.Params)
 			if err != nil {
 				return err
 			}
diff --git a/structured_type.go b/structured_type.go
new file mode 100644
index 0000000..247ccc1
--- /dev/null
+++ b/structured_type.go
@@ -0,0 +1,166 @@
+package gosnowflake
+
+import (
+	"database/sql"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"math/big"
+	"strconv"
+	"time"
+)
+
+// ObjectType Empty marker of an object used in column type ScanType function
+type ObjectType struct {
+}
+
+// StructuredType representation of structured type.
+type StructuredType interface {
+	GetString(fieldName string) (string, error)
+	GetByte(fieldName string) (byte, error)
+	GetInt16(fieldName string) (int16, error)
+	GetInt(fieldName string) (int, error)
+	GetInt64(fieldName string) (int64, error)
+	GetBigInt(fieldName string) (*big.Int, error)
+	GetFloat32(fieldName string) (float32, error)
+	GetFloat64(fieldName string) (float64, error)
+	GetBigFloat(fieldName string) (*big.Float, error)
+	GetBool(fieldName string) (bool, error)
+	GetBytes(fieldName string) ([]byte, error)
+	GetTime(fieldName string) (time.Time, error)
+	GetStruct(fieldName string, scanner sql.Scanner) (sql.Scanner, error)
+}
+
+type structuredType struct {
+	values        map[string]any
+	fieldMetadata []fieldMetadata
+	params        map[string]*string
+}
+
+func getType[T any](st *structuredType, fieldName string, emptyValue T) (T, error) {
+	v, ok := st.values[fieldName]
+	if !ok {
+		return emptyValue, errors.New("field " + fieldName + " does not exist")
+	}
+	v, ok = v.(T)
+	if !ok {
+		return emptyValue, fmt.Errorf("cannot convert field %v to %T", fieldName, emptyValue)
+	}
+	return v.(T), nil
+}
+
+func (st *structuredType) GetString(fieldName string) (string, error) {
+	return getType[string](st, fieldName, "")
+}
+
+func (st *structuredType) GetByte(fieldName string) (byte, error) {
+	b, err := st.GetInt64(fieldName)
+	if err != nil {
+		return 0, err
+	}
+	return byte(b), err
+}
+
+func (st *structuredType) GetInt16(fieldName string) (int16, error) {
+	i16, err := st.GetInt64(fieldName)
+	if err != nil {
+		return 0, err
+	}
+	return int16(i16), err
+}
+
+func (st *structuredType) GetInt(fieldName string) (int, error) {
+	i, err := st.GetInt64(fieldName)
+	if err != nil {
+		return 0, err
+	}
+	return int(i), err
+}
+
+func (st *structuredType) GetInt64(fieldName string) (int64, error) {
+	if b, err := getType[int64](st, fieldName, 0); err == nil {
+		return b, err
+	} else if b, err := getType[string](st, fieldName, ""); err == nil {
+		res, err := strconv.ParseInt(b, 10, 64)
+		return int64(res), err
+	} else if b, err := getType[float64](st, fieldName, 0); err == nil {
+		return int64(b), nil
+	} else {
+		return 0, fmt.Errorf("cannot cast column %v to byte", fieldName)
+	}
+}
+
+func (st *structuredType) GetBigInt(fieldName string) (*big.Int, error) {
+	return getType[*big.Int](st, fieldName, new(big.Int))
+}
+
+func (st *structuredType) GetFloat32(fieldName string) (float32, error) {
+	f32, err := getType[float64](st, fieldName, 0)
+	if err != nil {
+		return 0, err
+	}
+	return float32(f32), err
+}
+
+func (st *structuredType) GetFloat64(fieldName string) (float64, error) {
+	return getType[float64](st, fieldName, 0)
+}
+
+func (st *structuredType) GetBigFloat(fieldName string) (*big.Float, error) {
+	return getType[*big.Float](st, fieldName, new(big.Float))
+}
+
+func (st *structuredType) GetBool(fieldName string) (bool, error) {
+	return getType[bool](st, fieldName, false)
+}
+
+func (st *structuredType) GetBytes(fieldName string) ([]byte, error) {
+	if bi, err := getType[[]byte](st, fieldName, []byte{}); err == nil {
+		return bi, nil
+	} else if bi, err := getType[string](st, fieldName, ""); err == nil {
+		return hex.DecodeString(bi)
+	}
+	return getType[[]byte](st, fieldName, []byte{})
+}
+
+func (st *structuredType) GetTime(fieldName string) (time.Time, error) {
+	s, err := getType[string](st, fieldName, "")
+	if err == nil {
+		fieldMetadata, err := st.fieldMetadataByFieldName(fieldName)
+		if err != nil {
+			return time.Time{}, err
+		}
+		format, err := dateTimeFormatByType(fieldMetadata.Type, st.params)
+		if err != nil {
+			return time.Time{}, err
+		}
+		goFormat, err := snowflakeFormatToGoFormat(format)
+		if err != nil {
+			return time.Time{}, err
+		}
+		return time.Parse(goFormat, s)
+	}
+	dt, err := getType[time.Time](st, fieldName, time.Time{})
+	if err != nil {
+		return time.Time{}, err
+	}
+	return dt, nil
+}
+
+func (st *structuredType) GetStruct(fieldName string, scanner sql.Scanner) (sql.Scanner, error) {
+	childSt, err := getType[*structuredType](st, fieldName, &structuredType{})
+	if err != nil {
+		return nil, err
+	}
+	scanner.Scan(childSt)
+	return scanner, nil
+}
+
+func (st *structuredType) fieldMetadataByFieldName(fieldName string) (fieldMetadata, error) {
+	for _, fm := range st.fieldMetadata {
+		if fm.Name == fieldName {
+			return fm, nil
+		}
+	}
+	return fieldMetadata{}, errors.New("no metadata for field " + fieldName)
+}


Test patch:
diff --git a/assert_test.go b/assert_test.go
index 6bf28c4..f24fa8d 100644
--- a/assert_test.go
+++ b/assert_test.go
@@ -47,6 +47,10 @@ func assertTrueF(t *testing.T, actual bool, descriptions ...string) {
 	fatalOnNonEmpty(t, validateEqual(actual, true, descriptions...))
 }
 
+func assertTrueE(t *testing.T, actual bool, descriptions ...string) {
+	errorOnNonEmpty(t, validateEqual(actual, true, descriptions...))
+}
+
 func assertFalseF(t *testing.T, actual bool, descriptions ...string) {
 	fatalOnNonEmpty(t, validateEqual(actual, false, descriptions...))
 }
diff --git a/converter_test.go b/converter_test.go
index a3c62b7..0b152b5 100644
--- a/converter_test.go
+++ b/converter_test.go
@@ -139,9 +139,10 @@ func TestGoTypeToSnowflake(t *testing.T) {
 }
 
 type tcSnowflakeTypeToGo struct {
-	in    snowflakeType
-	scale int64
-	out   reflect.Type
+	in     snowflakeType
+	scale  int64
+	out    reflect.Type
+	fields []fieldMetadata
 }
 
 func TestSnowflakeTypeToGo(t *testing.T) {
@@ -156,6 +157,7 @@ func TestSnowflakeTypeToGo(t *testing.T) {
 		{in: timestampNtzType, scale: 0, out: reflect.TypeOf(time.Now())},
 		{in: timestampTzType, scale: 0, out: reflect.TypeOf(time.Now())},
 		{in: objectType, scale: 0, out: reflect.TypeOf("")},
+		{in: objectType, scale: 0, fields: []fieldMetadata{{}}, out: reflect.TypeOf(ObjectType{})},
 		{in: variantType, scale: 0, out: reflect.TypeOf("")},
 		{in: arrayType, scale: 0, out: reflect.TypeOf("")},
 		{in: binaryType, scale: 0, out: reflect.TypeOf([]byte{})},
@@ -164,7 +166,7 @@ func TestSnowflakeTypeToGo(t *testing.T) {
 	}
 	for _, test := range testcases {
 		t.Run(fmt.Sprintf("%v_%v", test.in, test.out), func(t *testing.T) {
-			a := snowflakeTypeToGo(test.in, test.scale)
+			a := snowflakeTypeToGo(test.in, test.scale, test.fields)
 			if a != test.out {
 				t.Errorf("failed. in: %v, scale: %v, expected: %v, got: %v",
 					test.in, test.scale, test.out, a)
@@ -272,7 +274,7 @@ func TestStringToValue(t *testing.T) {
 			rowType = &execResponseRowType{
 				Type: tt,
 			}
-			if err = stringToValue(&dest, *rowType, &source, nil); err == nil {
+			if err = stringToValue(&dest, *rowType, &source, nil, nil); err == nil {
 				t.Errorf("should raise error. type: %v, value:%v", tt, source)
 			}
 		})
@@ -293,7 +295,7 @@ func TestStringToValue(t *testing.T) {
 				rowType = &execResponseRowType{
 					Type: tt,
 				}
-				if err = stringToValue(&dest, *rowType, &ss, nil); err == nil {
+				if err = stringToValue(&dest, *rowType, &ss, nil, nil); err == nil {
 					t.Errorf("should raise error. type: %v, value:%v", tt, source)
 				}
 			})
@@ -301,7 +303,7 @@ func TestStringToValue(t *testing.T) {
 	}
 
 	src := "1549491451.123456789"
-	if err = stringToValue(&dest, execResponseRowType{Type: "timestamp_ltz"}, &src, nil); err != nil {
+	if err = stringToValue(&dest, execResponseRowType{Type: "timestamp_ltz"}, &src, nil, nil); err != nil {
 		t.Errorf("unexpected error: %v", err)
 	} else if ts, ok := dest.(time.Time); !ok {
 		t.Errorf("expected type: 'time.Time', got '%v'", reflect.TypeOf(dest))
@@ -341,7 +343,7 @@ func TestArrayToString(t *testing.T) {
 	}
 }
 
-func TestArrowToValue(t *testing.T) {
+func TestArrowToValues(t *testing.T) {
 	dest := make([]snowflakeValue, 2)
 
 	pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
@@ -611,7 +613,7 @@ func TestArrowToValue(t *testing.T) {
 			compare: func(src interface{}, dst []snowflakeValue) int {
 				srcvs := src.([]int32)
 				for i := range srcvs {
-					if int64(srcvs[i]) != dst[i].(int64) {
+					if int64(srcvs[i]) != dst[i] {
 						return i
 					}
 				}
@@ -865,7 +867,7 @@ func TestArrowToValue(t *testing.T) {
 
 			withHigherPrecision := tc.higherPrecision
 
-			if err := arrowToValue(dest, meta, arr, localTime.Location(), withHigherPrecision); err != nil {
+			if err := arrowToValues(dest, meta, arr, localTime.Location(), withHigherPrecision, nil); err != nil { // TODO
 				t.Fatalf("error: %s", err)
 			}
 
@@ -2062,7 +2064,7 @@ func TestTimestampLTZLocation(t *testing.T) {
 		src := "1549491451.123456789"
 		var dest driver.Value
 		loc, _ := time.LoadLocation(PSTLocation)
-		if err := stringToValue(&dest, execResponseRowType{Type: "timestamp_ltz"}, &src, loc); err != nil {
+		if err := stringToValue(&dest, execResponseRowType{Type: "timestamp_ltz"}, &src, loc, nil); err != nil {
 			t.Errorf("unexpected error: %v", err)
 		}
 		ts, ok := dest.(time.Time)
@@ -2073,7 +2075,7 @@ func TestTimestampLTZLocation(t *testing.T) {
 			t.Errorf("expected location to be %v, got '%v'", loc, ts.Location())
 		}
 
-		if err := stringToValue(&dest, execResponseRowType{Type: "timestamp_ltz"}, &src, nil); err != nil {
+		if err := stringToValue(&dest, execResponseRowType{Type: "timestamp_ltz"}, &src, nil, nil); err != nil {
 			t.Errorf("unexpected error: %v", err)
 		}
 		ts, ok = dest.(time.Time)
diff --git a/driver_test.go b/driver_test.go
index 1d6662e..75c9346 100644
--- a/driver_test.go
+++ b/driver_test.go
@@ -321,6 +321,25 @@ func (dbt *DBTest) mustPrepare(query string) (stmt *sql.Stmt) {
 	return stmt
 }
 
+func (dbt *DBTest) forceJSON() {
+	dbt.mustExec(forceJSON)
+}
+
+func (dbt *DBTest) forceArrow() {
+	dbt.mustExec(forceARROW)
+}
+
+func (dbt *DBTest) forceNativeArrow() { // structured types
+	dbt.mustExec(forceARROW)
+	dbt.mustExec("alter session set ENABLE_STRUCTURED_TYPES_NATIVE_ARROW_FORMAT = true")
+	dbt.mustExec("alter session set FORCE_ENABLE_STRUCTURED_TYPES_NATIVE_ARROW_FORMAT = true")
+}
+
+func (dbt *DBTest) enableStructuredTypes() {
+	dbt.mustExec("alter session set ENABLE_STRUCTURED_TYPES_IN_CLIENT_RESPONSE = true")
+	dbt.mustExec("alter session set IGNORE_CLIENT_VESRION_IN_STRUCTURED_TYPES_RESPONSE = true")
+}
+
 type SCTest struct {
 	*testing.T
 	sc *snowflakeConn
diff --git a/rows_test.go b/rows_test.go
index fabb6c3..50b4bf6 100644
--- a/rows_test.go
+++ b/rows_test.go
@@ -104,9 +104,14 @@ func TestRowsWithoutChunkDownloader(t *testing.T) {
 	}
 	cm := []execResponseChunk{}
 	rows := new(snowflakeRows)
-	rows.sc = nil
+	sc := &snowflakeConn{
+		cfg: &Config{
+			Params: make(map[string]*string),
+		},
+	}
+	rows.sc = sc
 	rows.ChunkDownloader = &snowflakeChunkDownloader{
-		sc:                 nil,
+		sc:                 sc,
 		ctx:                context.Background(),
 		Total:              int64(len(cc)),
 		ChunkMetas:         cm,
@@ -117,7 +122,8 @@ func TestRowsWithoutChunkDownloader(t *testing.T) {
 		RowSet:             rowSetType{RowType: rt, JSON: cc},
 		QueryResultFormat:  "json",
 	}
-	rows.ChunkDownloader.start()
+	err := rows.ChunkDownloader.start()
+	assertNilF(t, err)
 	dest := make([]driver.Value, 2)
 	for i = 0; i < len(cc); i++ {
 		if err := rows.Next(dest); err != nil {
@@ -173,9 +179,14 @@ func TestRowsWithChunkDownloader(t *testing.T) {
 		cm = append(cm, execResponseChunk{URL: fmt.Sprintf("dummyURL%v", i+1), RowCount: rowsInChunk})
 	}
 	rows := new(snowflakeRows)
-	rows.sc = nil
+	sc := &snowflakeConn{
+		cfg: &Config{
+			Params: make(map[string]*string),
+		},
+	}
+	rows.sc = sc
 	rows.ChunkDownloader = &snowflakeChunkDownloader{
-		sc:            nil,
+		sc:            sc,
 		ctx:           context.Background(),
 		Total:         int64(len(cc) + numChunks*rowsInChunk),
 		ChunkMetas:    cm,
@@ -252,9 +263,14 @@ func TestRowsWithChunkDownloaderError(t *testing.T) {
 		cm = append(cm, execResponseChunk{URL: fmt.Sprintf("dummyURL%v", i+1), RowCount: rowsInChunk})
 	}
 	rows := new(snowflakeRows)
-	rows.sc = nil
+	sc := &snowflakeConn{
+		cfg: &Config{
+			Params: make(map[string]*string),
+		},
+	}
+	rows.sc = sc
 	rows.ChunkDownloader = &snowflakeChunkDownloader{
-		sc:            nil,
+		sc:            sc,
 		ctx:           context.Background(),
 		Total:         int64(len(cc) + numChunks*rowsInChunk),
 		ChunkMetas:    cm,
@@ -330,9 +346,14 @@ func TestRowsWithChunkDownloaderErrorFail(t *testing.T) {
 		cm = append(cm, execResponseChunk{URL: fmt.Sprintf("dummyURL%v", i+1), RowCount: rowsInChunk})
 	}
 	rows := new(snowflakeRows)
-	rows.sc = nil
+	sc := &snowflakeConn{
+		cfg: &Config{
+			Params: make(map[string]*string),
+		},
+	}
+	rows.sc = sc
 	rows.ChunkDownloader = &snowflakeChunkDownloader{
-		sc:            nil,
+		sc:            sc,
 		ctx:           context.Background(),
 		Total:         int64(len(cc) + numChunks*rowsInChunk),
 		ChunkMetas:    cm,
diff --git a/structured_type_test.go b/structured_type_test.go
new file mode 100644
index 0000000..78946b6
--- /dev/null
+++ b/structured_type_test.go
@@ -0,0 +1,260 @@
+package gosnowflake
+
+import (
+	"context"
+	"math/big"
+	"reflect"
+	"testing"
+	"time"
+)
+
+type objectWithAllTypes struct {
+	s    string
+	b    byte
+	i16  int16
+	i    int
+	i64  int64
+	f32  float32
+	f64  float64
+	bo   bool
+	bi   []byte
+	date time.Time
+	time time.Time
+	ltz  time.Time
+	tz   time.Time
+	ntz  time.Time
+	so   simpleObject
+}
+
+func (o *objectWithAllTypes) Scan(val any) error {
+	st := val.(StructuredType)
+	var err error
+	if o.s, err = st.GetString("s"); err != nil {
+		return err
+	}
+	if o.b, err = st.GetByte("b"); err != nil {
+		return err
+	}
+	if o.i16, err = st.GetInt16("i16"); err != nil {
+		return err
+	}
+	if o.i, err = st.GetInt("i"); err != nil {
+		return err
+	}
+	if o.i64, err = st.GetInt64("i64"); err != nil {
+		return err
+	}
+	if o.f32, err = st.GetFloat32("f32"); err != nil {
+		return err
+	}
+	if o.f64, err = st.GetFloat64("f64"); err != nil {
+		return err
+	}
+	if o.bo, err = st.GetBool("bo"); err != nil {
+		return err
+	}
+	if o.bi, err = st.GetBytes("bi"); err != nil {
+		return err
+	}
+	if o.date, err = st.GetTime("date"); err != nil {
+		return err
+	}
+	if o.time, err = st.GetTime("time"); err != nil {
+		return err
+	}
+	if o.ltz, err = st.GetTime("ltz"); err != nil {
+		return err
+	}
+	if o.tz, err = st.GetTime("tz"); err != nil {
+		return err
+	}
+	if o.ntz, err = st.GetTime("ntz"); err != nil {
+		return err
+	}
+	so, err := st.GetStruct("so", &simpleObject{})
+	if err != nil {
+		return err
+	}
+	o.so = *so.(*simpleObject)
+	return nil
+}
+
+type simpleObject struct {
+	s string
+}
+
+func (so *simpleObject) Scan(val any) error {
+	st := val.(StructuredType)
+	var err error
+	if so.s, err = st.GetString("s"); err != nil {
+		return err
+	}
+	return nil
+}
+
+func skipStructuredTypesTestsOnGHActions(t *testing.T) {
+	if runningOnGithubAction() {
+		t.Skip("Structured types are not available on GH Actions")
+	}
+}
+
+func TestStructuredTypeWithAllTypes(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	warsawTz, err := time.LoadLocation("Europe/Warsaw")
+	assertNilF(t, err)
+	runDBTest(t, func(dbt *DBTest) {
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			dbt.mustExec("ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'")
+			rows := dbt.mustQuery("SELECT 1, {'s': 'some string', 'b': 1, 'i16': 2, 'i': 3, 'i64': 4, 'f32': '1.1', 'f64': 2.2, 'bo': true, 'bi': TO_BINARY('616263', 'HEX'), 'date': '2024-03-21'::DATE, 'time': '13:03:02'::TIME, 'ltz': '2021-07-21 11:22:33'::TIMESTAMP_LTZ, 'tz': '2022-08-31 13:43:22 +0200'::TIMESTAMP_TZ, 'ntz': '2023-05-22 01:17:19'::TIMESTAMP_NTZ, 'so': {'s': 'child'}}::OBJECT(s VARCHAR, b TINYINT, i16 SMALLINT, i INTEGER, i64 BIGINT, f32 FLOAT, f64 DOUBLE, bo BOOLEAN, bi BINARY, date DATE, time TIME, ltz TIMESTAMP_LTZ, tz TIMESTAMP_TZ, ntz TIMESTAMP_NTZ, so OBJECT(s VARCHAR))")
+			defer rows.Close()
+			rows.Next()
+			var ignore int
+			var res objectWithAllTypes
+			err := rows.Scan(&ignore, &res)
+			assertNilF(t, err)
+			assertEqualE(t, res.s, "some string")
+			assertEqualE(t, res.b, byte(1))
+			assertEqualE(t, res.i16, int16(2))
+			assertEqualE(t, res.i, 3)
+			assertEqualE(t, res.i64, int64(4))
+			assertEqualE(t, res.f32, float32(1.1))
+			assertEqualE(t, res.f64, 2.2)
+			assertEqualE(t, res.bo, true)
+			assertBytesEqualE(t, res.bi, []byte{'a', 'b', 'c'})
+			assertEqualE(t, res.date, time.Date(2024, time.March, 21, 0, 0, 0, 0, time.UTC))
+			assertEqualE(t, res.time.Hour(), 13)
+			assertEqualE(t, res.time.Minute(), 3)
+			assertEqualE(t, res.time.Second(), 2)
+			assertTrueE(t, res.ltz.Equal(time.Date(2021, time.July, 21, 11, 22, 33, 0, warsawTz)))
+			assertTrueE(t, res.tz.Equal(time.Date(2022, time.August, 31, 13, 43, 22, 0, warsawTz)))
+			assertTrueE(t, res.ntz.Equal(time.Date(2023, time.May, 22, 1, 17, 19, 0, time.UTC)))
+			assertEqualE(t, res.so, simpleObject{s: "child"})
+		})
+	})
+}
+
+type HigherPrecisionStruct struct {
+	i *big.Int
+	f *big.Float
+}
+
+func (hps *HigherPrecisionStruct) Scan(val any) error {
+	st := val.(StructuredType)
+	var err error
+	if hps.i, err = st.GetBigInt("i"); err != nil {
+		return err
+	}
+	if hps.f, err = st.GetBigFloat("f"); err != nil {
+		return err
+	}
+	return nil
+}
+
+func TestWithHigherPrecision(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			if format != "NATIVE_ARROW" {
+				t.Skip("JSON structured type does not support higher precision")
+			}
+			ctx := WithHigherPrecision(context.Background())
+			rows := dbt.mustQueryContext(ctx, "SELECT {'i': 10000000000000000000000000000000000000::DECIMAL(38, 0), 'f': 1.2345678901234567890123456789012345678::DECIMAL(38, 37)}::OBJECT(i DECIMAL(38, 0), f DECIMAL(38, 37))")
+			defer rows.Close()
+			rows.Next()
+			var v HigherPrecisionStruct
+			err := rows.Scan(&v)
+			assertNilF(t, err)
+			bigInt, b := new(big.Int).SetString("10000000000000000000000000000000000000", 10)
+			assertTrueF(t, b)
+			assertEqualE(t, bigInt.Cmp(v.i), 0)
+			bigFloat, b := new(big.Float).SetPrec(v.f.Prec()).SetString("1.2345678901234567890123456789012345678")
+			assertTrueE(t, b)
+			assertEqualE(t, bigFloat.Cmp(v.f), 0)
+		})
+	})
+}
+
+func TestObjectMetadata(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			rows := dbt.mustQuery("SELECT {'a': 'b'}::OBJECT(a VARCHAR) as structured_type")
+			defer rows.Close()
+			columnTypes, err := rows.ColumnTypes()
+			assertNilF(t, err)
+			assertEqualE(t, len(columnTypes), 1)
+			assertEqualE(t, columnTypes[0].ScanType(), reflect.TypeOf(ObjectType{}))
+			assertEqualE(t, columnTypes[0].DatabaseTypeName(), "OBJECT")
+			assertEqualE(t, columnTypes[0].Name(), "STRUCTURED_TYPE")
+		})
+	})
+}
+
+func TestObjectWithoutSchema(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			if format == "NATIVE_ARROW" {
+				t.Skip("Native arrow is not supported in objects without schema")
+			}
+			rows := dbt.mustQuery("SELECT {'a': 'b'}::OBJECT")
+			defer rows.Close()
+			rows.Next()
+			var v string
+			err := rows.Scan(&v)
+			assertNilF(t, err)
+			assertStringContainsE(t, v, `"a": "b"`)
+		})
+	})
+}
+
+func TestObjectWithoutSchemaMetadata(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			if format == "NATIVE_ARROW" {
+				t.Skip("Native arrow is not supported in objects without schema")
+			}
+			rows := dbt.mustQuery("SELECT {'a': 'b'}::OBJECT AS structured_type")
+			defer rows.Close()
+			columnTypes, err := rows.ColumnTypes()
+			assertNilF(t, err)
+			assertEqualE(t, len(columnTypes), 1)
+			assertEqualE(t, columnTypes[0].ScanType(), reflect.TypeOf(""))
+			assertEqualE(t, columnTypes[0].DatabaseTypeName(), "OBJECT")
+			assertEqualE(t, columnTypes[0].Name(), "STRUCTURED_TYPE")
+		})
+	})
+}
+
+func forAllStructureTypeFormats(dbt *DBTest, f func(t *testing.T, format string)) {
+	for _, tc := range []struct {
+		name        string
+		forceFormat func(test *DBTest)
+	}{
+		{
+			name: "JSON",
+			forceFormat: func(test *DBTest) {
+				dbt.forceJSON()
+			},
+		},
+		{
+			name: "ARROW",
+			forceFormat: func(test *DBTest) {
+				dbt.forceArrow()
+			},
+		},
+		{
+			name: "NATIVE_ARROW",
+			forceFormat: func(test *DBTest) {
+				dbt.forceNativeArrow()
+			},
+		},
+	} {
+		dbt.T.Run(tc.name, func(t *testing.T) {
+			tc.forceFormat(dbt)
+			dbt.enableStructuredTypes()
+			f(t, tc.name)
+		})
+	}
+}