Repository: snowflakedb/gosnowflake
Pull request #1089 (2024-04-03 15:52:22)

SNOW-1256917 Implement structured maps
### Description

SNOW-1256917 Implemented structured maps

### 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/1089?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 `0%` with `237 lines` in your changes are missing coverage. Please review.
> Project coverage is 78.73%. Comparing base [(`ad5a505`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/ad5a505bb8f70601110b16c9a5e5948cba32f032?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) to head [(`c192df7`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1089?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/1089?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) | Patch % | Lines |
|---|---|---|
| [converter.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1089?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) | 0.00% | [237 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1089?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    #1089      +/-   ##
==========================================
- Coverage   80.95%   78.73%   -2.23%     
==========================================
  Files          52       52              
  Lines       10444    10663     +219     
==========================================
- Hits         8455     8395      -60     
- Misses       1989     2268     +279     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1089?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/converter.go b/converter.go
index 86d3ac4..464f3d4 100644
--- a/converter.go
+++ b/converter.go
@@ -164,11 +164,46 @@ func snowflakeTypeToGo(ctx context.Context, dbtype snowflakeType, scale int64, f
 			return reflect.TypeOf([]ObjectType{})
 		}
 		return nil
+	case mapType:
+		switch getSnowflakeType(fields[0].Type) {
+		case textType:
+			return snowflakeTypeToGoForMaps[string](ctx, fields[1])
+		case fixedType:
+			return snowflakeTypeToGoForMaps[int64](ctx, fields[1])
+		}
+		return reflect.TypeOf(map[any]any{})
 	}
 	logger.Errorf("unsupported dbtype is specified. %v", dbtype)
 	return reflect.TypeOf("")
 }
 
+func snowflakeTypeToGoForMaps[K comparable](ctx context.Context, valueMetadata fieldMetadata) reflect.Type {
+	switch getSnowflakeType(valueMetadata.Type) {
+	case textType:
+		return reflect.TypeOf(map[K]string{})
+	case fixedType:
+		if higherPrecisionEnabled(ctx) && valueMetadata.Scale == 0 {
+			return reflect.TypeOf(map[K]*big.Int{})
+		} else if higherPrecisionEnabled(ctx) && valueMetadata.Scale != 0 {
+			return reflect.TypeOf(map[K]*big.Float{})
+		} else if !higherPrecisionEnabled(ctx) && valueMetadata.Scale == 0 {
+			return reflect.TypeOf(map[K]int64{})
+		} else {
+			return reflect.TypeOf(map[K]float64{})
+		}
+	case realType:
+		return reflect.TypeOf(map[K]float64{})
+	case booleanType:
+		return reflect.TypeOf(map[K]bool{})
+	case binaryType:
+		return reflect.TypeOf(map[K][]byte{})
+	case timeType, dateType, timestampTzType, timestampNtzType, timestampLtzType:
+		return reflect.TypeOf(map[K]time.Time{})
+	}
+	logger.Errorf("unsupported dbtype is specified for map value")
+	return reflect.TypeOf("")
+}
+
 // valueToString converts arbitrary golang type to a string. This is mainly used in binding data with placeholders
 // in queries.
 func valueToString(v driver.Value, tsmode snowflakeType) (*string, error) {
@@ -408,11 +443,87 @@ func stringToValue(dest *driver.Value, srcColumnMeta execResponseRowType, srcVal
 		}
 		*dest = v
 		return nil
+	case "map":
+		var err error
+		*dest, err = jsonToMap(srcColumnMeta, *srcValue, params)
+		return err
 	}
 	*dest = *srcValue
 	return nil
 }
 
