From 7d333c4def1e6a2d795f2ccdea43a8c62ac824b6 Mon Sep 17 00:00:00 2001 From: ErnestNeller Date: Thu, 24 Oct 2024 18:50:21 +0200 Subject: [PATCH 1/3] Add JSON and JSON array to convert to text. --- arrays.go | 1 + json.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++ json_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ struct.go | 1 + 4 files changed, 189 insertions(+) create mode 100644 json.go create mode 100644 json_test.go diff --git a/arrays.go b/arrays.go index 500f44b..c435cf3 100644 --- a/arrays.go +++ b/arrays.go @@ -6,6 +6,7 @@ import ( "fmt" ) +// Depcreated: use JSONArray instead type Array[T any] []T func (arr *Array[T]) Scan(value interface{}) error { diff --git a/json.go b/json.go new file mode 100644 index 0000000..62c15bc --- /dev/null +++ b/json.go @@ -0,0 +1,88 @@ +package sqltypes + +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +type JSON[T any] struct { + V *T +} + +func NewJSON[T any](val *T) JSON[T] { + return JSON[T]{V: val} +} + +func (s *JSON[T]) Scan(value interface{}) error { + if value == nil { + return nil + } + + var val T + switch value.(type) { + case []byte: + if err := json.Unmarshal(value.([]byte), &val); err != nil { + return fmt.Errorf("sqltypes: cannot unmarshal struct: %w", err) + } + case string: + if err := json.Unmarshal([]byte(value.(string)), &val); err != nil { + return fmt.Errorf("sqltypes: cannot unmarshal struct: %w", err) + } + default: + return fmt.Errorf("sqltypes: unknown array type %T", value) + } + *s = NewJSON(&val) + + return nil +} + +func (s JSON[T]) Value() (driver.Value, error) { + if s.V == nil { + return nil, nil + } + value, err := json.Marshal(s.V) + if err != nil { + return nil, err + } + return string(value), nil +} + +type JSONArray[T any] struct { + V T +} + +func NewJSONArray[T any](val *T) JSONArray[T] { + return JSONArray[T]{V: *val} +} + +func (s *JSONArray[T]) Scan(value interface{}) error { + if value == nil { + return nil + } + + var val T + switch value.(type) { + case []byte: + if err := json.Unmarshal(value.([]byte), &val); err != nil { + return fmt.Errorf("sqltypes: cannot unmarshal struct: %w", err) + } + case string: + if err := json.Unmarshal([]byte(value.(string)), &val); err != nil { + return fmt.Errorf("sqltypes: cannot unmarshal struct: %w", err) + } + default: + return fmt.Errorf("sqltypes: unknown array type %T", value) + } + *s = NewJSONArray(&val) + + return nil +} + +func (s JSONArray[T]) Value() (driver.Value, error) { + value, err := json.Marshal(s.V) + if err != nil { + return nil, err + } + return string(value), nil +} diff --git a/json_test.go b/json_test.go new file mode 100644 index 0000000..c56e811 --- /dev/null +++ b/json_test.go @@ -0,0 +1,99 @@ +package sqltypes + +import ( + "database/sql" + "testing" + + "github.com/stretchr/testify/require" +) + +type testJSON struct { + Foo string +} + +func TestJSONValue(t *testing.T) { + db := connectDB(t) + defer db.Close() + + var st JSON[testJSON] + st.V = &testJSON{Foo: "bar"} + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", st) + require.NoError(t, err) + + var val string + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&val)) + + require.Equal(t, `{"Foo":"bar"}`, val) +} + +func TestJSONValueNil(t *testing.T) { + db := connectDB(t) + defer db.Close() + + var st JSON[testJSON] + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", st) + require.NoError(t, err) + + var val sql.NullString + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&val)) + + require.False(t, val.Valid) +} + +func TestJSONScan(t *testing.T) { + db := connectDB(t) + defer db.Close() + + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", `{"Foo":"bar"}`) + require.NoError(t, err) + + var st JSON[testJSON] + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&st)) + + require.NotNil(t, st.V) + require.Equal(t, st.V.Foo, "bar") +} + +func TestJSONScanNil(t *testing.T) { + db := connectDB(t) + defer db.Close() + + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", nil) + require.NoError(t, err) + + var st JSON[testJSON] + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&st)) + + require.Nil(t, st.V) +} + +func TestJSONSaveLoad(t *testing.T) { + db := connectDB(t) + defer db.Close() + + var st JSON[testJSON] + st.V = &testJSON{Foo: "bar"} + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", st) + require.NoError(t, err) + + var other JSON[testJSON] + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&other)) + + require.NotNil(t, st.V) + require.Equal(t, st.V.Foo, "bar") +} + +func TestJSONArray(t *testing.T) { + db := connectDB(t) + defer db.Close() + + st := JSONArray[[]string]{V: []string{"foo", "bar"}} + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", st) + require.NoError(t, err) + + var other JSON[[]string] + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&other)) + + require.NotNil(t, st.V) + require.Len(t, st.V, 2) +} diff --git a/struct.go b/struct.go index 820740c..4376e11 100644 --- a/struct.go +++ b/struct.go @@ -14,6 +14,7 @@ func NewStruct[T any](val *T) Struct[T] { return Struct[T]{V: val} } +// Depcreated: use JSON instead func (s *Struct[T]) Scan(value interface{}) error { if value == nil { return nil From 079104281a9241d88cbf15cc22c6bd11f9c2db0e Mon Sep 17 00:00:00 2001 From: ErnestNeller Date: Thu, 24 Oct 2024 18:50:21 +0200 Subject: [PATCH 2/3] Use TEXT columns to serialize JSON structs & arrays. --- arrays.go | 1 + json.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++ json_test.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ struct.go | 1 + 4 files changed, 189 insertions(+) create mode 100644 json.go create mode 100644 json_test.go diff --git a/arrays.go b/arrays.go index 500f44b..43467ca 100644 --- a/arrays.go +++ b/arrays.go @@ -6,6 +6,7 @@ import ( "fmt" ) +// Deprecated: use JSONArray instead type Array[T any] []T func (arr *Array[T]) Scan(value interface{}) error { diff --git a/json.go b/json.go new file mode 100644 index 0000000..62c15bc --- /dev/null +++ b/json.go @@ -0,0 +1,88 @@ +package sqltypes + +import ( + "database/sql/driver" + "encoding/json" + "fmt" +) + +type JSON[T any] struct { + V *T +} + +func NewJSON[T any](val *T) JSON[T] { + return JSON[T]{V: val} +} + +func (s *JSON[T]) Scan(value interface{}) error { + if value == nil { + return nil + } + + var val T + switch value.(type) { + case []byte: + if err := json.Unmarshal(value.([]byte), &val); err != nil { + return fmt.Errorf("sqltypes: cannot unmarshal struct: %w", err) + } + case string: + if err := json.Unmarshal([]byte(value.(string)), &val); err != nil { + return fmt.Errorf("sqltypes: cannot unmarshal struct: %w", err) + } + default: + return fmt.Errorf("sqltypes: unknown array type %T", value) + } + *s = NewJSON(&val) + + return nil +} + +func (s JSON[T]) Value() (driver.Value, error) { + if s.V == nil { + return nil, nil + } + value, err := json.Marshal(s.V) + if err != nil { + return nil, err + } + return string(value), nil +} + +type JSONArray[T any] struct { + V T +} + +func NewJSONArray[T any](val *T) JSONArray[T] { + return JSONArray[T]{V: *val} +} + +func (s *JSONArray[T]) Scan(value interface{}) error { + if value == nil { + return nil + } + + var val T + switch value.(type) { + case []byte: + if err := json.Unmarshal(value.([]byte), &val); err != nil { + return fmt.Errorf("sqltypes: cannot unmarshal struct: %w", err) + } + case string: + if err := json.Unmarshal([]byte(value.(string)), &val); err != nil { + return fmt.Errorf("sqltypes: cannot unmarshal struct: %w", err) + } + default: + return fmt.Errorf("sqltypes: unknown array type %T", value) + } + *s = NewJSONArray(&val) + + return nil +} + +func (s JSONArray[T]) Value() (driver.Value, error) { + value, err := json.Marshal(s.V) + if err != nil { + return nil, err + } + return string(value), nil +} diff --git a/json_test.go b/json_test.go new file mode 100644 index 0000000..c56e811 --- /dev/null +++ b/json_test.go @@ -0,0 +1,99 @@ +package sqltypes + +import ( + "database/sql" + "testing" + + "github.com/stretchr/testify/require" +) + +type testJSON struct { + Foo string +} + +func TestJSONValue(t *testing.T) { + db := connectDB(t) + defer db.Close() + + var st JSON[testJSON] + st.V = &testJSON{Foo: "bar"} + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", st) + require.NoError(t, err) + + var val string + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&val)) + + require.Equal(t, `{"Foo":"bar"}`, val) +} + +func TestJSONValueNil(t *testing.T) { + db := connectDB(t) + defer db.Close() + + var st JSON[testJSON] + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", st) + require.NoError(t, err) + + var val sql.NullString + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&val)) + + require.False(t, val.Valid) +} + +func TestJSONScan(t *testing.T) { + db := connectDB(t) + defer db.Close() + + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", `{"Foo":"bar"}`) + require.NoError(t, err) + + var st JSON[testJSON] + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&st)) + + require.NotNil(t, st.V) + require.Equal(t, st.V.Foo, "bar") +} + +func TestJSONScanNil(t *testing.T) { + db := connectDB(t) + defer db.Close() + + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", nil) + require.NoError(t, err) + + var st JSON[testJSON] + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&st)) + + require.Nil(t, st.V) +} + +func TestJSONSaveLoad(t *testing.T) { + db := connectDB(t) + defer db.Close() + + var st JSON[testJSON] + st.V = &testJSON{Foo: "bar"} + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", st) + require.NoError(t, err) + + var other JSON[testJSON] + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&other)) + + require.NotNil(t, st.V) + require.Equal(t, st.V.Foo, "bar") +} + +func TestJSONArray(t *testing.T) { + db := connectDB(t) + defer db.Close() + + st := JSONArray[[]string]{V: []string{"foo", "bar"}} + _, err := db.Exec("INSERT INTO test_types (name, value_str) VALUES (?, ?)", "foo", st) + require.NoError(t, err) + + var other JSON[[]string] + require.NoError(t, db.QueryRow("SELECT value_str FROM test_types WHERE name = ?", "foo").Scan(&other)) + + require.NotNil(t, st.V) + require.Len(t, st.V, 2) +} diff --git a/struct.go b/struct.go index 820740c..80ea398 100644 --- a/struct.go +++ b/struct.go @@ -14,6 +14,7 @@ func NewStruct[T any](val *T) Struct[T] { return Struct[T]{V: val} } +// Deprecated: use JSON instead func (s *Struct[T]) Scan(value interface{}) error { if value == nil { return nil From 1b4df092a804f5dd51f5c218d8a05f49f6649013 Mon Sep 17 00:00:00 2001 From: ErnestNeller Date: Thu, 24 Oct 2024 18:58:21 +0200 Subject: [PATCH 3/3] Use TEXT columns to serialize JSON structs & arrays. --- arrays.go | 1 + struct.go | 1 + 2 files changed, 2 insertions(+) diff --git a/arrays.go b/arrays.go index 500f44b..43467ca 100644 --- a/arrays.go +++ b/arrays.go @@ -6,6 +6,7 @@ import ( "fmt" ) +// Deprecated: use JSONArray instead type Array[T any] []T func (arr *Array[T]) Scan(value interface{}) error { diff --git a/struct.go b/struct.go index 820740c..a330215 100644 --- a/struct.go +++ b/struct.go @@ -6,6 +6,7 @@ import ( "fmt" ) +// Deprecated: use JSON instead type Struct[T any] struct { V *T }