diff --git a/pkg/db/vsc.go b/pkg/db/vsc.go new file mode 100644 index 0000000..85c274c --- /dev/null +++ b/pkg/db/vsc.go @@ -0,0 +1,51 @@ +package db + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "strings" +) + +const separator = "\x1f" + +type VSC []string + +// MarshalJSON replaces `nil` with `[]` for hygiene. +func (vs VSC) MarshalJSON() ([]byte, error) { + if vs == nil { + vs = VSC{} + } + return json.Marshal([]string(vs)) //nolint: wrapcheck // Passthrough +} + +func (vs VSC) Value() (driver.Value, error) { + fields := make([]string, len(vs)) + for i, v := range vs { + fields[i] = strings.ReplaceAll(v, separator, "") + } + return strings.Join(fields, separator), nil +} + +var ErrWrongVSCType = errors.New("incompatible DB type for VSC") + +func (vs *VSC) Scan(src any) error { + var s string + switch v := src.(type) { + case string: + s = v + case []byte: + s = string(v) + case nil: + *vs = nil + return nil + default: + return ErrWrongVSCType + } + if s == "" { + *vs = VSC{} + return nil + } + *vs = strings.Split(s, separator) + return nil +} diff --git a/pkg/db/vsc_test.go b/pkg/db/vsc_test.go new file mode 100644 index 0000000..cfee133 --- /dev/null +++ b/pkg/db/vsc_test.go @@ -0,0 +1,48 @@ +package db_test + +import ( + "testing" + + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" + "github.com/stretchr/testify/require" + + "git.offline-twitter.com/offline-labs/gas-stack/pkg/db" +) + +func TestVSCDBRoundTrip(t *testing.T) { + conn, err := sqlx.Open("sqlite3", ":memory:") + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, conn.Close()) }) + + _, err = conn.Exec(`create table vsc_items (id integer primary key, items text not null)`) + require.NoError(t, err) + + type vscRow struct { + ID int `db:"id"` + Items db.VSC `db:"items"` + } + + for _, want := range []vscRow{ + {ID: 1, Items: db.VSC{"email"}}, + {ID: 2, Items: db.VSC{"email", "profile"}}, + {ID: 3, Items: db.VSC{}}, + } { + _, err = conn.NamedExec(`insert into vsc_items (id, items) values (:id, :items)`, want) + require.NoError(t, err) + + var got vscRow + require.NoError(t, conn.Get(&got, `select id, items from vsc_items where id = ?`, want.ID)) + require.Equal(t, want, got) + } + t.Run("nil gets upgrade to empty list", func(t *testing.T) { + want := vscRow{ID: 4, Items: nil} + _, err = conn.NamedExec(`insert into vsc_items (id, items) values (:id, :items)`, want) + require.NoError(t, err) + + var got vscRow + require.NoError(t, conn.Get(&got, `select id, items from vsc_items where id = ?`, want.ID)) + require.NotNil(t, got.Items) + require.Equal(t, db.VSC{}, got.Items) + }) +}