+func jsonToMap(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)
+	case "fixed":
+		return jsonToMapWithKeyType[int64](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) {
+	valueMetadata := srcColumnMeta.Fields[1]
+	switch valueMetadata.Type {
+	case "text":
+		return unmarshallMap(srcValue, make(map[K]string))
+	case "boolean":
+		return unmarshallMap(srcValue, make(map[K]bool))
+	case "fixed":
+		if valueMetadata.Scale == 0 {
+			return unmarshallMap(srcValue, make(map[K]int64))
+		}
+		return unmarshallMap(srcValue, make(map[K]float64))
+	case "real":
+		return unmarshallMap(srcValue, make(map[K]float64))
+	case "binary":
+		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
+			}
+		}
+		return bytesMap, nil
+	case "date", "time", "timestamp_ntz", "timestamp_ltz", "timestamp_tz":
+		stringMap, err := unmarshallMap(srcValue, make(map[K]string))
+		if err != nil {
+			return nil, err
+		}
+		sfFormat, err := dateTimeFormatByType(valueMetadata.Type, params)
+		if err != nil {
+			return nil, err
+		}
+		goFormat, err := snowflakeFormatToGoFormat(sfFormat)
+		if err != nil {
+			return nil, err
+		}
+		dateTimeMap := make(map[K]time.Time, len(stringMap))
+		for k, v := range stringMap {
+			if dateTimeMap[k], err = time.Parse(goFormat, v); err != nil {
+				return nil, err
+			}
+		}
+		return dateTimeMap, nil
+	default:
+		return nil, fmt.Errorf("unsupported map value type: %v", valueMetadata.Type)
+	}
+}
+
+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
+	}
+	return m, nil
+}
+
 func buildStructuredArray(fieldMetadata fieldMetadata, srcValue *string, params map[string]*string) (any, error) {
 	switch fieldMetadata.Type {
 	case "text":
@@ -688,42 +799,42 @@ func arrowToValue(rowIdx int, srcColumnMeta execResponseRowType, srcValue arrow.
 		snowflakeType := getSnowflakeType(srcColumnMeta.Fields[0].Type)
 		switch snowflakeType {
 		case fixedType:
-			switch values.(type) {
+			switch typedValues := values.(type) {
 			case *array.Decimal128:
 				if higherPrecision && srcColumnMeta.Fields[0].Scale == 0 {
 					return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (*big.Int, error) {
-						return arrowDecimal128ToValue(values.(*array.Decimal128), j, higherPrecision, srcColumnMeta.Fields[0].Scale).(*big.Int), nil
+						return arrowDecimal128ToValue(typedValues, j, higherPrecision, srcColumnMeta.Fields[0].Scale).(*big.Int), nil
 					})
 				} else if higherPrecision && srcColumnMeta.Fields[0].Scale != 0 {
 					return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (*big.Float, error) {
-						return arrowDecimal128ToValue(values.(*array.Decimal128), j, higherPrecision, srcColumnMeta.Fields[0].Scale).(*big.Float), nil
+						return arrowDecimal128ToValue(typedValues, j, higherPrecision, srcColumnMeta.Fields[0].Scale).(*big.Float), nil
 					})
 				} else if !higherPrecision && srcColumnMeta.Fields[0].Scale == 0 {
 					return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (int64, error) {
-						v := arrowDecimal128ToValue(values.(*array.Decimal128), int(j), higherPrecision, srcColumnMeta.Fields[0].Scale)
+						v := arrowDecimal128ToValue(typedValues, j, higherPrecision, srcColumnMeta.Fields[0].Scale)
 						return strconv.ParseInt(v.(string), 10, 64)
 					})
 				} else {
 					return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (float64, error) {
-						v := arrowDecimal128ToValue(values.(*array.Decimal128), int(j), higherPrecision, srcColumnMeta.Fields[0].Scale)
+						v := arrowDecimal128ToValue(typedValues, j, higherPrecision, srcColumnMeta.Fields[0].Scale)
 						return strconv.ParseFloat(v.(string), 64)
 					})
 				}
 			case *array.Int64:
 				return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (int64, error) {
-					return arrowInt64ToValue(values.(*array.Int64), int(j), higherPrecision, srcColumnMeta.Fields[0].Scale).(int64), nil
+					return arrowInt64ToValue(typedValues, j, higherPrecision, srcColumnMeta.Fields[0].Scale).(int64), nil
 				})
 			case *array.Int32:
 				return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (int32, error) {
-					return arrowInt32ToValue(values.(*array.Int32), int(j), higherPrecision, srcColumnMeta.Fields[0].Scale).(int32), nil
+					return arrowInt32ToValue(typedValues, j, higherPrecision, srcColumnMeta.Fields[0].Scale).(int32), nil
 				})
 			case *array.Int16:
 				return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (int16, error) {
-					return arrowInt16ToValue(values.(*array.Int16), int(j), higherPrecision, srcColumnMeta.Fields[0].Scale).(int16), nil
+					return arrowInt16ToValue(typedValues, j, higherPrecision, srcColumnMeta.Fields[0].Scale).(int16), nil
 				})
 			case *array.Int8:
 				return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (int8, error) {
-					return arrowInt8ToValue(values.(*array.Int8), int(j), higherPrecision, srcColumnMeta.Fields[0].Scale).(int8), nil
+					return arrowInt8ToValue(typedValues, j, higherPrecision, srcColumnMeta.Fields[0].Scale).(int8), nil
 				})
 			}
 		case realType:
