Repository: snowflakedb/gosnowflake
Pull request #1093 (2024-04-08 10:33:38)

SNOW-1303970 Handle null values correctly in structured types
### Description

SNOW-1303970 Handling of null values in structured objects and maps

### Checklist
- [x] Created tests which fail without the change (if possible)
- [x] Extended the README / documentation, if necessary


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1093?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 `2.71739%` with `358 lines` in your changes are missing coverage. Please review.
> Project coverage is 77.03%. Comparing base [(`4f7c965`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/4f7c9657bacabffed93eba788c644fee943c0f97?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) to head [(`b7f9fb6`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1093?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/1093?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/1093?src=pr&el=tree&filepath=structured_type.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-c3RydWN0dXJlZF90eXBlLmdv) | 0.00% | [197 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1093?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/1093?src=pr&el=tree&filepath=converter.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-Y29udmVydGVyLmdv) | 1.85% | [159 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1093?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [util.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1093?src=pr&el=tree&filepath=util.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-dXRpbC5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1093?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    #1093      +/-   ##
==========================================
- Coverage   79.24%   77.03%   -2.22%     
==========================================
  Files          52       52              
  Lines       10669    10945     +276     
==========================================
- Hits         8455     8431      -24     
- Misses       2214     2514     +300     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1093?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 24a3664..57c7198 100644
--- a/arrow_chunk.go
+++ b/arrow_chunk.go
@@ -4,6 +4,7 @@ package gosnowflake
 
 import (
 	"bytes"
+	"context"
 	"encoding/base64"
 	"time"
 
@@ -19,7 +20,7 @@ type arrowResultChunk struct {
 	allocator memory.Allocator
 }
 
-func (arc *arrowResultChunk) decodeArrowChunk(rowType []execResponseRowType, highPrec bool, params map[string]*string) ([]chunkRowType, error) {
+func (arc *arrowResultChunk) decodeArrowChunk(ctx context.Context, rowType []execResponseRowType, highPrec bool, params map[string]*string) ([]chunkRowType, error) {
 	logger.Debug("Arrow Decoder")
 	var chunkRows []chunkRowType
 
@@ -36,7 +37,7 @@ func (arc *arrowResultChunk) decodeArrowChunk(rowType []execResponseRowType, hig
 
 		for colIdx, col := range columns {
 			values := make([]snowflakeValue, numRows)
-			if err := arrowToValues(values, rowType[colIdx], col, arc.loc, highPrec, params); err != nil {
+			if err := arrowToValues(ctx, values, rowType[colIdx], col, arc.loc, highPrec, params); err != nil {
 				return nil, err
 			}
 
diff --git a/chunk_downloader.go b/chunk_downloader.go
index 4fcc15a..c167e4e 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.sc.cfg.Params)
+		scd.CurrentChunk, err = firstArrowChunk.decodeArrowChunk(scd.ctx, scd.RowSet.RowType, higherPrecision, scd.sc.cfg.Params)
 		scd.CurrentChunkSize = firstArrowChunk.rowCount
 		if err != nil {
 			return err
@@ -389,10 +389,10 @@ func downloadChunkHelper(ctx context.Context, scd *snowflakeChunkDownloader, idx
 			MessageArgs: []interface{}{idx},
 		}
 	}
-	return decodeChunk(scd, idx, bufStream)
+	return decodeChunk(ctx, scd, idx, bufStream)
 }
 
-func decodeChunk(scd *snowflakeChunkDownloader, idx int, bufStream *bufio.Reader) (err error) {
+func decodeChunk(ctx context.Context, scd *snowflakeChunkDownloader, idx int, bufStream *bufio.Reader) (err error) {
 	gzipMagic, err := bufStream.Peek(2)
 	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, scd.sc.cfg.Params)
+		respd, err = arc.decodeArrowChunk(ctx, scd.RowSet.RowType, highPrec, scd.sc.cfg.Params)
 		if err != nil {
 			return err
 		}
diff --git a/converter.go b/converter.go
index 464f3d4..363639e 100644
--- a/converter.go
+++ b/converter.go
@@ -334,7 +334,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, params map[string]*string) error {
+func stringToValue(ctx context.Context, 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
@@ -445,55 +445,92 @@ func stringToValue(dest *driver.Value, srcColumnMeta execResponseRowType, srcVal
 		return nil
 	case "map":
 		var err error
-		*dest, err = jsonToMap(srcColumnMeta, *srcValue, params)
+		*dest, err = jsonToMap(ctx, srcColumnMeta, *srcValue, params)
 		return err
 	}
 	*dest = *srcValue
 	return nil
 }
 
-func jsonToMap(srcColumnMeta execResponseRowType, srcValue string, params map[string]*string) (snowflakeValue, error) {
+func jsonToMap(ctx context.Context, srcColumnMeta execResponseRowType, srcValue string, params map[string]*string) (snowflakeValue, error) {
 	keyType := srcColumnMeta.Fields[0].Type
 	switch keyType {
 	case "text":
-		return jsonToMapWithKeyType[string](srcColumnMeta, srcValue, params)
+		return jsonToMapWithKeyType[string](ctx, srcColumnMeta, srcValue, params)
 	case "fixed":
-		return jsonToMapWithKeyType[int64](srcColumnMeta, srcValue, params)
+		return jsonToMapWithKeyType[int64](ctx, srcColumnMeta, srcValue, params)
 	default:
 		return nil, fmt.Errorf("unsupported map key type: %v", keyType)
 	}
 }
 
-func jsonToMapWithKeyType[K comparable](srcColumnMeta execResponseRowType, srcValue string, params map[string]*string) (snowflakeValue, error) {
+func ifNotNullOrDefault[T any](t *T, def T) T {
+	if t == nil {
+		return def
+	}
+	return *t
+}
+
+func jsonToMapWithKeyType[K comparable](ctx context.Context, srcColumnMeta execResponseRowType, srcValue string, params map[string]*string) (snowflakeValue, error) {
+	mapValuesNullableEnabled := mapValuesNullableEnabled(ctx)
 	valueMetadata := srcColumnMeta.Fields[1]
 	switch valueMetadata.Type {
 	case "text":
+		if mapValuesNullableEnabled {
+			return buildNullValue[K](srcValue, func(v *string) sql.NullString {
+				return sql.NullString{Valid: v != nil, String: ifNotNullOrDefault(v, "")}
+			})
+		}
 		return unmarshallMap(srcValue, make(map[K]string))
 	case "boolean":
+		if mapValuesNullableEnabled {
+			return buildNullValue[K](srcValue, func(v *bool) sql.NullBool {
+				return sql.NullBool{Valid: v != nil, Bool: ifNotNullOrDefault(v, false)}
+			})
+		}
 		return unmarshallMap(srcValue, make(map[K]bool))
 	case "fixed":
 		if valueMetadata.Scale == 0 {
+			if mapValuesNullableEnabled {
+				return buildNullValue[K](srcValue, func(v *int64) sql.NullInt64 {
+					return sql.NullInt64{Valid: v != nil, Int64: ifNotNullOrDefault(v, 0)}
+				})
+			}
 			return unmarshallMap(srcValue, make(map[K]int64))
 		}
+		if mapValuesNullableEnabled {
+			return buildNullValue[K](srcValue, func(v *float64) sql.NullFloat64 {
+				return sql.NullFloat64{Valid: v != nil, Float64: ifNotNullOrDefault(v, 0)}
+			})
+		}
 		return unmarshallMap(srcValue, make(map[K]float64))
 	case "real":
+		if mapValuesNullableEnabled {
+			return buildNullValue[K](srcValue, func(v *float64) sql.NullFloat64 {
+				return sql.NullFloat64{Valid: v != nil, Float64: ifNotNullOrDefault(v, 0)}
+			})
+		}
 		return unmarshallMap(srcValue, make(map[K]float64))
 	case "binary":
-		stringMap, err := unmarshallMap(srcValue, make(map[K]string))
+		stringMap, err := unmarshallMap(srcValue, make(map[K]*string))
 		if err != nil {
 			return nil, err
 		}
 		bytesMap := make(map[K][]byte, len(stringMap))
 		for k, v := range stringMap {
-			var err error
-			bytesMap[k], err = hex.DecodeString(v)
-			if err != nil {
-				return nil, err
+			if v == nil {
+				bytesMap[k] = nil
+			} else {
+				var err error
+				bytesMap[k], err = hex.DecodeString(*v)
+				if err != nil {
+					return nil, err
+				}
 			}
 		}
 		return bytesMap, nil
 	case "date", "time", "timestamp_ntz", "timestamp_ltz", "timestamp_tz":
-		stringMap, err := unmarshallMap(srcValue, make(map[K]string))
+		stringMap, err := unmarshallMap(srcValue, make(map[K]*string))
 		if err != nil {
 			return nil, err
 		}
@@ -505,9 +542,24 @@ func jsonToMapWithKeyType[K comparable](srcColumnMeta execResponseRowType, srcVa
 		if err != nil {
 			return nil, err
 		}
+		if mapValuesNullableEnabled {
+			dateTimeMap := make(map[K]sql.NullTime, len(stringMap))
+			for k, v := range stringMap {
+				if v == nil {
+					dateTimeMap[k] = sql.NullTime{Valid: false}
+				} else {
+					t, err := time.Parse(goFormat, *v)
+					if err != nil {
+						return nil, err
+					}
+					dateTimeMap[k] = sql.NullTime{Valid: true, Time: t}
+				}
+			}
+			return dateTimeMap, nil
+		}
 		dateTimeMap := make(map[K]time.Time, len(stringMap))
 		for k, v := range stringMap {
-			if dateTimeMap[k], err = time.Parse(goFormat, v); err != nil {
+			if dateTimeMap[k], err = time.Parse(goFormat, *v); err != nil {
 				return nil, err
 			}
 		}
@@ -517,6 +569,18 @@ func jsonToMapWithKeyType[K comparable](srcColumnMeta execResponseRowType, srcVa
 	}
 }
 
+func buildNullValue[K comparable, V any, nullV any](srcValue string, buildNullV func(v *V) nullV) (snowflakeValue, error) {
+	vMap, err := unmarshallMap(srcValue, make(map[K]*V))
+	if err != nil {
+		return nil, err
+	}
+	nullVMap := make(map[K]nullV, len(vMap))
+	for k, v := range vMap {
+		nullVMap[k] = buildNullV(v)
+	}
+	return nullVMap, nil
+}
+
 func unmarshallMap[K comparable, V any](srcValue string, m map[K]V) (map[K]V, error) {
 	if err := json.Unmarshal([]byte(srcValue), &m); err != nil {
 		return nil, err
@@ -613,7 +677,7 @@ func buildStructuredArray(fieldMetadata fieldMetadata, srcValue *string, params
 
 func buildStructuredTypeRecursive(m map[string]any, fields []fieldMetadata, params map[string]*string) *structuredType {
 	for _, fm := range fields {
-		if fm.Type == "object" {
+		if fm.Type == "object" && m[fm.Name] != nil {
 			m[fm.Name] = buildStructuredTypeRecursive(m[fm.Name].(map[string]any), fm.Fields, params)
 		}
 	}
@@ -720,6 +784,7 @@ 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 arrowToValues(
+	ctx context.Context,
 	destcol []snowflakeValue,
 	srcColumnMeta execResponseRowType,
 	srcValue arrow.Array,
@@ -734,14 +799,14 @@ func arrowToValues(
 
 	var err error
 	for i := range destcol {
-		if destcol[i], err = arrowToValue(i, srcColumnMeta, srcValue, loc, higherPrecision, params); err != nil {
+		if destcol[i], err = arrowToValue(ctx, 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) {
+func arrowToValue(ctx context.Context, 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:
@@ -843,15 +908,15 @@ func arrowToValue(rowIdx int, srcColumnMeta execResponseRowType, srcValue arrow.
 			})
 		case textType:
 			return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (string, error) {
-				return values.(*array.String).Value(j), nil
+				return arrowStringToValue(values.(*array.String), j).(string), nil
 			})
 		case booleanType:
 			return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (bool, error) {
-				return values.(*array.Boolean).Value(j), nil
+				return arrowBoolToValue(values.(*array.Boolean), j).(bool), nil
 			})
 		case binaryType:
 			return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) ([]byte, error) {
-				return values.(*array.Binary).Value(j), nil
+				return arrowBinaryToValue(values.(*array.Binary), j).([]byte), nil
 			})
 		case dateType:
 			return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (time.Time, error) {
@@ -863,7 +928,11 @@ func arrowToValue(rowIdx int, srcColumnMeta execResponseRowType, srcValue arrow.
 			})
 		case timestampNtzType, timestampLtzType, timestampTzType:
 			return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (time.Time, error) {
-				return *arrowSnowflakeTimestampToTime(values, snowflakeType, srcColumnMeta.Fields[0].Scale, int(j), loc), nil
+				ptr := arrowSnowflakeTimestampToTime(values, snowflakeType, srcColumnMeta.Fields[0].Scale, int(j), loc)
+				if ptr != nil {
+					return *ptr, nil
+				}
+				return time.Time{}, nil
 			})
 		case objectType:
 			return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (*structuredType, error) {
@@ -908,7 +977,7 @@ func arrowToValue(rowIdx int, srcColumnMeta execResponseRowType, srcValue arrow.
 		if ok {
 			// structured map as json
 			if !srcValue.IsNull(rowIdx) {
-				return jsonToMap(srcColumnMeta, strings.Value(rowIdx), params)
+				return jsonToMap(ctx, srcColumnMeta, strings.Value(rowIdx), params)
 			}
 		} else {
 			// structured map as native arrow
@@ -924,7 +993,7 @@ func arrowToValue(rowIdx int, srcColumnMeta execResponseRowType, srcValue arrow.
 				keyFunc := func(j int) (string, error) {
 					return keys.(*array.String).Value(j), nil
 				}
-				return buildStructuredMapFromArrow(rowIdx, srcColumnMeta, offsets, keyFunc, items, higherPrecision, loc)
+				return buildStructuredMapFromArrow(ctx, rowIdx, srcColumnMeta, offsets, keyFunc, items, higherPrecision, loc)
 			case "fixed":
 				keyFunc := func(j int) (int64, error) {
 					k, err := extractInt64(keys, int(j))
@@ -933,7 +1002,7 @@ func arrowToValue(rowIdx int, srcColumnMeta execResponseRowType, srcValue arrow.
 					}
 					return k, nil
 				}
-				return buildStructuredMapFromArrow(rowIdx, srcColumnMeta, offsets, keyFunc, items, higherPrecision, loc)
+				return buildStructuredMapFromArrow(ctx, rowIdx, srcColumnMeta, offsets, keyFunc, items, higherPrecision, loc)
 			}
 		}
 	case binaryType:
@@ -981,26 +1050,62 @@ func extractInt64(values arrow.Array, j int) (int64, error) {
 	return 0, fmt.Errorf("unsupported map type: %T", values.DataType().Name())
 }
 
-func buildStructuredMapFromArrow[K comparable](rowIdx int, srcColumnMeta execResponseRowType, offsets []int32, keyFunc func(j int) (K, error), items arrow.Array, higherPrecision bool, loc *time.Location) (snowflakeValue, error) {
+func buildStructuredMapFromArrow[K comparable](ctx context.Context, rowIdx int, srcColumnMeta execResponseRowType, offsets []int32, keyFunc func(j int) (K, error), items arrow.Array, higherPrecision bool, loc *time.Location) (snowflakeValue, error) {
+	mapNullValuesEnabled := mapValuesNullableEnabled(ctx)
 	switch srcColumnMeta.Fields[1].Type {
 	case "text":
+		if mapNullValuesEnabled {
+			return mapStructuredMapNativeArrowRows(make(map[K]sql.NullString), offsets, rowIdx, keyFunc, func(j int) (sql.NullString, error) {
+				if items.IsNull(j) {
+					return sql.NullString{Valid: false}, nil
+				}
+				return sql.NullString{Valid: true, String: items.(*array.String).Value(j)}, nil
+			})
+		}
 		return mapStructuredMapNativeArrowRows(make(map[K]string), offsets, rowIdx, keyFunc, func(j int) (string, error) {
 			return items.(*array.String).Value(j), nil
 		})
 	case "boolean":
+		if mapNullValuesEnabled {
+			return mapStructuredMapNativeArrowRows(make(map[K]sql.NullBool), offsets, rowIdx, keyFunc, func(j int) (sql.NullBool, error) {
+				if items.IsNull(j) {
+					return sql.NullBool{Valid: false}, nil
+				}
+				return sql.NullBool{Valid: true, Bool: items.(*array.Boolean).Value(j)}, nil
+			})
+		}
 		return mapStructuredMapNativeArrowRows(make(map[K]bool), offsets, rowIdx, keyFunc, func(j int) (bool, error) {
 			return items.(*array.Boolean).Value(j), nil
 		})
 	case "fixed":
 		if higherPrecision && srcColumnMeta.Fields[1].Scale == 0 {
 			return mapStructuredMapNativeArrowRows(make(map[K]*big.Int), offsets, rowIdx, keyFunc, func(j int) (*big.Int, error) {
+				if items.IsNull(j) {
+					return nil, nil
+				}
 				return mapStructuredMapNativeArrowFixedValue[*big.Int](srcColumnMeta, j, items, higherPrecision, nil)
 			})
 		} else if higherPrecision && srcColumnMeta.Fields[1].Scale != 0 {
 			return mapStructuredMapNativeArrowRows(make(map[K]*big.Float), offsets, rowIdx, keyFunc, func(j int) (*big.Float, error) {
+				if items.IsNull(j) {
+					return nil, nil
+				}
 				return mapStructuredMapNativeArrowFixedValue[*big.Float](srcColumnMeta, j, items, higherPrecision, nil)
 			})
 		} else if !higherPrecision && srcColumnMeta.Fields[1].Scale == 0 {
+			if mapNullValuesEnabled {
+				return mapStructuredMapNativeArrowRows(make(map[K]sql.NullInt64), offsets, rowIdx, keyFunc, func(j int) (sql.NullInt64, error) {
+					if items.IsNull(j) {
+						return sql.NullInt64{Valid: false}, nil
+					}
+					s, err := mapStructuredMapNativeArrowFixedValue[string](srcColumnMeta, j, items, higherPrecision, "")
+					if err != nil {
+						return sql.NullInt64{}, err
+					}
+					i64, err := strconv.ParseInt(s, 10, 64)
+					return sql.NullInt64{Valid: true, Int64: i64}, err
+				})
+			}
 			return mapStructuredMapNativeArrowRows(make(map[K]int64), offsets, rowIdx, keyFunc, func(j int) (int64, error) {
 				s, err := mapStructuredMapNativeArrowFixedValue[string](srcColumnMeta, j, items, higherPrecision, "")
 				if err != nil {
@@ -1009,6 +1114,19 @@ func buildStructuredMapFromArrow[K comparable](rowIdx int, srcColumnMeta execRes
 				return strconv.ParseInt(s, 10, 64)
 			})
 		} else {
+			if mapNullValuesEnabled {
+				return mapStructuredMapNativeArrowRows(make(map[K]sql.NullFloat64), offsets, rowIdx, keyFunc, func(j int) (sql.NullFloat64, error) {
+					if items.IsNull(j) {
+						return sql.NullFloat64{Valid: false}, nil
+					}
+					s, err := mapStructuredMapNativeArrowFixedValue[string](srcColumnMeta, j, items, higherPrecision, "")
+					if err != nil {
+						return sql.NullFloat64{}, err
+					}
+					f64, err := strconv.ParseFloat(s, 64)
+					return sql.NullFloat64{Valid: true, Float64: f64}, err
+				})
+			}
 			return mapStructuredMapNativeArrowRows(make(map[K]float64), offsets, rowIdx, keyFunc, func(j int) (float64, error) {
 				s, err := mapStructuredMapNativeArrowFixedValue[string](srcColumnMeta, j, items, higherPrecision, "")
 				if err != nil {
@@ -1018,29 +1136,55 @@ func buildStructuredMapFromArrow[K comparable](rowIdx int, srcColumnMeta execRes
 			})
 		}
 	case "real":
+		if mapNullValuesEnabled {
+			return mapStructuredMapNativeArrowRows(make(map[K]sql.NullFloat64), offsets, rowIdx, keyFunc, func(j int) (sql.NullFloat64, error) {
+				if items.IsNull(j) {
+					return sql.NullFloat64{Valid: false}, nil
+				}
+				f64 := items.(*array.Float64).Value(j)
+				return sql.NullFloat64{Valid: true, Float64: f64}, nil
+			})
+		}
 		return mapStructuredMapNativeArrowRows(make(map[K]float64), offsets, rowIdx, keyFunc, func(j int) (float64, error) {
-			return items.(*array.Float64).Value(j), nil
+			return arrowRealToValue(items.(*array.Float64), j).(float64), nil
 		})
 	case "binary":
 		return mapStructuredMapNativeArrowRows(make(map[K][]byte), offsets, rowIdx, keyFunc, func(j int) ([]byte, error) {
-			return items.(*array.Binary).Value(j), nil
+			if items.IsNull(j) {
+				return nil, nil
+			}
+			return arrowBinaryToValue(items.(*array.Binary), j).([]byte), nil
 		})
 	case "date":
-		return mapStructuredMapNativeArrowRows(make(map[K]time.Time), offsets, rowIdx, keyFunc, func(j int) (time.Time, error) {
-			return arrowDateToValue(items.(*array.Date32), j).(time.Time), nil
+		return buildTimeFromNativeArrowArray(mapNullValuesEnabled, offsets, rowIdx, keyFunc, items, func(j int) time.Time {
+			return arrowDateToValue(items.(*array.Date32), j).(time.Time)
 		})
 	case "time":
-		return mapStructuredMapNativeArrowRows(make(map[K]time.Time), offsets, rowIdx, keyFunc, func(j int) (time.Time, error) {
-			return arrowTimeToValue(items, j, srcColumnMeta.Fields[1].Scale).(time.Time), nil
+		return buildTimeFromNativeArrowArray(mapNullValuesEnabled, offsets, rowIdx, keyFunc, items, func(j int) time.Time {
+			return arrowTimeToValue(items, j, srcColumnMeta.Fields[1].Scale).(time.Time)
 		})
 	case "timestamp_ltz", "timestamp_ntz", "timestamp_tz":
-		return mapStructuredMapNativeArrowRows(make(map[K]time.Time), offsets, rowIdx, keyFunc, func(j int) (time.Time, error) {
-			return *arrowSnowflakeTimestampToTime(items, getSnowflakeType(srcColumnMeta.Fields[1].Type), srcColumnMeta.Fields[1].Scale, j, loc), nil
+		return buildTimeFromNativeArrowArray(mapNullValuesEnabled, offsets, rowIdx, keyFunc, items, func(j int) time.Time {
+			return *arrowSnowflakeTimestampToTime(items, getSnowflakeType(srcColumnMeta.Fields[1].Type), srcColumnMeta.Fields[1].Scale, j, loc)
 		})
 	}
 	return nil, errors.New("Unsupported map value: " + srcColumnMeta.Fields[1].Type)
 }
 
+func buildTimeFromNativeArrowArray[K comparable](mapNullValuesEnabled bool, offsets []int32, rowIdx int, keyFunc func(j int) (K, error), items arrow.Array, buildTime func(j int) time.Time) (snowflakeValue, error) {
+	if mapNullValuesEnabled {
+		return mapStructuredMapNativeArrowRows(make(map[K]sql.NullTime), offsets, rowIdx, keyFunc, func(j int) (sql.NullTime, error) {
+			if items.IsNull(j) {
+				return sql.NullTime{Valid: false}, nil
+			}
+			return sql.NullTime{Valid: true, Time: buildTime(j)}, nil
+		})
+	}
+	return mapStructuredMapNativeArrowRows(make(map[K]time.Time), offsets, rowIdx, keyFunc, func(j int) (time.Time, error) {
+		return buildTime(j), nil
+	})
+}
+
 func mapStructuredMapNativeArrowFixedValue[V any](srcColumnMeta execResponseRowType, j int, items arrow.Array, higherPrecision bool, defaultValue V) (V, error) {
 	v, err := extractNumberFromArrow(&items, j, higherPrecision, srcColumnMeta.Fields[1].Scale)
 	if err != nil {
@@ -1108,11 +1252,16 @@ func arrowToStructuredType(structs *array.Struct, fieldMetadata []fieldMetadata,
 		case timeType:
 			v = arrowTimeToValue(structs.Field(colIdx), rowIdx, fieldMetadata[colIdx].Scale)
 		case textType:
-			v = structs.Field(colIdx).(*array.String).Value(rowIdx)
+			v = arrowStringToValue(structs.Field(colIdx).(*array.String), rowIdx)
 		case timestampLtzType, timestampTzType, timestampNtzType:
-			v = *arrowSnowflakeTimestampToTime(structs.Field(colIdx), getSnowflakeType(fieldMetadata[colIdx].Type), fieldMetadata[colIdx].Scale, rowIdx, loc)
+			ptr := arrowSnowflakeTimestampToTime(structs.Field(colIdx), getSnowflakeType(fieldMetadata[colIdx].Type), fieldMetadata[colIdx].Scale, rowIdx, loc)
+			if ptr != nil {
+				v = *ptr
+			}
 		case objectType:
-			v = arrowToStructuredType(structs.Field(colIdx).(*array.Struct), fieldMetadata[colIdx].Fields, loc, rowIdx, higherPrecision, params)
+			if !structs.Field(colIdx).IsNull(rowIdx) {
+				v = arrowToStructuredType(structs.Field(colIdx).(*array.Struct), fieldMetadata[colIdx].Fields, loc, rowIdx, higherPrecision, params)
+			}
 		}
 		m[fieldMetadata[colIdx].Name] = v
 	}
@@ -1123,6 +1272,13 @@ func arrowToStructuredType(structs *array.Struct, fieldMetadata []fieldMetadata,
 	}
 }
 
+func arrowStringToValue(srcValue *array.String, rowIdx int) snowflakeValue {
+	if srcValue.IsNull(rowIdx) {
+		return nil
+	}
+	return srcValue.Value(rowIdx)
+}
+
 func arrowDecimal128ToValue(srcValue *array.Decimal128, rowIdx int, higherPrecision bool, scale int) snowflakeValue {
 	if !srcValue.IsNull(rowIdx) {
 		num := srcValue.Value(rowIdx)
diff --git a/rows.go b/rows.go
index 8898c56..6637f5a 100644
--- a/rows.go
+++ b/rows.go
@@ -192,7 +192,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(), rows.sc.cfg.Params)
+			err = stringToValue(rows.ctx, &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
index 4c4cd44..d59a3f8 100644
--- a/structured_type.go
+++ b/structured_type.go
@@ -1,6 +1,7 @@
 package gosnowflake
 
 import (
+	"context"
 	"database/sql"
 	"encoding/hex"
 	"encoding/json"
@@ -16,20 +17,28 @@ import (
 type ObjectType struct {
 }
 
-// StructuredType representation of structured type.
-type StructuredType interface {
+// StructuredObject representation of structured type.
+type StructuredObject interface {
 	GetString(fieldName string) (string, error)
+	GetNullString(fieldName string) (sql.NullString, error)
 	GetByte(fieldName string) (byte, error)
+	GetNullByte(fieldName string) (sql.NullByte, error)
 	GetInt16(fieldName string) (int16, error)
-	GetInt(fieldName string) (int, error)
+	GetNullInt16(fieldName string) (sql.NullInt16, error)
+	GetInt32(fieldName string) (int32, error)
+	GetNullInt32(fieldName string) (sql.NullInt32, error)
 	GetInt64(fieldName string) (int64, error)
+	GetNullInt64(fieldName string) (sql.NullInt64, error)
 	GetBigInt(fieldName string) (*big.Int, error)
 	GetFloat32(fieldName string) (float32, error)
 	GetFloat64(fieldName string) (float64, error)
+	GetNullFloat64(fieldName string) (sql.NullFloat64, error)
 	GetBigFloat(fieldName string) (*big.Float, error)
 	GetBool(fieldName string) (bool, error)
+	GetNullBool(fieldName string) (sql.NullBool, error)
 	GetBytes(fieldName string) ([]byte, error)
 	GetTime(fieldName string) (time.Time, error)
+	GetNullTime(fieldName string) (sql.NullTime, error)
 	GetStruct(fieldName string, scanner sql.Scanner) (sql.Scanner, error)
 }
 
@@ -63,62 +72,153 @@ type structuredType struct {
 	params        map[string]*string
 }
 
-func getType[T any](st *structuredType, fieldName string, emptyValue T) (T, error) {
+func getType[T any](st *structuredType, fieldName string, emptyValue T) (T, bool, error) {
 	v, ok := st.values[fieldName]
 	if !ok {
-		return emptyValue, errors.New("field " + fieldName + " does not exist")
+		return emptyValue, false, errors.New("field " + fieldName + " does not exist")
+	}
+	if v == nil {
+		return emptyValue, true, nil
 	}
 	v, ok = v.(T)
 	if !ok {
-		return emptyValue, fmt.Errorf("cannot convert field %v to %T", fieldName, emptyValue)
+		return emptyValue, false, fmt.Errorf("cannot convert field %v to %T", fieldName, emptyValue)
 	}
-	return v.(T), nil
+	return v.(T), false, nil
 }
 
 func (st *structuredType) GetString(fieldName string) (string, error) {
-	return getType[string](st, fieldName, "")
+	nullString, err := st.GetNullString(fieldName)
+	if err != nil {
+		return "", err
+	}
+	if !nullString.Valid {
+		return "", fmt.Errorf("nil value for %v, use GetNullString instead", fieldName)
+	}
+	return nullString.String, nil
+}
+
+func (st *structuredType) GetNullString(fieldName string) (sql.NullString, error) {
+	s, wasNil, err := getType[string](st, fieldName, "")
+	if err != nil {
+		return sql.NullString{}, err
+	}
+	if wasNil {
+		return sql.NullString{Valid: false}, err
+	}
+	return sql.NullString{Valid: true, String: s}, nil
 }
 
 func (st *structuredType) GetByte(fieldName string) (byte, error) {
-	b, err := st.GetInt64(fieldName)
+	nullByte, err := st.GetNullByte(fieldName)
 	if err != nil {
-		return 0, err
+		return 0, nil
 	}
-	return byte(b), err
+	if !nullByte.Valid {
+		return 0, fmt.Errorf("nil value for %v, use GetNullByte instead", fieldName)
+	}
+	return nullByte.Byte, nil
+}
+
+func (st *structuredType) GetNullByte(fieldName string) (sql.NullByte, error) {
+	b, err := st.GetNullInt64(fieldName)
+	if err != nil {
+		return sql.NullByte{}, err
+	}
+	if !b.Valid {
+		return sql.NullByte{Valid: false}, nil
+	}
+	return sql.NullByte{Valid: true, Byte: byte(b.Int64)}, nil
 }
 
 func (st *structuredType) GetInt16(fieldName string) (int16, error) {
-	i16, err := st.GetInt64(fieldName)
+	nullInt16, err := st.GetNullInt16(fieldName)
 	if err != nil {
-		return 0, err
+		return 0, nil
+	}
+	if !nullInt16.Valid {
+		return 0, fmt.Errorf("nil value for %v, use GetNullInt16 instead", fieldName)
 	}
-	return int16(i16), err
+	return nullInt16.Int16, nil
 }
 
-func (st *structuredType) GetInt(fieldName string) (int, error) {
-	i, err := st.GetInt64(fieldName)
+func (st *structuredType) GetNullInt16(fieldName string) (sql.NullInt16, error) {
+	b, err := st.GetNullInt64(fieldName)
 	if err != nil {
-		return 0, err
+		return sql.NullInt16{}, err
+	}
+	if !b.Valid {
+		return sql.NullInt16{Valid: false}, nil
 	}
-	return int(i), err
+	return sql.NullInt16{Valid: true, Int16: int16(b.Int64)}, nil
+}
+
+func (st *structuredType) GetInt32(fieldName string) (int32, error) {
+	nullInt32, err := st.GetNullInt32(fieldName)
+	if err != nil {
+		return 0, nil
+	}
+	if !nullInt32.Valid {
+		return 0, fmt.Errorf("nil value for %v, use GetNullInt32 instead", fieldName)
+	}
+	return nullInt32.Int32, nil
+}
+
+func (st *structuredType) GetNullInt32(fieldName string) (sql.NullInt32, error) {
+	b, err := st.GetNullInt64(fieldName)
+	if err != nil {
+		return sql.NullInt32{}, err
+	}
+	if !b.Valid {
+		return sql.NullInt32{Valid: false}, nil
+	}
+	return sql.NullInt32{Valid: true, Int32: int32(b.Int64)}, nil
 }
 
 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 {
-		return strconv.ParseInt(b, 10, 64)
-	} else if b, err := getType[float64](st, fieldName, 0); err == nil {
-		return int64(b), nil
-	} else if b, err := getType[json.Number](st, fieldName, ""); err == nil {
-		return strconv.ParseInt(string(b), 10, 64)
+	nullInt64, err := st.GetNullInt64(fieldName)
+	if err != nil {
+		return 0, nil
+	}
+	if !nullInt64.Valid {
+		return 0, fmt.Errorf("nil value for %v, use GetNullInt64 instead", fieldName)
+	}
+	return nullInt64.Int64, nil
+}
+
+func (st *structuredType) GetNullInt64(fieldName string) (sql.NullInt64, error) {
+	i64, wasNil, err := getType[int64](st, fieldName, 0)
+	if wasNil {
+		return sql.NullInt64{Valid: false}, err
+	}
+	if err == nil {
+		return sql.NullInt64{Valid: true, Int64: i64}, nil
+	}
+	if s, _, err := getType[string](st, fieldName, ""); err == nil {
+		i, err := strconv.ParseInt(s, 10, 64)
+		if err != nil {
+			return sql.NullInt64{Valid: false}, err
+		}
+		return sql.NullInt64{Valid: true, Int64: i}, nil
+	} else if b, _, err := getType[float64](st, fieldName, 0); err == nil {
+		return sql.NullInt64{Valid: true, Int64: int64(b)}, nil
+	} else if b, _, err := getType[json.Number](st, fieldName, ""); err == nil {
+		i, err := strconv.ParseInt(string(b), 10, 64)
+		if err != nil {
+			return sql.NullInt64{Valid: false}, err
+		}
+		return sql.NullInt64{Valid: true, Int64: i}, err
 	} else {
-		return 0, fmt.Errorf("cannot cast column %v to byte", fieldName)
+		return sql.NullInt64{Valid: false}, 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))
+	b, wasNull, err := getType[*big.Int](st, fieldName, new(big.Int))
+	if wasNull {
+		return nil, nil
+	}
+	return b, err
 }
 
 func (st *structuredType) GetFloat32(fieldName string) (float32, error) {
@@ -130,60 +230,132 @@ func (st *structuredType) GetFloat32(fieldName string) (float32, error) {
 }
 
 func (st *structuredType) GetFloat64(fieldName string) (float64, error) {
-	if b, err := getType[json.Number](st, fieldName, ""); err == nil {
-		return strconv.ParseFloat(string(b), 64)
+	nullFloat64, err := st.GetNullFloat64(fieldName)
+	if err != nil {
+		return 0, nil
+	}
+	if !nullFloat64.Valid {
+		return 0, fmt.Errorf("nil value for %v, use GetNullFloat64 instead", fieldName)
+	}
+	return nullFloat64.Float64, nil
+}
+
+func (st *structuredType) GetNullFloat64(fieldName string) (sql.NullFloat64, error) {
+	float64, wasNull, err := getType[float64](st, fieldName, 0)
+	if wasNull {
+		return sql.NullFloat64{Valid: false}, nil
+	}
+	if err == nil {
+		return sql.NullFloat64{Valid: true, Float64: float64}, nil
+	}
+	s, _, err := getType[string](st, fieldName, "")
+	if err == nil {
+		f64, err := strconv.ParseFloat(s, 64)
+		if err != nil {
+			return sql.NullFloat64{}, err
+		}
+		return sql.NullFloat64{Valid: true, Float64: f64}, err
+	}
+	jsonNumber, _, err := getType[json.Number](st, fieldName, "")
+	if err != nil {
+		return sql.NullFloat64{}, err
+	}
+	f64, err := strconv.ParseFloat(string(jsonNumber), 64)
+	if err != nil {
+		return sql.NullFloat64{}, err
 	}
-	return getType[float64](st, fieldName, 0)
+	return sql.NullFloat64{Valid: true, Float64: f64}, nil
 }
 
 func (st *structuredType) GetBigFloat(fieldName string) (*big.Float, error) {
-	return getType[*big.Float](st, fieldName, new(big.Float))
+	float, wasNull, err := getType[*big.Float](st, fieldName, new(big.Float))
+	if wasNull {
+		return nil, nil
+	}
+	return float, err
 }
 
 func (st *structuredType) GetBool(fieldName string) (bool, error) {
-	return getType[bool](st, fieldName, false)
+	nullBool, err := st.GetNullBool(fieldName)
+	if err != nil {
+		return false, err
+	}
+	if !nullBool.Valid {
+		return false, fmt.Errorf("nil value for %v, use GetNullBool instead", fieldName)
+	}
+	return nullBool.Bool, err
+}
+
+func (st *structuredType) GetNullBool(fieldName string) (sql.NullBool, error) {
+	b, wasNull, err := getType[bool](st, fieldName, false)
+	if wasNull {
+		return sql.NullBool{Valid: false}, nil
+	}
+	if err != nil {
+		return sql.NullBool{}, err
+	}
+	return sql.NullBool{Valid: true, Bool: b}, nil
 }
 
 func (st *structuredType) GetBytes(fieldName string) ([]byte, error) {
-	if bi, err := getType[[]byte](st, fieldName, []byte{}); err == nil {
+	if bi, _, err := getType[[]byte](st, fieldName, []byte{}); err == nil {
 		return bi, nil
-	} else if bi, err := getType[string](st, fieldName, ""); err == nil {
+	} else if bi, _, err := getType[string](st, fieldName, ""); err == nil {
 		return hex.DecodeString(bi)
 	}
-	return getType[[]byte](st, fieldName, []byte{})
+	bytes, _, err := getType[[]byte](st, fieldName, []byte{})
+	return bytes, err
 }
 
 func (st *structuredType) GetTime(fieldName string) (time.Time, error) {
-	s, err := getType[string](st, fieldName, "")
+	nullTime, err := st.GetNullTime(fieldName)
+	if err != nil {
+		return time.Time{}, nil
+	}
+	if !nullTime.Valid {
+		return time.Time{}, fmt.Errorf("nil value for %v, use GetNullBool instead", fieldName)
+	}
+	return nullTime.Time, nil
+}
+
+func (st *structuredType) GetNullTime(fieldName string) (sql.NullTime, error) {
+	s, wasNull, err := getType[string](st, fieldName, "")
+	if wasNull {
+		return sql.NullTime{Valid: false}, nil
+	}
 	if err == nil {
 		fieldMetadata, err := st.fieldMetadataByFieldName(fieldName)
 		if err != nil {
-			return time.Time{}, err
+			return sql.NullTime{}, err
 		}
 		format, err := dateTimeFormatByType(fieldMetadata.Type, st.params)
 		if err != nil {
-			return time.Time{}, err
+			return sql.NullTime{}, err
 		}
 		goFormat, err := snowflakeFormatToGoFormat(format)
 		if err != nil {
-			return time.Time{}, err
+			return sql.NullTime{}, err
 		}
-		return time.Parse(goFormat, s)
+		time, err := time.Parse(goFormat, s)
+		return sql.NullTime{Valid: true, Time: time}, err
 	}
-	dt, err := getType[time.Time](st, fieldName, time.Time{})
+	time, _, err := getType[time.Time](st, fieldName, time.Time{})
 	if err != nil {
-		return time.Time{}, err
+		return sql.NullTime{}, err
 	}
-	return dt, nil
+	return sql.NullTime{Valid: true, Time: time}, nil
 }
 
 func (st *structuredType) GetStruct(fieldName string, scanner sql.Scanner) (sql.Scanner, error) {
-	childSt, err := getType[*structuredType](st, fieldName, &structuredType{})
+	childSt, wasNull, err := getType[*structuredType](st, fieldName, &structuredType{})
+	if wasNull {
+		return nil, nil
+	}
 	if err != nil {
 		return nil, err
 	}
-	scanner.Scan(childSt)
-	return scanner, nil
+	err = scanner.Scan(childSt)
+	return scanner, err
 }
 
 func (st *structuredType) fieldMetadataByFieldName(fieldName string) (fieldMetadata, error) {
@@ -194,3 +366,12 @@ func (st *structuredType) fieldMetadataByFieldName(fieldName string) (fieldMetad
 	}
 	return fieldMetadata{}, errors.New("no metadata for field " + fieldName)
 }
+
+func mapValuesNullableEnabled(ctx context.Context) bool {
+	v := ctx.Value(mapValuesNullable)
+	if v == nil {
+		return false
+	}
+	d, ok := v.(bool)
+	return ok && d
+}
diff --git a/util.go b/util.go
index 876c160..c2874e0 100644
--- a/util.go
+++ b/util.go
@@ -33,6 +33,7 @@ const (
 	arrowAlloc                       contextKey = "ARROW_ALLOC"
 	arrowBatchesTimestampOption      contextKey = "ARROW_BATCHES_TIMESTAMP_OPTION"
 	queryTag                         contextKey = "QUERY_TAG"
+	mapValuesNullable                contextKey = "MAP_VALUES_NULLABLE"
 )
 
 const (
@@ -146,6 +147,12 @@ func WithQueryTag(ctx context.Context, tag string) context.Context {
 	return context.WithValue(ctx, queryTag, tag)
 }
 
+// WithMapValuesNullable changes how map values are returned.
+// Instead of simple values (like string) sql.NullXXX wrappers (like sql.NullString) are used.
+func WithMapValuesNullable(ctx context.Context) context.Context {
+	return context.WithValue(ctx, mapValuesNullable, true)
+}
+
 // Get the request ID from the context if specified, otherwise generate one
 func getOrGenerateRequestIDFromContext(ctx context.Context) UUID {
 	requestID, ok := ctx.Value(snowflakeRequestIDKey).(UUID)


Test patch:
diff --git a/converter_test.go b/converter_test.go
index d973281..8a9a0d5 100644
--- a/converter_test.go
+++ b/converter_test.go
@@ -287,7 +287,7 @@ func TestStringToValue(t *testing.T) {
 			rowType = &execResponseRowType{
 				Type: tt,
 			}
-			if err = stringToValue(&dest, *rowType, &source, nil, nil); err == nil {
+			if err = stringToValue(context.Background(), &dest, *rowType, &source, nil, nil); err == nil {
 				t.Errorf("should raise error. type: %v, value:%v", tt, source)
 			}
 		})
@@ -308,7 +308,7 @@ func TestStringToValue(t *testing.T) {
 				rowType = &execResponseRowType{
 					Type: tt,
 				}
-				if err = stringToValue(&dest, *rowType, &ss, nil, nil); err == nil {
+				if err = stringToValue(context.Background(), &dest, *rowType, &ss, nil, nil); err == nil {
 					t.Errorf("should raise error. type: %v, value:%v", tt, source)
 				}
 			})
@@ -316,7 +316,7 @@ func TestStringToValue(t *testing.T) {
 	}
 
 	src := "1549491451.123456789"
-	if err = stringToValue(&dest, execResponseRowType{Type: "timestamp_ltz"}, &src, nil, nil); err != nil {
+	if err = stringToValue(context.Background(), &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))
@@ -880,7 +880,7 @@ func TestArrowToValues(t *testing.T) {
 
 			withHigherPrecision := tc.higherPrecision
 
-			if err := arrowToValues(dest, meta, arr, localTime.Location(), withHigherPrecision, nil); err != nil { // TODO
+			if err := arrowToValues(context.Background(), dest, meta, arr, localTime.Location(), withHigherPrecision, nil); err != nil { // TODO
 				t.Fatalf("error: %s", err)
 			}
 
@@ -2077,7 +2077,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, nil); err != nil {
+		if err := stringToValue(context.Background(), &dest, execResponseRowType{Type: "timestamp_ltz"}, &src, loc, nil); err != nil {
 			t.Errorf("unexpected error: %v", err)
 		}
 		ts, ok := dest.(time.Time)
@@ -2088,7 +2088,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, nil); err != nil {
+		if err := stringToValue(context.Background(), &dest, execResponseRowType{Type: "timestamp_ltz"}, &src, nil, nil); err != nil {
 			t.Errorf("unexpected error: %v", err)
 		}
 		ts, ok = dest.(time.Time)
diff --git a/structured_type_test.go b/structured_type_test.go
index 4a783df..31954bc 100644
--- a/structured_type_test.go
+++ b/structured_type_test.go
@@ -2,6 +2,7 @@ package gosnowflake
 
 import (
 	"context"
+	"database/sql"
 	"math/big"
 	"reflect"
 	"strings"
@@ -10,25 +11,26 @@ import (
 )
 
 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
+	s         string
+	b         byte
+	i16       int16
+	i32       int32
+	i64       int64
+	f32       float32
+	f64       float64
+	nfraction 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)
+	st := val.(StructuredObject)
 	var err error
 	if o.s, err = st.GetString("s"); err != nil {
 		return err
@@ -39,7 +41,7 @@ func (o *objectWithAllTypes) Scan(val any) error {
 	if o.i16, err = st.GetInt16("i16"); err != nil {
 		return err
 	}
-	if o.i, err = st.GetInt("i"); err != nil {
+	if o.i32, err = st.GetInt32("i32"); err != nil {
 		return err
 	}
 	if o.i64, err = st.GetInt64("i64"); err != nil {
@@ -51,6 +53,9 @@ func (o *objectWithAllTypes) Scan(val any) error {
 	if o.f64, err = st.GetFloat64("f64"); err != nil {
 		return err
 	}
+	if o.nfraction, err = st.GetFloat64("nfraction"); err != nil {
+		return err
+	}
 	if o.bo, err = st.GetBool("bo"); err != nil {
 		return err
 	}
@@ -82,16 +87,16 @@ func (o *objectWithAllTypes) Scan(val any) error {
 
 type simpleObject struct {
 	s string
-	i int
+	i int32
 }
 
 func (so *simpleObject) Scan(val any) error {
-	st := val.(StructuredType)
+	st := val.(StructuredObject)
 	var err error
 	if so.s, err = st.GetString("s"); err != nil {
 		return err
 	}
-	if so.i, err = st.GetInt("i"); err != nil {
+	if so.i, err = st.GetInt32("i"); err != nil {
 		return err
 	}
 	return nil
@@ -108,9 +113,9 @@ func TestObjectWithAllTypes(t *testing.T) {
 	warsawTz, err := time.LoadLocation("Europe/Warsaw")
 	assertNilF(t, err)
 	runDBTest(t, func(dbt *DBTest) {
+		dbt.mustExec("ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'")
 		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': 9223372036854775807, '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', 'i': 9}}::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, i INTEGER))")
+			rows := dbt.mustQuery("SELECT 1, {'s': 'some string', 'b': 1, 'i16': 2, 'i32': 3, 'i64': 9223372036854775807, 'f32': '1.1', 'f64': 2.2, 'nfraction': 3.3, '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', 'i': 9}}::OBJECT(s VARCHAR, b TINYINT, i16 SMALLINT, i32 INTEGER, i64 BIGINT, f32 FLOAT, f64 DOUBLE, nfraction NUMBER(38, 19), bo BOOLEAN, bi BINARY, date DATE, time TIME, ltz TIMESTAMP_LTZ, tz TIMESTAMP_TZ, ntz TIMESTAMP_NTZ, so OBJECT(s VARCHAR, i INTEGER))")
 			defer rows.Close()
 			rows.Next()
 			var ignore int
@@ -120,10 +125,11 @@ func TestObjectWithAllTypes(t *testing.T) {
 			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.i32, int32(3))
 			assertEqualE(t, res.i64, int64(9223372036854775807))
 			assertEqualE(t, res.f32, float32(1.1))
 			assertEqualE(t, res.f64, 2.2)
+			assertEqualE(t, res.nfraction, 3.3)
 			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))
@@ -138,13 +144,150 @@ func TestObjectWithAllTypes(t *testing.T) {
 	})
 }
 
+type objectWithAllTypesNullable struct {
+	s    sql.NullString
+	b    sql.NullByte
+	i16  sql.NullInt16
+	i32  sql.NullInt32
+	i64  sql.NullInt64
+	f64  sql.NullFloat64
+	bo   sql.NullBool
+	bi   []byte
+	date sql.NullTime
+	time sql.NullTime
+	ltz  sql.NullTime
+	tz   sql.NullTime
+	ntz  sql.NullTime
+	so   *simpleObject
+}
+
+func (o *objectWithAllTypesNullable) Scan(val any) error {
+	st := val.(StructuredObject)
+	var err error
+	if o.s, err = st.GetNullString("s"); err != nil {
+		return err
+	}
+	if o.b, err = st.GetNullByte("b"); err != nil {
+		return err
+	}
+	if o.i16, err = st.GetNullInt16("i16"); err != nil {
+		return err
+	}
+	if o.i32, err = st.GetNullInt32("i32"); err != nil {
+		return err
+	}
+	if o.i64, err = st.GetNullInt64("i64"); err != nil {
+		return err
+	}
+	if o.f64, err = st.GetNullFloat64("f64"); err != nil {
+		return err
+	}
+	if o.bo, err = st.GetNullBool("bo"); err != nil {
+		return err
+	}
+	if o.bi, err = st.GetBytes("bi"); err != nil {
+		return err
+	}
+	if o.date, err = st.GetNullTime("date"); err != nil {
+		return err
+	}
+	if o.time, err = st.GetNullTime("time"); err != nil {
+		return err
+	}
+	if o.ltz, err = st.GetNullTime("ltz"); err != nil {
+		return err
+	}
+	if o.tz, err = st.GetNullTime("tz"); err != nil {
+		return err
+	}
+	if o.ntz, err = st.GetNullTime("ntz"); err != nil {
+		return err
+	}
+	so, err := st.GetStruct("so", &simpleObject{})
+	if err != nil {
+		return err
+	}
+	if so != nil {
+		o.so = so.(*simpleObject)
+	} else {
+		o.so = nil
+	}
+	return nil
+}
+
+func TestObjectWithAllTypesNullable(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	warsawTz, err := time.LoadLocation("Europe/Warsaw")
+	assertNilF(t, err)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.mustExec("ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'")
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			t.Run("null", func(t *testing.T) {
+				rows := dbt.mustQuery("select null, object_construct_keep_null('s', null, 'b', null, 'i16', null, 'i32', null, 'i64', null, 'f64', null, 'bo', null, 'bi', null, 'date', null, 'time', null, 'ltz', null, 'tz', null, 'ntz', null, 'so', null)::OBJECT(s VARCHAR, b TINYINT, i16 SMALLINT, i32 INTEGER, i64 BIGINT, f64 DOUBLE, bo BOOLEAN, bi BINARY, date DATE, time TIME, ltz TIMESTAMP_LTZ, tz TIMESTAMP_TZ, ntz TIMESTAMP_NTZ, so OBJECT(s VARCHAR, i INTEGER))")
+				defer rows.Close()
+				rows.Next()
+				var ignore sql.NullInt32
+				var res objectWithAllTypesNullable
+				err := rows.Scan(&ignore, &res)
+				assertNilF(t, err)
+				assertEqualE(t, ignore, sql.NullInt32{Valid: false})
+				assertEqualE(t, res.s, sql.NullString{Valid: false})
+				assertEqualE(t, res.b, sql.NullByte{Valid: false})
+				assertEqualE(t, res.i16, sql.NullInt16{Valid: false})
+				assertEqualE(t, res.i32, sql.NullInt32{Valid: false})
+				assertEqualE(t, res.i64, sql.NullInt64{Valid: false})
+				assertEqualE(t, res.f64, sql.NullFloat64{Valid: false})
+				assertEqualE(t, res.bo, sql.NullBool{Valid: false})
+				assertBytesEqualE(t, res.bi, nil)
+				assertEqualE(t, res.date, sql.NullTime{Valid: false})
+				assertEqualE(t, res.time, sql.NullTime{Valid: false})
+				assertEqualE(t, res.ltz, sql.NullTime{Valid: false})
+				assertEqualE(t, res.tz, sql.NullTime{Valid: false})
+				assertEqualE(t, res.ntz, sql.NullTime{Valid: false})
+				var so *simpleObject
+				assertDeepEqualE(t, res.so, so)
+			})
+			t.Run("not null", func(t *testing.T) {
+				rows := dbt.mustQuery("select 1, object_construct_keep_null('s', 'abc', 'b', 1, 'i16', 2, 'i32', 3, 'i64', 9223372036854775807, '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', 'i': 9}::OBJECT)::OBJECT(s VARCHAR, b TINYINT, i16 SMALLINT, i32 INTEGER, i64 BIGINT, f64 DOUBLE, bo BOOLEAN, bi BINARY, date DATE, time TIME, ltz TIMESTAMP_LTZ, tz TIMESTAMP_TZ, ntz TIMESTAMP_NTZ, so OBJECT(s VARCHAR, i INTEGER))")
+				defer rows.Close()
+				rows.Next()
+				var ignore sql.NullInt32
+				var res objectWithAllTypesNullable
+				err := rows.Scan(&ignore, &res)
+				assertNilF(t, err)
+				assertEqualE(t, ignore, sql.NullInt32{Valid: true, Int32: 1})
+				assertEqualE(t, res.s, sql.NullString{Valid: true, String: "abc"})
+				assertEqualE(t, res.b, sql.NullByte{Valid: true, Byte: byte(1)})
+				assertEqualE(t, res.i16, sql.NullInt16{Valid: true, Int16: int16(2)})
+				assertEqualE(t, res.i32, sql.NullInt32{Valid: true, Int32: 3})
+				assertEqualE(t, res.i64, sql.NullInt64{Valid: true, Int64: 9223372036854775807})
+				assertEqualE(t, res.f64, sql.NullFloat64{Valid: true, Float64: 2.2})
+				assertEqualE(t, res.bo, sql.NullBool{Valid: true, Bool: true})
+				assertBytesEqualE(t, res.bi, []byte{'a', 'b', 'c'})
+				assertEqualE(t, res.date, sql.NullTime{Valid: true, Time: time.Date(2024, time.March, 21, 0, 0, 0, 0, time.UTC)})
+				assertTrueE(t, res.time.Valid)
+				assertEqualE(t, res.time.Time.Hour(), 13)
+				assertEqualE(t, res.time.Time.Minute(), 3)
+				assertEqualE(t, res.time.Time.Second(), 2)
+				assertTrueE(t, res.ltz.Valid)
+				assertTrueE(t, res.ltz.Time.Equal(time.Date(2021, time.July, 21, 11, 22, 33, 0, warsawTz)))
+				assertTrueE(t, res.tz.Valid)
+				assertTrueE(t, res.tz.Time.Equal(time.Date(2022, time.August, 31, 13, 43, 22, 0, warsawTz)))
+				assertTrueE(t, res.ntz.Valid)
+				assertTrueE(t, res.ntz.Time.Equal(time.Date(2023, time.May, 22, 1, 17, 19, 0, time.UTC)))
+				assertDeepEqualE(t, res.so, &simpleObject{s: "child", i: 9})
+			})
+		})
+	})
+}
+
 type HigherPrecisionStruct struct {
 	i *big.Int
 	f *big.Float
 }
 
 func (hps *HigherPrecisionStruct) Scan(val any) error {
-	st := val.(StructuredType)
+	st := val.(StructuredObject)
 	var err error
 	if hps.i, err = st.GetBigInt("i"); err != nil {
 		return err
@@ -721,7 +864,7 @@ func TestWithHigherPrecision(t *testing.T) {
 				assertEqualE(t, columnTypes[0].Name(), "STRUCTURED_TYPE")
 			})
 			t.Run("map of string to big ints", func(t *testing.T) {
-				rows := dbt.mustQueryContext(ctx, "SELECT {'x': 10000000000000000000000000000000000000}::MAP(VARCHAR, DECIMAL(38, 0)) as structured_type")
+				rows := dbt.mustQueryContext(ctx, "SELECT object_construct_keep_null('x', 10000000000000000000000000000000000000, 'y', null)::MAP(VARCHAR, DECIMAL(38, 0)) as structured_type")
 				defer rows.Close()
 				rows.Next()
 				var v *map[string]*big.Int
@@ -730,6 +873,7 @@ func TestWithHigherPrecision(t *testing.T) {
 				bigInt, b := new(big.Int).SetString("10000000000000000000000000000000000000", 10)
 				assertTrueF(t, b)
 				assertEqualE(t, bigInt.Cmp((*v)["x"]), 0)
+				assertEqualE(t, (*v)["y"], (*big.Int)(nil))
 				columnTypes, err := rows.ColumnTypes()
 				assertNilF(t, err)
 				assertEqualE(t, len(columnTypes), 1)
@@ -738,7 +882,7 @@ func TestWithHigherPrecision(t *testing.T) {
 				assertEqualE(t, columnTypes[0].Name(), "STRUCTURED_TYPE")
 			})
 			t.Run("map of string to big floats", func(t *testing.T) {
-				rows := dbt.mustQueryContext(ctx, "SELECT {'x': 1.2345678901234567890123456789012345678}::MAP(VARCHAR, DECIMAL(38, 37)) as structured_type")
+				rows := dbt.mustQueryContext(ctx, "SELECT {'x': 1.2345678901234567890123456789012345678, 'y': null}::MAP(VARCHAR, DECIMAL(38, 37)) as structured_type")
 				defer rows.Close()
 				rows.Next()
 				var v *map[string]*big.Float
@@ -747,6 +891,7 @@ func TestWithHigherPrecision(t *testing.T) {
 				bigFloat, b := new(big.Float).SetPrec((*v)["x"].Prec()).SetString("1.2345678901234567890123456789012345678")
 				assertTrueE(t, b)
 				assertEqualE(t, bigFloat.Cmp((*v)["x"]), 0)
+				assertEqualE(t, (*v)["y"], (*big.Float)(nil))
 				columnTypes, err := rows.ColumnTypes()
 				assertNilF(t, err)
 				assertEqualE(t, len(columnTypes), 1)
@@ -813,6 +958,114 @@ func TestNullAndEmptyMaps(t *testing.T) {
 	})
 }
 
+func TestMapWithNullValues(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	warsawTz, err := time.LoadLocation("Europe/Warsaw")
+	assertNilF(t, err)
+	testcases := []struct {
+		name     string
+		query    string
+		actual   any
+		expected any
+	}{
+		{
+			name:     "string",
+			query:    "SELECT object_construct_keep_null('x', 'abc', 'y', null)::MAP(VARCHAR, VARCHAR)",
+			actual:   make(map[string]sql.NullString),
+			expected: map[string]sql.NullString{"x": {Valid: true, String: "abc"}, "y": {Valid: false}},
+		},
+		{
+			name:     "bool",
+			query:    "SELECT object_construct_keep_null('x', true, 'y', null)::MAP(VARCHAR, BOOLEAN)",
+			actual:   make(map[string]sql.NullBool),
+			expected: map[string]sql.NullBool{"x": {Valid: true, Bool: true}, "y": {Valid: false}},
+		},
+		{
+			name:     "fixed - scale == 0",
+			query:    "SELECT object_construct_keep_null('x', 1, 'y', null)::MAP(VARCHAR, BIGINT)",
+			actual:   make(map[string]sql.NullInt64),
+			expected: map[string]sql.NullInt64{"x": {Valid: true, Int64: 1}, "y": {Valid: false}},
+		},
+		{
+			name:     "fixed - scale != 0",
+			query:    "SELECT object_construct_keep_null('x', 1.1, 'y', null)::MAP(VARCHAR, NUMBER(38, 19))",
+			actual:   make(map[string]sql.NullFloat64),
+			expected: map[string]sql.NullFloat64{"x": {Valid: true, Float64: 1.1}, "y": {Valid: false}},
+		},
+		{
+			name:     "fixed - real",
+			query:    "SELECT object_construct_keep_null('x', 1.1, 'y', null)::MAP(VARCHAR, DOUBLE)",
+			actual:   make(map[string]sql.NullFloat64),
+			expected: map[string]sql.NullFloat64{"x": {Valid: true, Float64: 1.1}, "y": {Valid: false}},
+		},
+		{
+			name:     "binary",
+			query:    "SELECT object_construct_keep_null('x', TO_BINARY('616263'), 'y', null)::MAP(VARCHAR, BINARY)",
+			actual:   make(map[string][]byte),
+			expected: map[string][]byte{"x": {'a', 'b', 'c'}, "y": nil},
+		},
+		{
+			name:     "date",
+			query:    "SELECT object_construct_keep_null('x', '2024-04-05'::DATE, 'y', null)::MAP(VARCHAR, DATE)",
+			actual:   make(map[string]sql.NullTime),
+			expected: map[string]sql.NullTime{"x": {Valid: true, Time: time.Date(2024, time.April, 5, 0, 0, 0, 0, time.UTC)}, "y": {Valid: false}},
+		},
+		{
+			name:     "time",
+			query:    "SELECT object_construct_keep_null('x', '13:14:15'::TIME, 'y', null)::MAP(VARCHAR, TIME)",
+			actual:   make(map[string]sql.NullTime),
+			expected: map[string]sql.NullTime{"x": {Valid: true, Time: time.Date(1, 0, 0, 13, 14, 15, 0, time.UTC)}, "y": {Valid: false}},
+		},
+		{
+			name:     "timestamp_tz",
+			query:    "SELECT object_construct_keep_null('x', '2022-08-31 13:43:22 +0200'::TIMESTAMP_TZ, 'y', null)::MAP(VARCHAR, TIMESTAMP_TZ)",
+			actual:   make(map[string]sql.NullTime),
+			expected: map[string]sql.NullTime{"x": {Valid: true, Time: time.Date(2022, 8, 31, 13, 43, 22, 0, warsawTz)}, "y": {Valid: false}},
+		},
+		{
+			name:     "timestamp_ntz",
+			query:    "SELECT object_construct_keep_null('x', '2022-08-31 13:43:22'::TIMESTAMP_NTZ, 'y', null)::MAP(VARCHAR, TIMESTAMP_NTZ)",
+			actual:   make(map[string]sql.NullTime),
+			expected: map[string]sql.NullTime{"x": {Valid: true, Time: time.Date(2022, 8, 31, 13, 43, 22, 0, time.UTC)}, "y": {Valid: false}},
+		},
+		{
+			name:     "timestamp_tz",
+			query:    "SELECT object_construct_keep_null('x', '2022-08-31 13:43:22'::TIMESTAMP_LTZ, 'y', null)::MAP(VARCHAR, TIMESTAMP_LTZ)",
+			actual:   make(map[string]sql.NullTime),
+			expected: map[string]sql.NullTime{"x": {Valid: true, Time: time.Date(2022, 8, 31, 13, 43, 22, 0, warsawTz)}, "y": {Valid: false}},
+		},
+	}
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.mustExec("ALTER SESSION SET TIMEZONE = 'Europe/Warsaw'")
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			for _, tc := range testcases {
+				t.Run(tc.name, func(t *testing.T) {
+					rows := dbt.mustQueryContext(WithMapValuesNullable(context.Background()), tc.query)
+					defer rows.Close()
+					rows.Next()
+					err := rows.Scan(&tc.actual)
+					assertNilF(t, err)
+					if tc.name == "time" {
+						for i, nt := range tc.actual.(map[string]sql.NullTime) {
+							assertEqualE(t, nt.Valid, tc.expected.(map[string]sql.NullTime)[i].Valid)
+							assertEqualE(t, nt.Time.Hour(), tc.expected.(map[string]sql.NullTime)[i].Time.Hour())
+							assertEqualE(t, nt.Time.Minute(), tc.expected.(map[string]sql.NullTime)[i].Time.Minute())
+							assertEqualE(t, nt.Time.Second(), tc.expected.(map[string]sql.NullTime)[i].Time.Second())
+						}
+					} else if tc.name == "timestamp_tz" || tc.name == "timestamp_ltz" || tc.name == "timestamp_ntz" {
+						for i, nt := range tc.actual.(map[string]sql.NullTime) {
+							assertEqualE(t, nt.Valid, tc.expected.(map[string]sql.NullTime)[i].Valid)
+							assertTrueE(t, nt.Time.Equal(tc.expected.(map[string]sql.NullTime)[i].Time))
+						}
+					} else {
+						assertDeepEqualE(t, tc.actual, tc.expected)
+					}
+				})
+			}
+		})
+	})
+}
+
 func forAllStructureTypeFormats(dbt *DBTest, f func(t *testing.T, format string)) {
 	for _, tc := range []struct {
 		name        string