diff --git a/DOC/DATA_API.md b/DOC/DATA_API.md index 091a3f62..5076a90a 100644 --- a/DOC/DATA_API.md +++ b/DOC/DATA_API.md @@ -93,6 +93,7 @@ Response: { "results": [ { + "types": {"id": "integer", "age": "integer", "name": "text"}, "rows": [ { "id": 1, "age": 20, "name": "fiona"}, { "id": 2, "age": 25, "name": "declan"} diff --git a/command/encoding/json.go b/command/encoding/json.go index efac100f..7019c44a 100644 --- a/command/encoding/json.go +++ b/command/encoding/json.go @@ -3,11 +3,18 @@ package encoding import ( "bytes" "encoding/json" + "errors" "fmt" "github.com/rqlite/rqlite/command" ) +var ( + // ErrTypesColumnsLengthViolation is returned when a results + // object doesn't have the same number of types and columns + ErrTypesColumnsLengthViolation = errors.New("types and columns are different lengths") +) + // Result represents the outcome of an operation that changes rows. type Result struct { LastInsertID int64 `json:"last_insert_id,omitempty"` @@ -27,7 +34,7 @@ type Rows struct { // AssociativeRows represents the outcome of an operation that returns query data. type AssociativeRows struct { - Types []string `json:"types,omitempty"` + Types map[string]string `json:"types,omitempty"` Rows []map[string]interface{} `json:"rows,omitempty"` Error string `json:"error,omitempty"` Time float64 `json:"time,omitempty"` @@ -45,6 +52,10 @@ func NewResultFromExecuteResult(e *command.ExecuteResult) (*Result, error) { // NewRowsFromQueryRows returns an API Rows object from a QueryRows func NewRowsFromQueryRows(q *command.QueryRows) (*Rows, error) { + if len(q.Columns) != len(q.Types) { + return nil, ErrTypesColumnsLengthViolation + } + values := make([][]interface{}, len(q.Values)) if err := NewValuesFromQueryValues(values, q.Values); err != nil { return nil, err @@ -60,6 +71,10 @@ func NewRowsFromQueryRows(q *command.QueryRows) (*Rows, error) { // NewAssociativeRowsFromQueryRows returns an associative API object from a QueryRows func NewAssociativeRowsFromQueryRows(q *command.QueryRows) (*AssociativeRows, error) { + if len(q.Columns) != len(q.Types) { + return nil, ErrTypesColumnsLengthViolation + } + values := make([][]interface{}, len(q.Values)) if err := NewValuesFromQueryValues(values, q.Values); err != nil { return nil, err @@ -74,7 +89,13 @@ func NewAssociativeRowsFromQueryRows(q *command.QueryRows) (*AssociativeRows, er rows[i] = m } + types := make(map[string]string) + for i := range q.Types { + types[q.Columns[i]] = q.Types[i] + } + return &AssociativeRows{ + Types: types, Rows: rows, Error: q.Error, Time: q.Time, diff --git a/command/encoding/json_test.go b/command/encoding/json_test.go index 2301b37d..593f0f6f 100644 --- a/command/encoding/json_test.go +++ b/command/encoding/json_test.go @@ -97,6 +97,30 @@ func Test_MarshalExecuteResults(t *testing.T) { } } +// Test_MarshalQueryRowsError tests error cases +func Test_MarshalQueryRowsError(t *testing.T) { + var err error + var r *command.QueryRows + enc := Encoder{} + + r = &command.QueryRows{ + Columns: []string{"c1", "c2"}, + Types: []string{"int", "float", "string"}, + Time: 6789, + } + + _, err = enc.JSONMarshal(r) + if err != ErrTypesColumnsLengthViolation { + t.Fatalf("succeeded marshaling QueryRows: %s", err) + } + + enc.Associative = true + _, err = enc.JSONMarshal(r) + if err != ErrTypesColumnsLengthViolation { + t.Fatalf("succeeded marshaling QueryRows (associative): %s", err) + } +} + // Test_MarshalQueryRows tests JSON marshaling of a QueryRows func Test_MarshalQueryRows(t *testing.T) { var b []byte @@ -207,7 +231,7 @@ func Test_MarshalQueryAssociativeRows(t *testing.T) { if err != nil { t.Fatalf("failed to marshal QueryRows: %s", err.Error()) } - if exp, got := `{"rows":[{"c1":123,"c2":678,"c3":"fiona"}],"time":6789}`, string(b); exp != got { + if exp, got := `{"types":{"c1":"int","c2":"float","c3":"string"},"rows":[{"c1":123,"c2":678,"c3":"fiona"}],"time":6789}`, string(b); exp != got { t.Fatalf("failed to marshal QueryRows: exp %s, got %s", exp, got) } @@ -216,6 +240,11 @@ func Test_MarshalQueryAssociativeRows(t *testing.T) { t.Fatalf("failed to marshal QueryRows: %s", err.Error()) } exp := `{ + "types": { + "c1": "int", + "c2": "float", + "c3": "string" + }, "rows": [ { "c1": 123, @@ -312,7 +341,7 @@ func Test_MarshalQueryAssociativeRowses(t *testing.T) { if err != nil { t.Fatalf("failed to marshal QueryRows: %s", err.Error()) } - if exp, got := `[{"rows":[{"c1":123,"c2":678,"c3":"fiona"}],"time":6789},{"rows":[{"c1":123,"c2":678,"c3":"fiona"}],"time":6789}]`, string(b); exp != got { + if exp, got := `[{"types":{"c1":"int","c2":"float","c3":"string"},"rows":[{"c1":123,"c2":678,"c3":"fiona"}],"time":6789},{"types":{"c1":"int","c2":"float","c3":"string"},"rows":[{"c1":123,"c2":678,"c3":"fiona"}],"time":6789}]`, string(b); exp != got { t.Fatalf("failed to marshal QueryRows: exp %s, got %s", exp, got) } }