@@ -740,7 +851,7 @@ func arrowToValue(rowIdx int, srcColumnMeta execResponseRowType, srcValue arrow.
 			})
 		case binaryType:
 			return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) ([]byte, error) {
-				return values.(*array.Binary).Value(int(j)), nil
+				return values.(*array.Binary).Value(j), nil
 			})
 		case dateType:
 			return mapStructuredArrayNativeArrowRows(offsets, rowIdx, func(j int) (time.Time, error) {
@@ -789,6 +900,42 @@ func arrowToValue(rowIdx int, srcColumnMeta execResponseRowType, srcValue arrow.
 		// structured objects as native arrow
 		structs := srcValue.(*array.Struct)
 		return arrowToStructuredType(structs, srcColumnMeta.Fields, loc, rowIdx, higherPrecision, params), nil
+	case mapType:
+		if srcValue.IsNull(rowIdx) {
+			return nil, nil
+		}
+		strings, ok := srcValue.(*array.String)
+		if ok {
+			// structured map as json
+			if !srcValue.IsNull(rowIdx) {
+				return jsonToMap(srcColumnMeta, strings.Value(rowIdx), params)
+			}
+		} else {
+			// structured map as native arrow
+			arrowMap := srcValue.(*array.Map)
+			if arrowMap.IsNull(rowIdx) {
+				return nil, nil
+			}
+			keys := arrowMap.Keys()
+			items := arrowMap.Items()
+			offsets := arrowMap.Offsets()
+			switch srcColumnMeta.Fields[0].Type {
+			case "text":
+				keyFunc := func(j int) (string, error) {
+					return keys.(*array.String).Value(j), nil
+				}
+				return buildStructuredMapFromArrow(rowIdx, srcColumnMeta, offsets, keyFunc, items, higherPrecision, loc)
+			case "fixed":
+				keyFunc := func(j int) (int64, error) {
+					k, err := extractInt64(keys, int(j))
+					if err != nil {
+						return 0, err
+					}
+					return k, nil
+				}
+				return buildStructuredMapFromArrow(rowIdx, srcColumnMeta, offsets, keyFunc, items, higherPrecision, loc)
+			}
+		}
 	case binaryType:
 		return arrowBinaryToValue(srcValue.(*array.Binary), rowIdx), nil
 	case dateType:
@@ -818,6 +965,119 @@ func mapStructuredArrayNativeArrowRows[T any](offsets []int32, rowIdx int, creat
 	return arr, nil
 }
 
+func extractInt64(values arrow.Array, j int) (int64, error) {
+	switch typedValues := values.(type) {
+	case *array.Decimal128:
+		return int64(typedValues.Value(j).LowBits()), nil
+	case *array.Int64:
+		return typedValues.Value(j), nil
+	case *array.Int32:
+		return int64(typedValues.Value(j)), nil
+	case *array.Int16:
+		return int64(typedValues.Value(j)), nil
+	case *array.Int8:
+		return int64(typedValues.Value(j)), nil
+	}
+	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) {
+	switch srcColumnMeta.Fields[1].Type {
+	case "text":
+		return mapStructuredMapNativeArrowRows(make(map[K]string), offsets, rowIdx, keyFunc, func(j int) (string, error) {
+			return items.(*array.String).Value(j), nil
+		})
+	case "boolean":
+		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) {
+				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) {
+				return mapStructuredMapNativeArrowFixedValue[*big.Float](srcColumnMeta, j, items, higherPrecision, nil)
+			})
+		} else if !higherPrecision && srcColumnMeta.Fields[1].Scale == 0 {
+			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 {
+					return 0, err
+				}
+				return strconv.ParseInt(s, 10, 64)
+			})
+		} else {
+			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 {
+					return 0, err
+				}
+				return strconv.ParseFloat(s, 64)
+			})
+		}
+	case "real":
+		return mapStructuredMapNativeArrowRows(make(map[K]float64), offsets, rowIdx, keyFunc, func(j int) (float64, error) {
+			return items.(*array.Float64).Value(j), nil
+		})
+	case "binary":
+		return mapStructuredMapNativeArrowRows(make(map[K][]byte), offsets, rowIdx, keyFunc, func(j int) ([]byte, error) {
+			return items.(*array.Binary).Value(j), 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
+		})
+	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
+		})
+	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 nil, errors.New("Unsupported map value: " + srcColumnMeta.Fields[1].Type)
+}
+
+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 {
+		return defaultValue, err
+	}
+	return v.(V), nil
+}
+
+func extractNumberFromArrow(values *arrow.Array, j int, higherPrecision bool, scale int) (snowflakeValue, error) {
+	switch typedValues := (*values).(type) {
+	case *array.Decimal128:
+		return arrowDecimal128ToValue(typedValues, j, higherPrecision, scale), nil
+	case *array.Int64:
+		return arrowInt64ToValue(typedValues, j, higherPrecision, scale), nil
+	case *array.Int32:
+		return arrowInt32ToValue(typedValues, j, higherPrecision, scale), nil
+	case *array.Int16:
+		return arrowInt16ToValue(typedValues, j, higherPrecision, scale), nil
+	case *array.Int8:
+		return arrowInt8ToValue(typedValues, j, higherPrecision, scale), nil
+	}
+	return 0, fmt.Errorf("unknown number type: %T", values)
+}
+
+func mapStructuredMapNativeArrowRows[K comparable, V any](m map[K]V, offsets []int32, rowIdx int, keyFunc func(j int) (K, error), itemFunc func(j int) (V, error)) (map[K]V, error) {
+	for j := offsets[rowIdx]; j < offsets[rowIdx+1]; j++ {
+		k, err := keyFunc(int(j))
+		if err != nil {
+			return nil, err
+		}
+		if m[k], err = itemFunc(int(j)); err != nil {
+			return nil, err
+		}
+	}
+	return m, nil
+}
+
 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++ {
@@ -825,17 +1085,17 @@ func arrowToStructuredType(structs *array.Struct, fieldMetadata []fieldMetadata,
 		switch getSnowflakeType(fieldMetadata[colIdx].Type) {
 		case fixedType:
 			v = structs.Field(colIdx).ValueStr(rowIdx)
-			switch structs.Field(colIdx).(type) {
+			switch typedValues := structs.Field(colIdx).(type) {
 			case *array.Decimal128:
-				v = arrowDecimal128ToValue(structs.Field(colIdx).(*array.Decimal128), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
+				v = arrowDecimal128ToValue(typedValues, rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
 			case *array.Int64:
-				v = arrowInt64ToValue(structs.Field(colIdx).(*array.Int64), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
+				v = arrowInt64ToValue(typedValues, rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
 			case *array.Int32:
-				v = arrowInt32ToValue(structs.Field(colIdx).(*array.Int32), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
+				v = arrowInt32ToValue(typedValues, rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
 			case *array.Int16:
-				v = arrowInt16ToValue(structs.Field(colIdx).(*array.Int16), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
+				v = arrowInt16ToValue(typedValues, rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
 			case *array.Int8:
-				v = arrowInt8ToValue(structs.Field(colIdx).(*array.Int8), rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
+				v = arrowInt8ToValue(typedValues, rowIdx, higherPrecision, fieldMetadata[colIdx].Scale)
 			}
 		case booleanType:
 			v = arrowBoolToValue(structs.Field(colIdx).(*array.Boolean), rowIdx)
@@ -850,8 +1110,7 @@ func arrowToStructuredType(structs *array.Struct, fieldMetadata []fieldMetadata,
 		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
+			v = *arrowSnowflakeTimestampToTime(structs.Field(colIdx), getSnowflakeType(fieldMetadata[colIdx].Type), fieldMetadata[colIdx].Scale, rowIdx, loc)
 		case objectType:
 			v = arrowToStructuredType(structs.Field(colIdx).(*array.Struct), fieldMetadata[colIdx].Fields, loc, rowIdx, higherPrecision, params)
 		}
diff --git a/datatype.go b/datatype.go
index db61a90..1245054 100644
--- a/datatype.go
+++ b/datatype.go
@@ -23,6 +23,7 @@ const (
 	timestampTzType
 	objectType
 	arrayType
+	mapType
 	binaryType
 	timeType
 	booleanType
@@ -44,6 +45,7 @@ var snowflakeToDriverType = map[string]snowflakeType{
 	"TIMESTAMP_TZ":  timestampTzType,
 	"OBJECT":        objectType,
 	"ARRAY":         arrayType,
+	"MAP":           mapType,
 	"BINARY":        binaryType,
 	"TIME":          timeType,
 	"BOOLEAN":       booleanType,


Test patch:
diff --git a/structured_type_test.go b/structured_type_test.go
index 9bd2214..4a783df 100644
--- a/structured_type_test.go
+++ b/structured_type_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"math/big"
 	"reflect"
+	"strings"
 	"testing"
 	"time"
 )
@@ -236,14 +237,14 @@ func TestArrayAndMetadata(t *testing.T) {
 					actual:    []float64{},
 				},
 				{
-					name:      "number - fixed",
+					name:      "number - fixed integer",
 					query:     "SELECT ARRAY_CONSTRUCT(1, 2)::ARRAY(NUMBER(38, 0)) as structured_type UNION SELECT ARRAY_CONSTRUCT(3)::ARRAY(NUMBER(38, 0))",
 					expected1: []int64{1, 2},
 					expected2: []int64{3},
 					actual:    []int64{},
 				},
 				{
-					name:      "number - real",
+					name:      "number - fixed fraction",
 					query:     "SELECT ARRAY_CONSTRUCT(1.1, 2.2)::ARRAY(NUMBER(38, 19)) as structured_type UNION SELECT ARRAY_CONSTRUCT()::ARRAY(NUMBER(38, 19))",
 					expected1: []float64{1.1, 2.2},
 					expected2: []float64{},
@@ -425,6 +426,238 @@ func TestArrayOfObjects(t *testing.T) {
 	})
 }
 
+func TestMapAndMetadata(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'")
+		testcases := []struct {
+			name      string
+			query     string
+			expected1 any
+			expected2 any
+			actual    any
+		}{
+			{
+				name:      "string string",
+				query:     "SELECT {'a': 'x', 'b': 'y'}::MAP(VARCHAR, VARCHAR) as structured_type UNION SELECT {'c': 'z'}::MAP(VARCHAR, VARCHAR)",
+				expected1: map[string]string{"a": "x", "b": "y"},
+				expected2: map[string]string{"c": "z"},
+				actual:    make(map[string]string),
+			},
+			{
+				name:      "integer string",
+				query:     "SELECT {'1': 'x', '2': 'y'}::MAP(INTEGER, VARCHAR) as structured_type UNION SELECT {'3': 'z'}::MAP(INTEGER, VARCHAR)",
+				expected1: map[int64]string{int64(1): "x", int64(2): "y"},
+				expected2: map[int64]string{int64(3): "z"},
+				actual:    make(map[int64]string),
+			},
+			{
+				name:      "string bool",
+				query:     "SELECT {'a': true, 'b': false}::MAP(VARCHAR, BOOLEAN) as structured_type UNION SELECT {'c': true}::MAP(VARCHAR, BOOLEAN)",
+				expected1: map[string]bool{"a": true, "b": false},
+				expected2: map[string]bool{"c": true},
+				actual:    make(map[string]bool),
+			},
+			{
+				name:      "integer bool",
+				query:     "SELECT {'1': true, '2': false}::MAP(INTEGER, BOOLEAN) as structured_type UNION SELECT {'3': true}::MAP(INTEGER, BOOLEAN)",
+				expected1: map[int64]bool{int64(1): true, int64(2): false},
+				expected2: map[int64]bool{int64(3): true},
+				actual:    make(map[int64]bool),
+			},
+			{
+				name:      "string integer",
+				query:     "SELECT {'a': 11, 'b': 22}::MAP(VARCHAR, INTEGER) as structured_type UNION SELECT {'c': 33}::MAP(VARCHAR, INTEGER)",
+				expected1: map[string]int64{"a": 11, "b": 22},
+				expected2: map[string]int64{"c": 33},
+				actual:    make(map[string]int64),
+			},
+			{
+				name:      "integer integer",
+				query:     "SELECT {'1': 11, '2': 22}::MAP(INTEGER, INTEGER) as structured_type UNION SELECT {'3': 33}::MAP(INTEGER, INTEGER)",
+				expected1: map[int64]int64{int64(1): int64(11), int64(2): int64(22)},
+				expected2: map[int64]int64{int64(3): int64(33)},
+				actual:    make(map[int64]int64),
+			},
+			{
+				name:      "string double",
+				query:     "SELECT {'a': 11.1, 'b': 22.2}::MAP(VARCHAR, DOUBLE) as structured_type UNION SELECT {'c': 33.3}::MAP(VARCHAR, DOUBLE)",
+				expected1: map[string]float64{"a": 11.1, "b": 22.2},
+				expected2: map[string]float64{"c": 33.3},
+				actual:    make(map[string]float64),
+			},
+			{
+				name:      "integer double",
+				query:     "SELECT {'1': 11.1, '2': 22.2}::MAP(INTEGER, DOUBLE) as structured_type UNION SELECT {'3': 33.3}::MAP(INTEGER, DOUBLE)",
+				expected1: map[int64]float64{int64(1): 11.1, int64(2): 22.2},
+				expected2: map[int64]float64{int64(3): 33.3},
+				actual:    make(map[int64]float64),
+			},
+			{
+				name:      "string number integer",
+				query:     "SELECT {'a': 11, 'b': 22}::MAP(VARCHAR, NUMBER(38, 0)) as structured_type UNION SELECT {'c': 33}::MAP(VARCHAR, NUMBER(38, 0))",
+				expected1: map[string]int64{"a": 11, "b": 22},
+				expected2: map[string]int64{"c": 33},
+				actual:    make(map[string]int64),
+			},
+			{
+				name:      "integer number integer",
+				query:     "SELECT {'1': 11, '2': 22}::MAP(INTEGER, NUMBER(38, 0)) as structured_type UNION SELECT {'3': 33}::MAP(INTEGER, NUMBER(38, 0))",
+				expected1: map[int64]int64{int64(1): int64(11), int64(2): int64(22)},
+				expected2: map[int64]int64{int64(3): int64(33)},
+				actual:    make(map[int64]int64),
+			},
+			{
+				name:      "string number fraction",
+				query:     "SELECT {'a': 11.1, 'b': 22.2}::MAP(VARCHAR, NUMBER(38, 19)) as structured_type UNION SELECT {'c': 33.3}::MAP(VARCHAR, NUMBER(38, 19))",
+				expected1: map[string]float64{"a": 11.1, "b": 22.2},
+				expected2: map[string]float64{"c": 33.3},
+				actual:    make(map[string]float64),
+			},
+			{
+				name:      "integer number fraction",
+				query:     "SELECT {'1': 11.1, '2': 22.2}::MAP(INTEGER, NUMBER(38, 19)) as structured_type UNION SELECT {'3': 33.3}::MAP(INTEGER, NUMBER(38, 19))",
+				expected1: map[int64]float64{int64(1): 11.1, int64(2): 22.2},
+				expected2: map[int64]float64{int64(3): 33.3},
+				actual:    make(map[int64]float64),
+			},
+			{
+				name:      "string binary",
+				query:     "SELECT {'a': TO_BINARY('616263', 'HEX'), 'b': TO_BINARY('646566', 'HEX')}::MAP(VARCHAR, BINARY) as structured_type UNION SELECT {'c': TO_BINARY('676869', 'HEX')}::MAP(VARCHAR, BINARY)",
+				expected1: map[string][]byte{"a": {'a', 'b', 'c'}, "b": {'d', 'e', 'f'}},
+				expected2: map[string][]byte{"c": {'g', 'h', 'i'}},
+				actual:    make(map[string][]byte),
+			},
+			{
+				name:      "integer binary",
+				query:     "SELECT {'1': TO_BINARY('616263', 'HEX'), '2': TO_BINARY('646566', 'HEX')}::MAP(INTEGER, BINARY) as structured_type UNION SELECT {'3': TO_BINARY('676869', 'HEX')}::MAP(INTEGER, BINARY)",
+				expected1: map[int64][]byte{1: {'a', 'b', 'c'}, 2: {'d', 'e', 'f'}},
+				expected2: map[int64][]byte{3: {'g', 'h', 'i'}},
+				actual:    make(map[int64][]byte),
+			},
+			{
+				name:      "string date",
+				query:     "SELECT {'a': '2024-04-02'::DATE, 'b': '2024-04-03'::DATE}::MAP(VARCHAR, DATE) as structured_type UNION SELECT {'c': '2024-04-04'::DATE}::MAP(VARCHAR, DATE)",
+				expected1: map[string]time.Time{"a": time.Date(2024, time.April, 2, 0, 0, 0, 0, time.UTC), "b": time.Date(2024, time.April, 3, 0, 0, 0, 0, time.UTC)},
+				expected2: map[string]time.Time{"c": time.Date(2024, time.April, 4, 0, 0, 0, 0, time.UTC)},
+				actual:    make(map[string]time.Time),
+			},
+			{
+				name:      "integer date",
+				query:     "SELECT {'1': '2024-04-02'::DATE, '2': '2024-04-03'::DATE}::MAP(INTEGER, DATE) as structured_type UNION SELECT {'3': '2024-04-04'::DATE}::MAP(INTEGER, DATE)",
+				expected1: map[int64]time.Time{1: time.Date(2024, time.April, 2, 0, 0, 0, 0, time.UTC), 2: time.Date(2024, time.April, 3, 0, 0, 0, 0, time.UTC)},
+				expected2: map[int64]time.Time{3: time.Date(2024, time.April, 4, 0, 0, 0, 0, time.UTC)},
+				actual:    make(map[int64]time.Time),
+			},
+			{
+				name:      "string time",
+				query:     "SELECT {'a': '13:03:02'::TIME, 'b': '13:03:03'::TIME}::MAP(VARCHAR, TIME) as structured_type UNION SELECT {'c': '13:03:04'::TIME}::MAP(VARCHAR, TIME)",
+				expected1: map[string]time.Time{"a": time.Date(0, 0, 0, 13, 3, 2, 0, time.UTC), "b": time.Date(0, 0, 0, 13, 3, 3, 0, time.UTC)},
+				expected2: map[string]time.Time{"c": time.Date(0, 0, 0, 13, 3, 4, 0, time.UTC)},
+				actual:    make(map[string]time.Time),
+			},
+			{
+				name:      "integer time",
+				query:     "SELECT {'1': '13:03:02'::TIME, '2': '13:03:03'::TIME}::MAP(VARCHAR, TIME) as structured_type UNION SELECT {'3': '13:03:04'::TIME}::MAP(VARCHAR, TIME)",
+				expected1: map[string]time.Time{"1": time.Date(0, 0, 0, 13, 3, 2, 0, time.UTC), "2": time.Date(0, 0, 0, 13, 3, 3, 0, time.UTC)},
+				expected2: map[string]time.Time{"3": time.Date(0, 0, 0, 13, 3, 4, 0, time.UTC)},
+				actual:    make(map[int64]time.Time),
+			},
+			{
+				name:      "string timestamp_ntz",
+				query:     "SELECT {'a': '2024-01-05 11:22:33'::TIMESTAMP_NTZ, 'b': '2024-01-06 11:22:33'::TIMESTAMP_NTZ}::MAP(VARCHAR, TIMESTAMP_NTZ) as structured_type UNION SELECT {'c': '2024-01-07 11:22:33'::TIMESTAMP_NTZ}::MAP(VARCHAR, TIMESTAMP_NTZ)",
+				expected1: map[string]time.Time{"a": time.Date(2024, time.January, 5, 11, 22, 33, 0, time.UTC), "b": time.Date(2024, time.January, 6, 11, 22, 33, 0, time.UTC)},
+				expected2: map[string]time.Time{"c": time.Date(2024, time.January, 7, 11, 22, 33, 0, time.UTC)},
+				actual:    make(map[string]time.Time),
+			},
+			{
+				name:      "integer timestamp_ntz",
+				query:     "SELECT {'1': '2024-01-05 11:22:33'::TIMESTAMP_NTZ, '2': '2024-01-06 11:22:33'::TIMESTAMP_NTZ}::MAP(INTEGER, TIMESTAMP_NTZ) as structured_type UNION SELECT {'3': '2024-01-07 11:22:33'::TIMESTAMP_NTZ}::MAP(INTEGER, TIMESTAMP_NTZ)",
+				expected1: map[int64]time.Time{1: time.Date(2024, time.January, 5, 11, 22, 33, 0, time.UTC), 2: time.Date(2024, time.January, 6, 11, 22, 33, 0, time.UTC)},
+				expected2: map[int64]time.Time{3: time.Date(2024, time.January, 7, 11, 22, 33, 0, time.UTC)},
+				actual:    make(map[int64]time.Time),
+			},
+			{
+				name:      "string timestamp_tz",
+				query:     "SELECT {'a': '2024-01-05 11:22:33 +0100'::TIMESTAMP_TZ, 'b': '2024-01-06 11:22:33 +0100'::TIMESTAMP_TZ}::MAP(VARCHAR, TIMESTAMP_TZ) as structured_type UNION SELECT {'c': '2024-01-07 11:22:33 +0100'::TIMESTAMP_TZ}::MAP(VARCHAR, TIMESTAMP_TZ)",
+				expected1: map[string]time.Time{"a": time.Date(2024, time.January, 5, 11, 22, 33, 0, warsawTz), "b": time.Date(2024, time.January, 6, 11, 22, 33, 0, warsawTz)},
+				expected2: map[string]time.Time{"c": time.Date(2024, time.January, 7, 11, 22, 33, 0, warsawTz)},
+				actual:    make(map[string]time.Time),
+			},
+			{
+				name:      "integer timestamp_tz",
+				query:     "SELECT {'1': '2024-01-05 11:22:33 +0100'::TIMESTAMP_TZ, '2': '2024-01-06 11:22:33 +0100'::TIMESTAMP_TZ}::MAP(INTEGER, TIMESTAMP_TZ) as structured_type UNION SELECT {'3': '2024-01-07 11:22:33 +0100'::TIMESTAMP_TZ}::MAP(INTEGER, TIMESTAMP_TZ)",
+				expected1: map[int64]time.Time{1: time.Date(2024, time.January, 5, 11, 22, 33, 0, time.UTC), 2: time.Date(2024, time.January, 6, 11, 22, 33, 0, time.UTC)},
+				expected2: map[int64]time.Time{3: time.Date(2024, time.January, 7, 11, 22, 33, 0, time.UTC)},
+				actual:    make(map[int64]time.Time),
+			},
+			{
+				name:      "string timestamp_ltz",
+				query:     "SELECT {'a': '2024-01-05 11:22:33'::TIMESTAMP_LTZ, 'b': '2024-01-06 11:22:33'::TIMESTAMP_LTZ}::MAP(VARCHAR, TIMESTAMP_LTZ) as structured_type UNION SELECT {'c': '2024-01-07 11:22:33'::TIMESTAMP_LTZ}::MAP(VARCHAR, TIMESTAMP_LTZ)",
+				expected1: map[string]time.Time{"a": time.Date(2024, time.January, 5, 11, 22, 33, 0, warsawTz), "b": time.Date(2024, time.January, 6, 11, 22, 33, 0, warsawTz)},
+				expected2: map[string]time.Time{"c": time.Date(2024, time.January, 7, 11, 22, 33, 0, warsawTz)},
+				actual:    make(map[string]time.Time),
+			},
+			{
+				name:      "integer timestamp_ltz",
+				query:     "SELECT {'1': '2024-01-05 11:22:33'::TIMESTAMP_LTZ, '2': '2024-01-06 11:22:33'::TIMESTAMP_LTZ}::MAP(INTEGER, TIMESTAMP_LTZ) as structured_type UNION SELECT {'3': '2024-01-07 11:22:33'::TIMESTAMP_LTZ}::MAP(INTEGER, TIMESTAMP_LTZ)",
+				expected1: map[int64]time.Time{1: time.Date(2024, time.January, 5, 11, 22, 33, 0, time.UTC), 2: time.Date(2024, time.January, 6, 11, 22, 33, 0, time.UTC)},
+				expected2: map[int64]time.Time{3: time.Date(2024, time.January, 7, 11, 22, 33, 0, time.UTC)},
+				actual:    make(map[int64]time.Time),
+			},
+		}
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			for _, tc := range testcases {
+				t.Run(tc.name, func(t *testing.T) {
+					rows := dbt.mustQuery(tc.query)
+					defer rows.Close()
+
+					checkRow := func(expected any) {
+						rows.Next()
+						err := rows.Scan(&tc.actual)
+						assertNilF(t, err)
+						if _, ok := expected.(map[string]time.Time); ok {
+							assertEqualE(t, len(tc.actual.(map[string]time.Time)), len(expected.(map[string]time.Time)))
+							for k, v := range expected.(map[string]time.Time) {
+								if strings.Contains(tc.name, "time") {
+									assertEqualE(t, v.Hour(), tc.actual.(map[string]time.Time)[k].Hour())
+									assertEqualE(t, v.Minute(), tc.actual.(map[string]time.Time)[k].Minute())
+									assertEqualE(t, v.Second(), tc.actual.(map[string]time.Time)[k].Second())
+								} else {
+									assertTrueE(t, v.UTC().Equal(tc.actual.(map[string]time.Time)[k].UTC()))
+								}
+							}
+						} else if _, ok := expected.(map[int64]time.Time); ok {
+							assertEqualE(t, len(tc.actual.(map[int64]time.Time)), len(expected.(map[int64]time.Time)))
+							for k, v := range expected.(map[int64]time.Time) {
+								if strings.Contains(tc.name, "time") {
+
+								} else {
+									assertTrueE(t, v.UTC().Equal(tc.actual.(map[int64]time.Time)[k].UTC()))
+								}
+							}
+						} else {
+							assertDeepEqualE(t, tc.actual, expected)
+						}
+					}
+
+					checkRow(tc.expected1)
+					checkRow(tc.expected2)
+
+					columnTypes, err := rows.ColumnTypes()
+					assertNilF(t, err)
+					assertEqualE(t, len(columnTypes), 1)
+					assertEqualE(t, columnTypes[0].ScanType(), reflect.TypeOf(tc.expected1))
+					assertEqualE(t, columnTypes[0].DatabaseTypeName(), "MAP")
+					assertEqualE(t, columnTypes[0].Name(), "STRUCTURED_TYPE")
+				})
+			}
+		})
+	})
+}
+
 func TestWithHigherPrecision(t *testing.T) {
 	skipStructuredTypesTestsOnGHActions(t)
 	runDBTest(t, func(dbt *DBTest) {
@@ -487,6 +720,95 @@ func TestWithHigherPrecision(t *testing.T) {
 				assertEqualE(t, columnTypes[0].DatabaseTypeName(), "ARRAY")
 				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")
+				defer rows.Close()
+				rows.Next()
+				var v *map[string]*big.Int
+				err := rows.Scan(&v)
+				assertNilF(t, err)
+				bigInt, b := new(big.Int).SetString("10000000000000000000000000000000000000", 10)
+				assertTrueF(t, b)
+				assertEqualE(t, bigInt.Cmp((*v)["x"]), 0)
+				columnTypes, err := rows.ColumnTypes()
+				assertNilF(t, err)
+				assertEqualE(t, len(columnTypes), 1)
+				assertEqualE(t, columnTypes[0].ScanType(), reflect.TypeOf(map[string]*big.Int{}))
+				assertEqualE(t, columnTypes[0].DatabaseTypeName(), "MAP")
+				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")
+				defer rows.Close()
+				rows.Next()
+				var v *map[string]*big.Float
+				err := rows.Scan(&v)
+				assertNilF(t, err)
+				bigFloat, b := new(big.Float).SetPrec((*v)["x"].Prec()).SetString("1.2345678901234567890123456789012345678")
+				assertTrueE(t, b)
+				assertEqualE(t, bigFloat.Cmp((*v)["x"]), 0)
+				columnTypes, err := rows.ColumnTypes()
+				assertNilF(t, err)
+				assertEqualE(t, len(columnTypes), 1)
+				assertEqualE(t, columnTypes[0].ScanType(), reflect.TypeOf(map[string]*big.Float{}))
+				assertEqualE(t, columnTypes[0].DatabaseTypeName(), "MAP")
+				assertEqualE(t, columnTypes[0].Name(), "STRUCTURED_TYPE")
+			})
+			t.Run("map of int64 to big ints", func(t *testing.T) {
+				rows := dbt.mustQueryContext(ctx, "SELECT {'1': 10000000000000000000000000000000000000}::MAP(INTEGER, DECIMAL(38, 0)) as structured_type")
+				defer rows.Close()
+				rows.Next()
+				var v *map[int64]*big.Int
+				err := rows.Scan(&v)
+				assertNilF(t, err)
+				bigInt, b := new(big.Int).SetString("10000000000000000000000000000000000000", 10)
+				assertTrueF(t, b)
+				assertEqualE(t, bigInt.Cmp((*v)[1]), 0)
+				columnTypes, err := rows.ColumnTypes()
+				assertNilF(t, err)
+				assertEqualE(t, len(columnTypes), 1)
+				assertEqualE(t, columnTypes[0].ScanType(), reflect.TypeOf(map[int64]*big.Int{}))
+				assertEqualE(t, columnTypes[0].DatabaseTypeName(), "MAP")
+				assertEqualE(t, columnTypes[0].Name(), "STRUCTURED_TYPE")
+			})
+			t.Run("map of int64 to big floats", func(t *testing.T) {
+				rows := dbt.mustQueryContext(ctx, "SELECT {'1': 1.2345678901234567890123456789012345678}::MAP(INTEGER, DECIMAL(38, 37)) as structured_type")
+				defer rows.Close()
+				rows.Next()
+				var v *map[int64]*big.Float
+				err := rows.Scan(&v)
+				assertNilF(t, err)
+				bigFloat, b := new(big.Float).SetPrec((*v)[1].Prec()).SetString("1.2345678901234567890123456789012345678")
+				assertTrueE(t, b)
+				assertEqualE(t, bigFloat.Cmp((*v)[1]), 0)
+				columnTypes, err := rows.ColumnTypes()
+				assertNilF(t, err)
+				assertEqualE(t, len(columnTypes), 1)
+				assertEqualE(t, columnTypes[0].ScanType(), reflect.TypeOf(map[int64]*big.Float{}))
+				assertEqualE(t, columnTypes[0].DatabaseTypeName(), "MAP")
+				assertEqualE(t, columnTypes[0].Name(), "STRUCTURED_TYPE")
+			})
+		})
+	})
+}
+
+func TestNullAndEmptyMaps(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			rows := dbt.mustQuery("SELECT {'a': 1}::MAP(VARCHAR, INTEGER) UNION SELECT NULL UNION SELECT {}::MAP(VARCHAR, INTEGER) UNION SELECT {'d': 4}::MAP(VARCHAR, INTEGER)")
+			defer rows.Close()
+			checkRow := func(rows *RowsExtended, expected *map[string]int64) {
+				rows.Next()
+				var res *map[string]int64
+				err := rows.Scan(&res)
+				assertNilF(t, err)
+				assertDeepEqualE(t, res, expected)
+			}
+			checkRow(rows, &map[string]int64{"a": 1})
+			checkRow(rows, nil)
+			checkRow(rows, &map[string]int64{})
+			checkRow(rows, &map[string]int64{"d": 4})
 		})
 	})
 }