Repository: snowflakedb/gosnowflake
Pull request #1140 (2024-05-22 09:45:38)

SNOW-1416000 Simplify structured objects reading
### Description

SNOW-1416000 Simplified structured objects reading using `ScanTo` function.
### 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/1140?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 `153 lines` in your changes are missing coverage. Please review.
> Project coverage is 74.42%. Comparing base [(`a0145ec`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/commit/a0145ec5c912210edfe96257efc83d3929769fe2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=snowflakedb) to head [(`11eb7c1`)](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1140?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/1140?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/1140?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% | [153 Missing :warning: ](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1140?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    #1140      +/-   ##
==========================================
- Coverage   75.44%   74.42%   -1.02%     
==========================================
  Files          53       53              
  Lines       11272    11425     +153     
==========================================
- Hits         8504     8503       -1     
- Misses       2768     2922     +154     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/snowflakedb/gosnowflake/pull/1140?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/doc.go b/doc.go
index 3cf9339..1f590da 100644
--- a/doc.go
+++ b/doc.go
@@ -453,15 +453,15 @@ Example table definition:
 
 To retrieve structured objects, follow these steps:
 
-1. Create a struct, example:
+1. Create a struct implementing sql.Scanner interface, example:
+
+a)
 
 	type simpleObject struct {
 		s string
 		i int32
 	}
 
-2. Implement sql.Scanner interface:
-
 	func (so *simpleObject) Scan(val any) error {
 		st := val.(StructuredObject)
 		var err error
@@ -474,7 +474,27 @@ To retrieve structured objects, follow these steps:
 		return nil
 	}
 
-3. Use it in regular scan:
+b)
+
+	type simpleObject struct {
+		S string `sf:"otherName"`
+		I int32 `sf:"i,ignore"`
+	}
+
+	func (so *simpleObject) Scan(val any) error {
+		st := val.(StructuredObject)
+		return st.ScanTo(so)
+	}
+
+Automatic scan goes through all fields in a struct and read object fields.
+Struct fields have to be public.
+Embedded structs have to be pointers.
+Matching name is built using struct field name with first letter lowercase.
+Additionally, `sf` tag can be added:
+- first value is always a name of a field in an SQL object
+- additionally `ignore` parameter can be passed to omit this field
+
+2. Use it in regular scan:
 
 	var res simpleObject
 	err := rows.Scan(&res)
diff --git a/structured_type.go b/structured_type.go
index 3e5750e..e5eba58 100644
--- a/structured_type.go
+++ b/structured_type.go
@@ -10,7 +10,9 @@ import (
 	"math/big"
 	"reflect"
 	"strconv"
+	"strings"
 	"time"
+	"unicode"
 )
 
 // ObjectType Empty marker of an object used in column type ScanType function
@@ -41,6 +43,7 @@ type StructuredObject interface {
 	GetNullTime(fieldName string) (sql.NullTime, error)
 	GetStruct(fieldName string, scanner sql.Scanner) (sql.Scanner, error)
 	GetRaw(fieldName string) (any, error)
+	ScanTo(sc sql.Scanner) error
 }
 
 // ArrayOfScanners Helper type for scanning array of sql.Scanner values.
@@ -388,6 +391,153 @@ func (st *structuredType) GetRaw(fieldName string) (any, error) {
 	return st.values[fieldName], nil
 }
 
+func (st *structuredType) ScanTo(sc sql.Scanner) error {
+	v := reflect.Indirect(reflect.ValueOf(sc))
+	t := v.Type()
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		if st.shouldIgnoreField(field) {
+			continue
+		}
+		switch field.Type.Kind() {
+		case reflect.String:
+			s, err := st.GetString(st.getFieldName(field))
+			if err != nil {
+				return err
+			}
+			v.FieldByName(field.Name).SetString(s)
+		case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+			i, err := st.GetInt64(st.getFieldName(field))
+			if err != nil {
+				return err
+			}
+			v.FieldByName(field.Name).SetInt(i)
+		case reflect.Uint8:
+			b, err := st.GetByte(st.getFieldName(field))
+			if err != nil {
+				return err
+			}
+			v.FieldByName(field.Name).SetUint(uint64(int64(b)))
+		case reflect.Float32, reflect.Float64:
+			f, err := st.GetFloat64(st.getFieldName(field))
+			if err != nil {
+				return err
+			}
+			v.FieldByName(field.Name).SetFloat(f)
+		case reflect.Bool:
+			b, err := st.GetBool(st.getFieldName(field))
+			if err != nil {
+				return err
+			}
+			v.FieldByName(field.Name).SetBool(b)
+		case reflect.Slice, reflect.Array:
+			switch field.Type.Elem().Kind() {
+			case reflect.Uint8:
+				b, err := st.GetBytes(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).SetBytes(b)
+			default:
+				raw, err := st.GetRaw(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				if raw != nil {
+					v.FieldByName(field.Name).Set(reflect.ValueOf(raw))
+				}
+			}
+		case reflect.Map:
+			raw, err := st.GetRaw(st.getFieldName(field))
+			if err != nil {
+				return err
+			}
+			if raw != nil {
+				v.FieldByName(field.Name).Set(reflect.ValueOf(raw))
+			}
+		case reflect.Struct:
+			a := v.FieldByName(field.Name).Interface()
+			if _, ok := a.(time.Time); ok {
+				time, err := st.GetTime(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.ValueOf(time))
+			} else if _, ok := a.(sql.Scanner); ok {
+				scanner := reflect.New(reflect.TypeOf(a)).Interface().(sql.Scanner)
+				s, err := st.GetStruct(st.getFieldName(field), scanner)
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.Indirect(reflect.ValueOf(s)))
+			} else if _, ok := a.(sql.NullString); ok {
+				ns, err := st.GetNullString(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.ValueOf(ns))
+			} else if _, ok := a.(sql.NullByte); ok {
+				nb, err := st.GetNullByte(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.ValueOf(nb))
+			} else if _, ok := a.(sql.NullBool); ok {
+				nb, err := st.GetNullBool(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.ValueOf(nb))
+			} else if _, ok := a.(sql.NullInt16); ok {
+				ni, err := st.GetNullInt16(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.ValueOf(ni))
+			} else if _, ok := a.(sql.NullInt32); ok {
+				ni, err := st.GetNullInt32(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.ValueOf(ni))
+			} else if _, ok := a.(sql.NullInt64); ok {
+				ni, err := st.GetNullInt64(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.ValueOf(ni))
+			} else if _, ok := a.(sql.NullFloat64); ok {
+				nf, err := st.GetNullFloat64(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.ValueOf(nf))
+			} else if _, ok := a.(sql.NullTime); ok {
+				nt, err := st.GetNullTime(st.getFieldName(field))
+				if err != nil {
+					return err
+				}
+				v.FieldByName(field.Name).Set(reflect.ValueOf(nt))
+			}
+		case reflect.Pointer:
+			switch field.Type.Elem().Kind() {
+			case reflect.Struct:
+				a := reflect.New(field.Type.Elem()).Interface()
+				s, err := st.GetStruct(st.getFieldName(field), a.(sql.Scanner))
+				if err != nil {
+					return err
+				}
+				if s != nil {
+					v.FieldByName(field.Name).Set(reflect.ValueOf(s))
+				}
+			default:
+				return errors.New("only struct pointers are supported")
+			}
+		}
+	}
+	return nil
+}
+
 func (st *structuredType) fieldMetadataByFieldName(fieldName string) (fieldMetadata, error) {
 	for _, fm := range st.fieldMetadata {
 		if fm.Name == fieldName {
@@ -405,3 +555,21 @@ func mapValuesNullableEnabled(ctx context.Context) bool {
 	d, ok := v.(bool)
 	return ok && d
 }
+
+func (st *structuredType) getFieldName(field reflect.StructField) string {
+	sfTag := field.Tag.Get("sf")
+	if sfTag != "" {
+		return strings.Split(sfTag, ",")[0]
+	}
+	r := []rune(field.Name)
+	r[0] = unicode.ToLower(r[0])
+	return string(r)
+}
+
+func (st *structuredType) shouldIgnoreField(field reflect.StructField) bool {
+	sfTag := field.Tag.Get("sf")
+	if sfTag == "" {
+		return false
+	}
+	return contains(strings.Split(sfTag, ",")[1:], "ignore")
+}


Test patch:
diff --git a/structured_type_read_test.go b/structured_type_read_test.go
index 0003610..e576522 100644
--- a/structured_type_read_test.go
+++ b/structured_type_read_test.go
@@ -170,6 +170,32 @@ func TestObjectWithAllTypes(t *testing.T) {
 	})
 }
 
+func TestNullObject(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			t.Run("null", func(t *testing.T) {
+				rows := dbt.mustQuery("SELECT null::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), sArr ARRAY(VARCHAR), f64Arr ARRAY(DOUBLE), someMap MAP(VARCHAR, BOOLEAN))")
+				defer rows.Close()
+				assertTrueF(t, rows.Next())
+				var res *objectWithAllTypes
+				err := rows.Scan(&res)
+				assertNilF(t, err)
+				assertNilE(t, res)
+			})
+			t.Run("not null", func(t *testing.T) {
+				rows := dbt.mustQuery("SELECT {'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}, 'sArr': ARRAY_CONSTRUCT('x', 'y', 'z'), 'f64Arr': ARRAY_CONSTRUCT(1.1, 2.2, 3.3), 'someMap': {'x': true, 'y': false}}::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), sArr ARRAY(VARCHAR), f64Arr ARRAY(DOUBLE), someMap MAP(VARCHAR, BOOLEAN))")
+				defer rows.Close()
+				assertTrueF(t, rows.Next())
+				var res *objectWithAllTypes
+				err := rows.Scan(&res)
+				assertNilF(t, err)
+				assertEqualE(t, res.s, "some string")
+			})
+		})
+	})
+}
+
 type objectWithAllTypesNullable struct {
 	s       sql.NullString
 	b       sql.NullByte
@@ -334,6 +360,218 @@ func TestObjectWithAllTypesNullable(t *testing.T) {
 	})
 }
 
