Repository: snowflakedb/gosnowflake
Pull request #1154 (2024-06-11 11:40:34)

SNOW-1460206 Introduce structured arrays
### Description

SNOW-1460206 Introduced structrued arrays binding

### 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/1154?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 `32.99492%` with `132 lines` in your changes missing coverage. Please review.
> Project coverage is 71.57%. Comparing base [(`5d28db8`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/5d28db80c2ffac67e9e8991eb2d78c591faedb47?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) to head [(`8c0337a`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/8c0337a56ff0d16d495c7ce12003074b6ea7b641?dropdown=coverage&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/1154?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/1154?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) | 32.25% | [126 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1154?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [structured\_type.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1154?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% | [3 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1154?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [datatype.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1154?src=pr&el=tree&filepath=datatype.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-ZGF0YXR5cGUuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1154?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) |
| [bind\_uploader.go](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1154?src=pr&el=tree&filepath=bind_uploader.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb#diff-YmluZF91cGxvYWRlci5nbw==) | 80.00% | [1 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1154?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    #1154      +/-   ##
==========================================
- Coverage   72.34%   71.57%   -0.78%     
==========================================
  Files          53       53              
  Lines       11909    12064     +155     
==========================================
+ Hits         8616     8635      +19     
- Misses       3293     3429     +136     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1154?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/bind_uploader.go b/bind_uploader.go
index a2680d8..e673933 100644
--- a/bind_uploader.go
+++ b/bind_uploader.go
@@ -258,6 +258,8 @@ func getBindValues(bindings []driver.NamedValue, params map[string]*string) (map
 				t = textType // if null or not supported, pass to GS as text
 			} else if t == nullObjectType {
 				t = objectType
+			} else if t == emptyArrayType {
+				t = arrayType
 			}
 			bindValues[bindingName(binding, idx)] = execBindParameter{
 				Type:   t.String(),
@@ -348,3 +350,8 @@ func supportedStructuredObjectWriterBind(nv *driver.NamedValue) bool {
 	_, ok := nv.Value.(reflect.Type)
 	return ok
 }
+
+func supportedStructuredArrayBind(nv *driver.NamedValue) bool {
+	typ := reflect.TypeOf(nv.Value)
+	return typ != nil && (typ.Kind() == reflect.Array || typ.Kind() == reflect.Slice)
+}
diff --git a/connection.go b/connection.go
index a2c0c19..29e7f89 100644
--- a/connection.go
+++ b/connection.go
@@ -470,7 +470,7 @@ func (sc *snowflakeConn) Ping(ctx context.Context) error {
 // CheckNamedValue determines which types are handled by this driver aside from
 // the instances captured by driver.Value
 func (sc *snowflakeConn) CheckNamedValue(nv *driver.NamedValue) error {
-	if supportedNullBind(nv) || supportedArrayBind(nv) || supportedStructuredObjectWriterBind(nv) {
+	if supportedNullBind(nv) || supportedArrayBind(nv) || supportedStructuredObjectWriterBind(nv) || supportedStructuredArrayBind(nv) {
 		return nil
 	}
 	return driver.ErrSkip
diff --git a/converter.go b/converter.go
index 351f2eb..5a7ede4 100644
--- a/converter.go
+++ b/converter.go
@@ -77,6 +77,9 @@ func isInterfaceArrayBinding(t interface{}) bool {
 
 // goTypeToSnowflake translates Go data type to Snowflake data type.
 func goTypeToSnowflake(v driver.Value, tsmode snowflakeType) snowflakeType {
+	if v == nil {
+		return nullType
+	}
 	switch t := v.(type) {
 	case int64, sql.NullInt64:
 		return fixedType
@@ -105,11 +108,19 @@ func goTypeToSnowflake(v driver.Value, tsmode snowflakeType) snowflakeType {
 	}
 	if supportedArrayBind(&driver.NamedValue{Value: v}) {
 		return sliceType
-	} else if _, ok := v.(StructuredObjectWriter); ok {
+	}
+	// structured objects
+	if _, ok := v.(StructuredObjectWriter); ok {
 		return objectType
 	} else if _, ok := v.(reflect.Type); ok && tsmode == nullObjectType {
 		return nullObjectType
 	}
+	// structured arrays
+	if reflect.TypeOf(v).Kind() == reflect.Slice || (reflect.TypeOf(v).Kind() == reflect.Pointer && reflect.ValueOf(v).Elem().Kind() == reflect.Slice) {
+		return arrayType
+	} else if tsmode == emptyArrayType {
+		return emptyArrayType
+	}
 	return unSupportedType
 }
 
@@ -215,7 +226,7 @@ func valueToString(v driver.Value, tsmode snowflakeType, params map[string]*stri
 	if v == nil {
 		return bindingValue{nil, "", nil}, nil
 	}
-	v1 := reflect.ValueOf(v)
+	v1 := reflect.Indirect(reflect.ValueOf(v))
 	switch v1.Kind() {
 	case reflect.Bool:
 		s := strconv.FormatBool(v1.Bool())
@@ -229,52 +240,184 @@ func valueToString(v driver.Value, tsmode snowflakeType, params map[string]*stri
 	case reflect.String:
 		s := v1.String()
 		return bindingValue{&s, "", nil}, nil
-	case reflect.Slice, reflect.Map:
+	case reflect.Slice, reflect.Array:
+		return arrayToString(v, tsmode, params)
+	case reflect.Map:
 		if v1.IsNil() {
 			return bindingValue{nil, "", nil}, nil
 		}
-		if bd, ok := v.([]byte); ok {
-			if tsmode == binaryType {
-				s := hex.EncodeToString(bd)
-				return bindingValue{&s, "", nil}, nil
-			}
-		}
 		// TODO: is this good enough?
 		s := v1.String()
 		return bindingValue{&s, "", nil}, nil
 	case reflect.Struct:
-		switch typedVal := v.(type) {
-		case time.Time:
-			return timeTypeValueToString(typedVal, tsmode)
-		case sql.NullTime:
-			if !typedVal.Valid {
-				return bindingValue{nil, "", nil}, nil
-			}
-			return timeTypeValueToString(typedVal.Time, tsmode)
-		case sql.NullBool:
-			if !typedVal.Valid {
-				return bindingValue{nil, "", nil}, nil
-			}
-			s := strconv.FormatBool(typedVal.Bool)
-			return bindingValue{&s, "", nil}, nil
-		case sql.NullInt64:
-			if !typedVal.Valid {
-				return bindingValue{nil, "", nil}, nil
-			}
-			s := strconv.FormatInt(typedVal.Int64, 10)
-			return bindingValue{&s, "", nil}, nil
-		case sql.NullFloat64:
-			if !typedVal.Valid {
-				return bindingValue{nil, "", nil}, nil
-			}
-			s := strconv.FormatFloat(typedVal.Float64, 'g', -1, 32)
-			return bindingValue{&s, "", nil}, nil
-		case sql.NullString:
-			if !typedVal.Valid {
-				return bindingValue{nil, "", nil}, nil
-			}
-			return bindingValue{&typedVal.String, "", nil}, nil
+		return structValueToString(v, tsmode, params)
+	}
+
+	return bindingValue{}, fmt.Errorf("unsupported type: %v", v1.Kind())
+}
+
+func arrayToString(v driver.Value, tsmode snowflakeType, params map[string]*string) (bindingValue, error) {
+	v1 := reflect.Indirect(reflect.ValueOf(v))
+	if v1.IsNil() {
+		return bindingValue{nil, "", nil}, nil
+	}
+	if bd, ok := v.([][]byte); ok && tsmode == binaryType {
+		schema := bindingSchema{
+			Typ:      "array",
+			Nullable: true,
+			Fields: []fieldMetadata{
+				{
+					Type:     "binary",
+					Nullable: true,
+				},
+			},
+		}
+		if len(bd) == 0 {
+			res := "[]"
+			return bindingValue{value: &res, format: "json", schema: &schema}, nil
+		}
+		s := ""
+		for _, b := range bd {
+			s += "\"" + hex.EncodeToString(b) + "\","
+		}
+		s = "[" + s[:len(s)-1] + "]"
+		return bindingValue{&s, "json", &schema}, nil
+	} else if times, ok := v.([]time.Time); ok {
+		typ := driverTypeToSnowflake[tsmode]
+		sfFormat, err := dateTimeInputFormatByType(typ, params)
+		if err != nil {
+			return bindingValue{nil, "", nil}, err
+		}
+		goFormat, err := snowflakeFormatToGoFormat(sfFormat)
+		if err != nil {
+			return bindingValue{nil, "", nil}, err
+		}
+		arr := make([]string, len(times))
+		for idx, t := range times {
+			arr[idx] = t.Format(goFormat)
 		}
+		res, err := json.Marshal(v)
+		if err != nil {
+			return bindingValue{nil, "", &bindingSchema{
+				Typ:      "array",
+				Nullable: true,
+				Fields: []fieldMetadata{
+					{
+						Type:     typ,
+						Nullable: true,
+					},
+				},
+			}}, err
+		}
+		resString := string(res)
+		return bindingValue{&resString, "json", nil}, nil
+	} else if isArrayOfStructs(v) {
+		stringEntries := make([]string, v1.Len())
+		entrySchema := &bindingSchema{}
+		for i := 0; i < v1.Len(); i++ {
+			potentialSow := v1.Index(i)
+			if sow, ok := potentialSow.Interface().(StructuredObjectWriter); ok {
+				bv, err := structValueToString(sow, tsmode, params)
+				if err != nil {
+					return bindingValue{nil, "", nil}, err
+				}
+				stringEntries[i] = *bv.value
+				entrySchema = bv.schema
+			}
+		}
+		value := "[" + strings.Join(stringEntries, ",") + "]"
+		arraySchema := &bindingSchema{
+			Typ:      "array",
+			Nullable: true,
+			Fields: []fieldMetadata{
+				{
+					Type:     "OBJECT",
+					Nullable: true,
+					Fields:   entrySchema.Fields,
+				},
+			},
+		}
+		return bindingValue{&value, "json", arraySchema}, nil
+	} else if reflect.ValueOf(v).Len() == 0 {
+		value := "[]"
+		return bindingValue{&value, "json", nil}, nil
+	} else if barr, ok := v.([]byte); ok {
+		if tsmode == binaryType {
+			res := hex.EncodeToString(barr)
+			return bindingValue{&res, "", nil}, nil
+		}
+		schemaForBytes := bindingSchema{
+			Typ:      "array",
+			Nullable: true,
+			Fields: []fieldMetadata{
+				{
+					Type:     "TINYINT",
+					Nullable: true,
+				},
+			},
+		}
+		if len(barr) == 0 {
+			res := "[]"
+			return bindingValue{&res, "", &schemaForBytes}, nil
+		}
+		res := "["
+		for _, b := range barr {
+			res += fmt.Sprint(b) + ","
+		}
+		res = res[0:len(res)-1] + "]"
+		return bindingValue{&res, "", &schemaForBytes}, nil
+	} else if isSliceOfSlices(v) {
+		return bindingValue{}, errors.New("array of arrays is not supported")
+	}
+	res, err := json.Marshal(v)
+	if err != nil {
+		return bindingValue{nil, "", nil}, err
+	}
+	resString := string(res)
+	return bindingValue{&resString, "json", nil}, nil
+}
+
+func isSliceOfSlices(v any) bool {
+	typ := reflect.TypeOf(v)
+	return typ.Kind() == reflect.Slice && typ.Elem().Kind() == reflect.Slice
+}
+
+func isArrayOfStructs(v any) bool {
+	return reflect.TypeOf(v).Elem().Kind() == reflect.Struct || (reflect.TypeOf(v).Elem().Kind() == reflect.Pointer && reflect.TypeOf(v).Elem().Elem().Kind() == reflect.Struct)
+}
+
+func structValueToString(v driver.Value, tsmode snowflakeType, params map[string]*string) (bindingValue, error) {
+	switch typedVal := v.(type) {
+	case time.Time:
+		return timeTypeValueToString(typedVal, tsmode)
+	case sql.NullTime:
+		if !typedVal.Valid {
+			return bindingValue{nil, "", nil}, nil
+		}
+		return timeTypeValueToString(typedVal.Time, tsmode)
+	case sql.NullBool:
+		if !typedVal.Valid {
+			return bindingValue{nil, "", nil}, nil
+		}
+		s := strconv.FormatBool(typedVal.Bool)
+		return bindingValue{&s, "", nil}, nil
+	case sql.NullInt64:
+		if !typedVal.Valid {
+			return bindingValue{nil, "", nil}, nil
+		}
+		s := strconv.FormatInt(typedVal.Int64, 10)
+		return bindingValue{&s, "", nil}, nil
+	case sql.NullFloat64:
+		if !typedVal.Valid {
+			return bindingValue{nil, "", nil}, nil
+		}
+		s := strconv.FormatFloat(typedVal.Float64, 'g', -1, 32)
+		return bindingValue{&s, "", nil}, nil
+	case sql.NullString:
+		if !typedVal.Valid {
+			return bindingValue{nil, "", nil}, nil
+		}
+		return bindingValue{&typedVal.String, "", nil}, nil
 	}
 	if sow, ok := v.(StructuredObjectWriter); ok {
 		sowc := &structuredObjectWriterContext{}
@@ -304,9 +447,25 @@ func valueToString(v driver.Value, tsmode snowflakeType, params map[string]*stri
 			Nullable: true,
 			Fields:   sowc.toFields(),
 		}
-		return bindingValue{nil, "json", &schema}, nil
-	}
-	return bindingValue{nil, "", nil}, fmt.Errorf("unsupported type: %v", v1.Kind())
+		var val *string = nil
+		if tsmode == emptyArrayType {
+			emptyArray := "[]"
+			val = &emptyArray
+			schema = bindingSchema{
+				Typ:      "array",
+				Nullable: true,
+				Fields: []fieldMetadata{
+					{
+						Type:     schema.Typ,
+						Nullable: schema.Nullable,
+						Fields:   schema.Fields,
+					},
+				},
+			}
+		}
+		return bindingValue{val, "json", &schema}, nil
+	}
+	return bindingValue{nil, "", nil}, nil
 }
 
 func timeTypeValueToString(tm time.Time, tsmode snowflakeType) (bindingValue, error) {
diff --git a/datatype.go b/datatype.go
index f927923..476c242 100644
--- a/datatype.go
+++ b/datatype.go
@@ -33,6 +33,7 @@ const (
 	changeType
 	unSupportedType
 	nullObjectType
+	emptyArrayType
 )
 
 var snowflakeToDriverType = map[string]snowflakeType{
@@ -109,6 +110,8 @@ var (
 	DataTypeBoolean = []byte{booleanType.Byte()}
 	// DataTypeNullObject represents a null structured object.
 	DataTypeNullObject = []byte{nullObjectType.Byte()}
+	// DataTypeEmptyArray represents an empty structured array.
+	DataTypeEmptyArray = []byte{emptyArrayType.Byte()}
 )
 
 // dataTypeMode returns the subsequent data type in a string representation.
@@ -135,6 +138,8 @@ func dataTypeMode(v driver.Value) (tsmode snowflakeType, err error) {
 			tsmode = variantType
 		case bytes.Equal(bd, DataTypeNullObject):
 			tsmode = nullObjectType
+		case bytes.Equal(bd, DataTypeEmptyArray):
+			tsmode = emptyArrayType
 		default:
 			return nullType, fmt.Errorf(errMsgInvalidByteArray, v)
 		}
diff --git a/doc.go b/doc.go
index 304bced..353e560 100644
--- a/doc.go
+++ b/doc.go
@@ -549,6 +549,11 @@ b)
 
 	db.Exec('INSERT INTO some_table VALUES ?', sf.DataTypeNullObject, reflect.TypeOf(simpleObject{})
 
+Binding structured arrays are like any other parameter.
+The only difference is - if you want to insert empty array (not nil but empty), you have to use:
+
+	db.Exec('INSERT INTO some_table VALUES ?', sf.DataTypeEmptyArray, reflect.TypeOf(simpleObject{}))
+
 # Using higher precision numbers
 
 The following example shows how to retrieve very large values using the math/big
diff --git a/query.go b/query.go
index 628b86d..d9234ff 100644
--- a/query.go
+++ b/query.go
@@ -72,7 +72,7 @@ func (ex *execResponseRowType) toFieldMetadata() fieldMetadata {
 }
 
 type fieldMetadata struct {
-	Name      string          `json:"name"`
+	Name      string          `json:"name,omitempty"`
 	Type      string          `json:"type"`
 	Nullable  bool            `json:"nullable"`
 	Length    int             `json:"length"`
diff --git a/structured_type.go b/structured_type.go
index 9e5d2fd..07e568c 100644
--- a/structured_type.go
+++ b/structured_type.go
@@ -538,6 +538,9 @@ func (sowc *structuredObjectWriterContext) toFields() []fieldMetadata {
 type ArrayOfScanners[T sql.Scanner] []T
 
 func (st *ArrayOfScanners[T]) Scan(val any) error {
+	if val == nil {
+		return nil
+	}
 	sts := val.([]*structuredType)
 	*st = make([]T, len(sts))
 	var t T


Test patch:
diff --git a/converter_test.go b/converter_test.go
index 619308e..f94a4e8 100644
--- a/converter_test.go
+++ b/converter_test.go
@@ -102,6 +102,7 @@ func TestGoTypeToSnowflake(t *testing.T) {
 		{in: time.Now(), tmode: timestampTzType, out: timestampTzType},
 		{in: time.Now(), tmode: timestampLtzType, out: timestampLtzType},
 		{in: []byte{1, 2, 3}, tmode: binaryType, out: binaryType},
+		{in: []int{1}, tmode: nullType, out: arrayType},
 		{in: Array([]interface{}{int64(123)}), tmode: nullType, out: sliceType},
 		{in: Array([]interface{}{float64(234.56)}), tmode: nullType, out: sliceType},
 		{in: Array([]interface{}{true}), tmode: nullType, out: sliceType},
@@ -117,6 +118,7 @@ func TestGoTypeToSnowflake(t *testing.T) {
 		{in: Array([]interface{}{time.Now()}, TimestampTZType), tmode: timeType, out: sliceType},
 		{in: Array([]interface{}{time.Now()}, DateType), tmode: timestampNtzType, out: sliceType},
 		{in: Array([]interface{}{time.Now()}, TimeType), tmode: timestampTzType, out: sliceType},
+		{in: nil, tmode: nullType, out: nullType},
 		// negative
 		{in: 123, tmode: nullType, out: unSupportedType},
 		{in: int8(12), tmode: nullType, out: unSupportedType},
@@ -125,8 +127,6 @@ func TestGoTypeToSnowflake(t *testing.T) {
 		{in: uint8(12), tmode: nullType, out: unSupportedType},
 		{in: uint64(456), tmode: nullType, out: unSupportedType},
 		{in: []byte{100}, tmode: nullType, out: unSupportedType},
-		{in: []int{1}, tmode: nullType, out: unSupportedType},
-		{in: nil, tmode: nullType, out: unSupportedType},
 	}
 	for _, test := range testcases {
 		t.Run(fmt.Sprintf("%v_%v_%v", test.in, test.out, test.tmode), func(t *testing.T) {
@@ -2396,3 +2396,24 @@ func TestTimeTypeValueToString(t *testing.T) {
 		})
 	}
 }
+
+func TestIsArrayOfStructs(t *testing.T) {
+	testcases := []struct {
+		value    any
+		expected bool
+	}{
+		{[]simpleObject{}, true},
+		{[]*simpleObject{}, true},
+		{[]int{1}, false},
+		{[]string{"abc"}, false},
+		{&[]bool{true}, false},
+	}
+	for _, tc := range testcases {
+		t.Run(fmt.Sprintf("%v", tc.value), func(t *testing.T) {
+			res := isArrayOfStructs(tc.value)
+			if res != tc.expected {
+				t.Errorf("expected %v to result in %v", tc.value, tc.expected)
+			}
+		})
+	}
+}
diff --git a/driver_test.go b/driver_test.go
index 48fe16f..21bba05 100644
--- a/driver_test.go
+++ b/driver_test.go
@@ -178,7 +178,7 @@ func (dbt *DBTest) connParams() map[string]*string {
 	return params
 }
 
-func (dbt *DBTest) mustQuery(query string, args ...interface{}) (rows *RowsExtended) {
+func (dbt *DBTest) mustQueryT(t *testing.T, query string, args ...any) (rows *RowsExtended) {
 	// handler interrupt signal
 	ctx, cancel := context.WithCancel(context.Background())
 	c := make(chan os.Signal, 1)
@@ -201,7 +201,7 @@ func (dbt *DBTest) mustQuery(query string, args ...interface{}) (rows *RowsExten
 
 	rs, err := dbt.conn.QueryContext(ctx, query, args...)
 	if err != nil {
-		dbt.fail("query", query, err)
+		t.Fatalf("query, query=%v, err=%v", query, err)
 	}
 	return &RowsExtended{
 		rows:      rs,
@@ -209,7 +209,15 @@ func (dbt *DBTest) mustQuery(query string, args ...interface{}) (rows *RowsExten
 	}
 }
 
+func (dbt *DBTest) mustQuery(query string, args ...interface{}) (rows *RowsExtended) {
+	return dbt.mustQueryT(dbt.T, query, args...)
+}
+
 func (dbt *DBTest) mustQueryContext(ctx context.Context, query string, args ...interface{}) (rows *RowsExtended) {
+	return dbt.mustQueryContextT(ctx, dbt.T, query, args...)
+}
+
+func (dbt *DBTest) mustQueryContextT(ctx context.Context, t *testing.T, query string, args ...interface{}) (rows *RowsExtended) {
 	// handler interrupt signal
 	ctx, cancel := context.WithCancel(ctx)
 	c := make(chan os.Signal, 1)
@@ -232,7 +240,7 @@ func (dbt *DBTest) mustQueryContext(ctx context.Context, query string, args ...i
 
 	rs, err := dbt.conn.QueryContext(ctx, query, args...)
 	if err != nil {
-		dbt.fail("query", query, err)
+		t.Fatalf("query, query=%v, err=%v", query, err)
 	}
 	return &RowsExtended{
 		rows:      rs,
@@ -271,6 +279,10 @@ func (dbt *DBTest) mustExec(query string, args ...interface{}) (res sql.Result)
 	return dbt.mustExecContext(context.Background(), query, args...)
 }
 
+func (dbt *DBTest) mustExecT(t *testing.T, query string, args ...any) (res sql.Result) {
+	return dbt.mustExecContextT(context.Background(), t, query, args...)
+}
+
 func (dbt *DBTest) mustExecContext(ctx context.Context, query string, args ...interface{}) (res sql.Result) {
 	res, err := dbt.conn.ExecContext(ctx, query, args...)
 	if err != nil {
@@ -279,6 +291,14 @@ func (dbt *DBTest) mustExecContext(ctx context.Context, query string, args ...in
 	return res
 }
 
+func (dbt *DBTest) mustExecContextT(ctx context.Context, t *testing.T, query string, args ...any) (res sql.Result) {
+	res, err := dbt.conn.ExecContext(ctx, query, args...)
+	if err != nil {
+		t.Fatalf("exec context: query=%v, err=%v", query, err)
+	}
+	return res
+}
+
 func (dbt *DBTest) exec(query string, args ...any) (sql.Result, error) {
 	return dbt.conn.ExecContext(context.Background(), query, args...)
 }
diff --git a/structured_type_write_test.go b/structured_type_write_test.go
index d9f961c..8432335 100644
--- a/structured_type_write_test.go
+++ b/structured_type_write_test.go
@@ -2,6 +2,7 @@ package gosnowflake
 
 import (
 	"database/sql"
+	"fmt"
 	"reflect"
 	"testing"
 	"time"
@@ -215,7 +216,7 @@ func TestBindingObjectWithNullableFieldsWithSchema(t *testing.T) {
 				tz:   sql.NullTime{Time: time.Date(2027, time.May, 24, 11, 22, 33, 44, warsawTz), Valid: true},
 				so:   &simpleObject{s: "another string", i: 123},
 			}
-			dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+			dbt.mustExecT(t, "INSERT INTO test_object_binding SELECT (?)", o)
 			rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
 			defer rows.Close()
 
@@ -257,7 +258,7 @@ func TestBindingObjectWithNullableFieldsWithSchema(t *testing.T) {
 				tz:   sql.NullTime{},
 				so:   nil,
 			}
-			dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+			dbt.mustExecT(t, "INSERT INTO test_object_binding SELECT (?)", o)
 			rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
 			defer rows.Close()
 
@@ -374,7 +375,7 @@ func TestBindingObjectWithNullableFieldsWithSchemaSimpleWrite(t *testing.T) {
 				Tz:   sql.NullTime{Time: time.Date(2027, time.May, 24, 11, 22, 33, 44, warsawTz), Valid: true},
 				So:   &simpleObject{s: "another string", i: 123},
 			}
-			dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+			dbt.mustExecT(t, "INSERT INTO test_object_binding SELECT (?)", o)
 			rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
 			defer rows.Close()
 
@@ -416,7 +417,7 @@ func TestBindingObjectWithNullableFieldsWithSchemaSimpleWrite(t *testing.T) {
 				Tz:   sql.NullTime{},
 				So:   nil,
 			}
-			dbt.mustExec("INSERT INTO test_object_binding SELECT (?)", o)
+			dbt.mustExecT(t, "INSERT INTO test_object_binding SELECT (?)", o)
 			rows := dbt.mustQuery("SELECT * FROM test_object_binding WHERE obj = ?", o)
 			defer rows.Close()
 
@@ -532,3 +533,177 @@ func TestBindingNullStructuredObjects(t *testing.T) {
 		assertNilE(t, res)
 	})
 }
+
+func TestBindingArrayWithSchema(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.enableStructuredTypesBinding()
+		testcases := []struct {
+			name      string
+			arrayType string
+			values    []any
+			expected  any
+		}{
+			{
+				name:      "byte - empty",
+				arrayType: "TINYINT",
+				values:    []any{[]byte{}},
+				expected:  []int64{},
+			},
+			{
+				name:      "byte - not empty",
+				arrayType: "TINYINT",
+				values:    []any{[]byte{1, 2, 3}},
+				expected:  []int64{1, 2, 3},
+			},
+			{
+				name:      "int16",
+				arrayType: "SMALLINT",
+				values:    []any{[]int16{1, 2, 3}},
+				expected:  []int64{1, 2, 3},
+			},
+			{
+				name:      "int16 - empty",
+				arrayType: "SMALLINT",
+				values:    []any{[]int16{}},
+				expected:  []int64{},
+			},
+			{
+				name:      "int32",
+				arrayType: "INTEGER",
+				values:    []any{[]int32{1, 2, 3}},
+				expected:  []int64{1, 2, 3},
+			},
+			{
+				name:      "int64",
+				arrayType: "BIGINT",
+				values:    []any{[]int64{1, 2, 3}},
+				expected:  []int64{1, 2, 3},
+			},
+			{
+				name:      "float32",
+				arrayType: "FLOAT",
+				values:    []any{[]float32{1.2, 3.4}},
+				expected:  []float64{1.2, 3.4},
+			},
+			{
+				name:      "float64",
+				arrayType: "FLOAT",
+				values:    []any{[]float64{1.2, 3.4}},
+				expected:  []float64{1.2, 3.4},
+			},
+			{
+				name:      "bool",
+				arrayType: "BOOLEAN",
+				values:    []any{[]bool{true, false}},
+				expected:  []bool{true, false},
+			},
+			{
+				name:      "binary",
+				arrayType: "BINARY",
+				values:    []any{DataTypeBinary, [][]byte{{'a', 'b'}, {'c', 'd'}}},
+				expected:  [][]byte{{'a', 'b'}, {'c', 'd'}},
+			},
+			{
+				name:      "binary - empty",
+				arrayType: "BINARY",
+				values:    []any{DataTypeBinary, [][]byte{}},
+				expected:  [][]byte{},
+			},
+			{
+				name:      "date",
+				arrayType: "DATE",
+				values:    []any{DataTypeDate, []time.Time{time.Date(2024, time.June, 4, 0, 0, 0, 0, time.UTC)}},
+				expected:  []time.Time{time.Date(2024, time.June, 4, 0, 0, 0, 0, time.UTC)},
+			},
+		}
+		for _, tc := range testcases {
+			t.Run(tc.name, func(t *testing.T) {
+				dbt.mustExecT(t, fmt.Sprintf("CREATE OR REPLACE TABLE test_array_binding (arr ARRAY(%s))", tc.arrayType))
+				defer func() {
+					dbt.mustExec("DROP TABLE IF EXISTS test_array_binding")
+				}()
+
+				dbt.mustExec("INSERT INTO test_array_binding SELECT (?)", tc.values...)
+
+				rows := dbt.mustQuery("SELECT * FROM test_array_binding")
+				defer rows.Close()
+
+				assertTrueE(t, rows.Next())
+				var res any
+				err := rows.Scan(&res)
+				assertNilF(t, err)
+				assertDeepEqualE(t, res, tc.expected)
+			})
+		}
+	})
+}
+
+func TestBindingArrayOfObjects(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("CREATE OR REPLACE TABLE test_array_binding (arr ARRAY(OBJECT(s VARCHAR, i INTEGER)))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_array_binding")
+		}()
+
+		arr := []*simpleObject{{s: "some string", i: 123}}
+		dbt.mustExec("INSERT INTO test_array_binding SELECT (?)", arr)
+
+		rows := dbt.mustQuery("SELECT * FROM test_array_binding WHERE arr = ?", arr)
+		defer rows.Close()
+
+		assertTrueE(t, rows.Next())
+		var res []*simpleObject
+		err := rows.Scan(ScanArrayOfScanners(&res))
+		assertNilF(t, err)
+		assertDeepEqualE(t, res, arr)
+	})
+}
+
+func TestBindingEmptyArrayOfObjects(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("CREATE OR REPLACE TABLE test_array_binding (arr ARRAY(OBJECT(s VARCHAR, i INTEGER)))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_array_binding")
+		}()
+
+		arr := []*simpleObject{}
+		dbt.mustExec("INSERT INTO test_array_binding SELECT (?)", DataTypeEmptyArray, reflect.TypeOf(simpleObject{}))
+
+		rows := dbt.mustQuery("SELECT * FROM test_array_binding WHERE arr = ?", DataTypeEmptyArray, reflect.TypeOf(simpleObject{}))
+		defer rows.Close()
+
+		assertTrueF(t, rows.Next())
+		var res []*simpleObject
+		err := rows.Scan(ScanArrayOfScanners(&res))
+		assertNilF(t, err)
+		assertDeepEqualE(t, res, arr)
+	})
+}
+
+func TestBindingNilArrayOfObjects(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		dbt.enableStructuredTypesBinding()
+		dbt.mustExec("CREATE OR REPLACE TABLE test_array_binding (arr ARRAY(OBJECT(s VARCHAR, i INTEGER)))")
+		defer func() {
+			dbt.mustExec("DROP TABLE IF EXISTS test_array_binding")
+		}()
+
+		var arr []*simpleObject
+		dbt.mustExec("INSERT INTO test_array_binding SELECT (?)", arr)
+
+		rows := dbt.mustQuery("SELECT * FROM test_array_binding")
+		defer rows.Close()
+
+		assertTrueF(t, rows.Next())
+		var res []*simpleObject
+		err := rows.Scan(ScanArrayOfScanners(&res))
+		assertNilF(t, err)
+		assertDeepEqualE(t, res, arr)
+	})
+}