Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dbutil.ScanAllJSON #100

Merged
merged 1 commit into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion dbutil/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,22 @@
return nil
}

// ScanAllSlice scans a single value from each single column row into the given slice
// ScanAllJSON scans all rows as a single column containing JSON that be unmarshalled into instances of V.
func ScanAllJSON[V any](rows *sql.Rows, s []V) ([]V, error) {
defer rows.Close()

var v V

for rows.Next() {
if err := ScanJSON(rows, &v); err != nil {
return nil, err
}

Check warning on line 52 in dbutil/scan.go

View check run for this annotation

Codecov / codecov/patch

dbutil/scan.go#L51-L52

Added lines #L51 - L52 were not covered by tests
s = append(s, v)
}
return s, rows.Err()
}

// ScanAllSlice scans all rows as a single value and returns them in the given slice.
func ScanAllSlice[V any](rows *sql.Rows, s []V) ([]V, error) {
defer rows.Close()

Expand Down
25 changes: 22 additions & 3 deletions dbutil/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ func TestScanJSON(t *testing.T) {
queryRows := func(sql string, args ...any) *sql.Rows {
rows, err := db.QueryContext(ctx, sql, args...)
require.NoError(t, err)
require.True(t, rows.Next())
return rows
}

// if query returns valid JSON which can be unmarshaled into our struct, all good
rows := queryRows(`SELECT ROW_TO_JSON(r) FROM (SELECT f.uuid, f.name, f.age FROM foo f WHERE id = 1) r`)
require.True(t, rows.Next())

f := &foo{}
err := dbutil.ScanAndValidateJSON(rows, f)
Expand All @@ -47,6 +47,7 @@ func TestScanJSON(t *testing.T) {
assert.Equal(t, 40, f.Age)

rows = queryRows(`SELECT ROW_TO_JSON(r) FROM (SELECT f.uuid, f.name, f.age FROM foo f WHERE id = 2) r`)
require.True(t, rows.Next())

err = dbutil.ScanAndValidateJSON(rows, f)
assert.NoError(t, err)
Expand All @@ -56,34 +57,52 @@ func TestScanJSON(t *testing.T) {

// error if row value is not JSON
rows = queryRows(`SELECT id FROM foo f WHERE id = 1`)
require.True(t, rows.Next())

err = dbutil.ScanAndValidateJSON(rows, f)
assert.EqualError(t, err, `error scanning row JSON: sql: Scan error on column index 0, name "id": unsupported Scan, storing driver.Value type int64 into type *json.RawMessage`)

// error if we can't marshal into the struct
rows = queryRows(`SELECT ROW_TO_JSON(r) FROM (SELECT f.uuid as uuid, f.name AS age FROM foo f WHERE id = 1) r`)
require.True(t, rows.Next())

err = dbutil.ScanAndValidateJSON(rows, f)
assert.EqualError(t, err, "error unmarshalling row JSON: json: cannot unmarshal string into Go struct field foo.age of type int")

// error if rows aren't ready to be scanned - e.g. next hasn't been called
rows, err = db.QueryContext(ctx, `SELECT ROW_TO_JSON(r) FROM (SELECT f.uuid as uuid, f.name AS name FROM foo f WHERE id = 1) r`)
require.NoError(t, err)
rows = queryRows(`SELECT ROW_TO_JSON(r) FROM (SELECT f.uuid as uuid, f.name AS name FROM foo f WHERE id = 1) r`)

err = dbutil.ScanAndValidateJSON(rows, f)
assert.EqualError(t, err, "error scanning row JSON: sql: Scan called without calling Next")

// error if we request validation and returned JSON is invalid
rows = queryRows(`SELECT ROW_TO_JSON(r) FROM (SELECT f.uuid, f.name, f.age FROM foo f WHERE id = 3) r`)
require.True(t, rows.Next())

err = dbutil.ScanAndValidateJSON(rows, f)
assert.EqualError(t, err, "error validating unmarsalled JSON: Key: 'foo.Age' Error:Field validation for 'Age' failed on the 'min' tag")

rows.Close()

// no error if we don't do validation
rows = queryRows(`SELECT ROW_TO_JSON(r) FROM (SELECT f.uuid, f.name, f.age FROM foo f WHERE id = 3) r`)
require.True(t, rows.Next())

err = dbutil.ScanJSON(rows, f)
assert.NoError(t, err)
assert.Equal(t, "a5850c89-dd29-46f6-9de1-d068b3c2db94", f.UUID)
assert.Equal(t, "George", f.Name)
assert.Equal(t, -1, f.Age)

rows.Close()

// can all scan all rows with ScanAllJSON
rows = queryRows(`SELECT ROW_TO_JSON(r) FROM (SELECT f.uuid, f.name, f.age FROM foo f) r`)

var foos []*foo
foos, err = dbutil.ScanAllJSON(rows, foos)
assert.NoError(t, err)
assert.Len(t, foos, 3)
}

func TestScanAllSlice(t *testing.T) {
Expand Down
Loading