+type objectWithAllTypesSimpleScan struct {
+	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
+	SArr      []string
+	F64Arr    []float64
+	SomeMap   map[string]bool
+}
+
+func (so *objectWithAllTypesSimpleScan) Scan(val any) error {
+	st := val.(StructuredObject)
+	return st.ScanTo(so)
+}
+
+func TestObjectWithAllTypesSimpleScan(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) {
+			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}, 'sArr': ARRAY_CONSTRUCT('x', 'y', 'z'), 'f64Arr': ARRAY_CONSTRUCT(1.1, 2.2, 3.3), 'someMap': {'x': true, 'y': false}}::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), sArr ARRAY(VARCHAR), f64Arr ARRAY(DOUBLE), someMap MAP(VARCHAR, BOOLEAN))")
+			defer rows.Close()
+			rows.Next()
+			var ignore int
+			var res objectWithAllTypesSimpleScan
+			err := rows.Scan(&ignore, &res)
+			assertNilF(t, err)
+			assertEqualE(t, res.S, "some string")
+			assertEqualE(t, res.B, byte(1))
+			assertEqualE(t, res.I16, int16(2))
+			assertEqualE(t, res.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))
+			assertEqualE(t, res.Time.Hour(), 13)
+			assertEqualE(t, res.Time.Minute(), 3)
+			assertEqualE(t, res.Time.Second(), 2)
+			assertTrueE(t, res.Ltz.Equal(time.Date(2021, time.July, 21, 11, 22, 33, 0, warsawTz)))
+			assertTrueE(t, res.Tz.Equal(time.Date(2022, time.August, 31, 13, 43, 22, 0, warsawTz)))
+			assertTrueE(t, res.Ntz.Equal(time.Date(2023, time.May, 22, 1, 17, 19, 0, time.UTC)))
+			assertDeepEqualE(t, res.So, &simpleObject{s: "child", i: 9})
+			assertDeepEqualE(t, res.SArr, []string{"x", "y", "z"})
+			assertDeepEqualE(t, res.F64Arr, []float64{1.1, 2.2, 3.3})
+			assertDeepEqualE(t, res.SomeMap, map[string]bool{"x": true, "y": false})
+		})
+	})
+}
+
+func TestNullObjectSimpleScan(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			t.Run("null", func(t *testing.T) {
+				rows := dbt.mustQuery("SELECT null::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), sArr ARRAY(VARCHAR), f64Arr ARRAY(DOUBLE), someMap MAP(VARCHAR, BOOLEAN))")
+				defer rows.Close()
+				assertTrueF(t, rows.Next())
+				var res *objectWithAllTypesSimpleScan
+				err := rows.Scan(&res)
+				assertNilF(t, err)
+				assertNilE(t, res)
+			})
+			t.Run("not null", func(t *testing.T) {
+				rows := dbt.mustQuery("SELECT {'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}, 'sArr': ARRAY_CONSTRUCT('x', 'y', 'z'), 'f64Arr': ARRAY_CONSTRUCT(1.1, 2.2, 3.3), 'someMap': {'x': true, 'y': false}}::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), sArr ARRAY(VARCHAR), f64Arr ARRAY(DOUBLE), someMap MAP(VARCHAR, BOOLEAN))")
+				defer rows.Close()
+				assertTrueF(t, rows.Next())
+				var res *objectWithAllTypesSimpleScan
+				err := rows.Scan(&res)
+				assertNilF(t, err)
+				assertEqualE(t, res.S, "some string")
+			})
+		})
+	})
+}
+
+type objectWithAllTypesNullableSimpleScan 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
+	SArr    []string
+	F64Arr  []float64
+	SomeMap map[string]bool
+}
+
+func (o *objectWithAllTypesNullableSimpleScan) Scan(val any) error {
+	st := val.(StructuredObject)
+	return st.ScanTo(o)
+}
+
+func TestObjectWithAllTypesSimpleScanNullable(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, 'sArr', null, 'f64Arr', null, 'someMap', 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), sArr ARRAY(VARCHAR), f64Arr ARRAY(DOUBLE), someMap MAP(VARCHAR, BOOLEAN))")
+				defer rows.Close()
+				rows.Next()
+				var ignore sql.NullInt32
+				var res objectWithAllTypesNullableSimpleScan
+				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, 'sArr', ARRAY_CONSTRUCT('x', 'y', 'z'), 'f64Arr', ARRAY_CONSTRUCT(1.1, 2.2, 3.3), 'someMap', {'x': true, 'y': false})::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), sArr ARRAY(VARCHAR), f64Arr ARRAY(DOUBLE), someMap MAP(VARCHAR, BOOLEAN))")
+				defer rows.Close()
+				rows.Next()
+				var ignore sql.NullInt32
+				var res objectWithAllTypesNullableSimpleScan
+				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})
+				assertDeepEqualE(t, res.SArr, []string{"x", "y", "z"})
+				assertDeepEqualE(t, res.F64Arr, []float64{1.1, 2.2, 3.3})
+				assertDeepEqualE(t, res.SomeMap, map[string]bool{"x": true, "y": false})
+			})
+		})
+	})
+}
+
+type objectWithCustomNameAndIgnoredField struct {
+	SomeString string `sf:"anotherName"`
+	IgnoreMe   string `sf:"ignoreMe,ignore"`
+}
+
+func (o *objectWithCustomNameAndIgnoredField) Scan(val any) error {
+	st := val.(StructuredObject)
+	return st.ScanTo(o)
+}
+
+func TestObjectWithCustomName(t *testing.T) {
+	skipStructuredTypesTestsOnGHActions(t)
+	runDBTest(t, func(dbt *DBTest) {
+		forAllStructureTypeFormats(dbt, func(t *testing.T, format string) {
+			rows := dbt.mustQuery("SELECT {'anotherName': 'some string'}::OBJECT(anotherName VARCHAR)")
+			defer rows.Close()
+			rows.Next()
+			var res objectWithCustomNameAndIgnoredField
+			err := rows.Scan(&res)
+			assertNilF(t, err)
+			assertEqualE(t, res.SomeString, "some string")
+			assertEqualE(t, res.IgnoreMe, "")
+		})
+	})
+}
+
 func TestObjectMetadata(t *testing.T) {
 	skipStructuredTypesTestsOnGHActions(t)
 	runDBTest(t, func(dbt *DBTest) {