Skip to content

Commit 9b1a758

Browse files
committed
Add POST /query. It calls gitbase and returns json
Signed-off-by: Carlos Martín <[email protected]>
1 parent 66fd811 commit 9b1a758

File tree

4 files changed

+244
-9
lines changed

4 files changed

+244
-9
lines changed

cmd/server/main.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,33 @@
11
package main
22

33
import (
4+
"database/sql"
45
"fmt"
56
"net/http"
67

78
"github.com/src-d/gitbase-playground/server"
89
"github.com/src-d/gitbase-playground/server/handler"
910
"github.com/src-d/gitbase-playground/server/service"
1011

12+
_ "github.com/go-sql-driver/mysql"
1113
"github.com/kelseyhightower/envconfig"
1214
)
1315

1416
// version will be replaced automatically by the CI build.
1517
// See https://github.com/src-d/ci/blob/v1/Makefile.main#L56
1618
var version = "dev"
1719

20+
// Note: maxAllowedPacket must be explicitly set for go-sql-driver/mysql v1.3.
21+
// Otherwise gitbase will be asked for the max_allowed_packet column and the
22+
// query will fail.
23+
// The next release should make this parameter optional for us:
24+
// https://github.com/go-sql-driver/mysql/pull/680
1825
type appConfig struct {
1926
Env string `envconfig:"ENV" default:"production"`
2027
Host string `envconfig:"HOST" default:"0.0.0.0"`
2128
Port int `envconfig:"PORT" default:"8080"`
2229
ServerURL string `envconfig:"SERVER_URL"`
30+
DBConn string `envconfig:"DB_CONNECTION" default:"root@tcp(localhost:3306)/none?maxAllowedPacket=4194304"`
2331
}
2432

2533
func main() {
@@ -33,11 +41,18 @@ func main() {
3341
// logger
3442
logger := service.NewLogger(conf.Env)
3543

44+
// database
45+
db, err := sql.Open("mysql", conf.DBConn)
46+
if err != nil {
47+
logger.Fatalf("error opening the database: %s", err)
48+
}
49+
defer db.Close()
50+
3651
static := handler.NewStatic("build", conf.ServerURL)
3752

3853
// start the router
39-
router := server.Router(logger, static, version)
54+
router := server.Router(logger, static, version, db)
4055
logger.Infof("listening on %s:%d", conf.Host, conf.Port)
41-
err := http.ListenAndServe(fmt.Sprintf("%s:%d", conf.Host, conf.Port), router)
56+
err = http.ListenAndServe(fmt.Sprintf("%s:%d", conf.Host, conf.Port), router)
4257
logger.Fatal(err)
4358
}

server/handler/query.go

+198
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package handler
2+
3+
import (
4+
"database/sql"
5+
"encoding/json"
6+
"fmt"
7+
"io/ioutil"
8+
"net/http"
9+
"strings"
10+
11+
"github.com/src-d/gitbase-playground/server/serializer"
12+
"gopkg.in/bblfsh/sdk.v1/uast"
13+
14+
"github.com/go-sql-driver/mysql"
15+
)
16+
17+
type queryRequest struct {
18+
Query string `json:"query"`
19+
Limit int `json:"limit,omitempty"`
20+
}
21+
22+
// genericVals returns a slice of interface{}, each one a pointer to the proper
23+
// type for each column
24+
func genericVals(colTypes []string) []interface{} {
25+
columnValsPtr := make([]interface{}, len(colTypes))
26+
27+
for i, colType := range colTypes {
28+
switch colType {
29+
case "BIT":
30+
columnValsPtr[i] = new(sql.NullBool)
31+
case "TIMESTAMP", "DATE", "DATETIME":
32+
columnValsPtr[i] = new(mysql.NullTime)
33+
case "INT", "MEDIUMINT", "BIGINT", "SMALLINT", "TINYINT":
34+
columnValsPtr[i] = new(sql.NullInt64)
35+
case "DOUBLE", "FLOAT":
36+
columnValsPtr[i] = new(sql.NullFloat64)
37+
case "JSON":
38+
columnValsPtr[i] = new([]byte)
39+
default: // All the text and binary variations
40+
columnValsPtr[i] = new(sql.NullString)
41+
}
42+
}
43+
44+
return columnValsPtr
45+
}
46+
47+
// Query returns a function that forwards an SQL query to gitbase and returns
48+
// the rows as JSON
49+
func Query(db *sql.DB) RequestProcessFunc {
50+
return func(r *http.Request) (*serializer.Response, error) {
51+
var queryRequest queryRequest
52+
body, err := ioutil.ReadAll(r.Body)
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
err = json.Unmarshal(body, &queryRequest)
58+
if err != nil {
59+
return nil, serializer.NewHTTPError(http.StatusBadRequest,
60+
`Bad Request. Expected body: { "query": "SQL statement", "limit": 1234 }`)
61+
}
62+
63+
query := addLimit(queryRequest.Query, queryRequest.Limit)
64+
rows, err := db.Query(query)
65+
if err != nil {
66+
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
67+
return nil, serializer.NewMySQLError(
68+
http.StatusBadRequest,
69+
mysqlErr.Number,
70+
mysqlErr.Message)
71+
}
72+
73+
return nil, serializer.NewHTTPError(http.StatusBadRequest, err.Error())
74+
}
75+
defer rows.Close()
76+
77+
columnNames, columnTypes, err := columnsInfo(rows)
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
columnValsPtr := genericVals(columnTypes)
83+
84+
tableData := make([]map[string]interface{}, 0)
85+
86+
for rows.Next() {
87+
if err := rows.Scan(columnValsPtr...); err != nil {
88+
return nil, err
89+
}
90+
91+
colData := make(map[string]interface{}, len(columnTypes))
92+
93+
for i, val := range columnValsPtr {
94+
colData[columnNames[i]] = nil
95+
96+
switch val.(type) {
97+
case *sql.NullBool:
98+
sqlVal, _ := val.(*sql.NullBool)
99+
if sqlVal.Valid {
100+
colData[columnNames[i]] = sqlVal.Bool
101+
}
102+
case *mysql.NullTime:
103+
sqlVal, _ := val.(*mysql.NullTime)
104+
if sqlVal.Valid {
105+
colData[columnNames[i]] = sqlVal.Time
106+
}
107+
case *sql.NullInt64:
108+
sqlVal, _ := val.(*sql.NullInt64)
109+
if sqlVal.Valid {
110+
colData[columnNames[i]] = sqlVal.Int64
111+
}
112+
case *sql.NullString:
113+
sqlVal, _ := val.(*sql.NullString)
114+
if sqlVal.Valid {
115+
colData[columnNames[i]] = sqlVal.String
116+
}
117+
case *[]byte:
118+
// DatabaseTypeName JSON is used for arrays of uast nodes and
119+
// arrays of strings, but we don't know the exact type.
120+
// We try with arry of uast nodes first and any JSON later
121+
nodes, err := unmarshallUAST(val)
122+
if err == nil {
123+
colData[columnNames[i]] = nodes
124+
} else {
125+
var data interface{}
126+
127+
if err := json.Unmarshal(*val.(*[]byte), &data); err != nil {
128+
return nil, err
129+
}
130+
colData[columnNames[i]] = data
131+
}
132+
}
133+
}
134+
135+
tableData = append(tableData, colData)
136+
}
137+
138+
if err := rows.Err(); err != nil {
139+
return nil, err
140+
}
141+
142+
return serializer.NewQueryResponse(tableData, columnNames, columnTypes), nil
143+
}
144+
}
145+
146+
// columnsInfo returns the column names and column types, or error
147+
func columnsInfo(rows *sql.Rows) ([]string, []string, error) {
148+
names, err := rows.Columns()
149+
if err != nil {
150+
return nil, nil, err
151+
}
152+
153+
types, err := rows.ColumnTypes()
154+
if err != nil {
155+
return nil, nil, err
156+
}
157+
158+
typesStr := make([]string, len(types))
159+
for i, colType := range types {
160+
typesStr[i] = colType.DatabaseTypeName()
161+
}
162+
163+
return names, typesStr, nil
164+
}
165+
166+
// unmarshallUAST tries to cast data as [][]byte and unmarshall uast nodes
167+
func unmarshallUAST(data interface{}) ([]*uast.Node, error) {
168+
var protobufs [][]byte
169+
if err := json.Unmarshal(*data.(*[]byte), &protobufs); err != nil {
170+
return nil, err
171+
}
172+
173+
nodes := make([]*uast.Node, len(protobufs))
174+
175+
for i, v := range protobufs {
176+
node := uast.NewNode()
177+
if err := node.Unmarshal(v); err != nil {
178+
return nil, err
179+
}
180+
nodes[i] = node
181+
}
182+
183+
return nodes, nil
184+
}
185+
186+
// addLimit adds LIMIT to the query if it's a SELECT, avoiding '; limit'
187+
func addLimit(query string, limit int) string {
188+
if limit <= 0 {
189+
return query
190+
}
191+
192+
query = strings.TrimRight(strings.TrimSpace(query), ";")
193+
if strings.HasPrefix(strings.ToUpper(query), "SELECT") {
194+
return fmt.Sprintf("%s LIMIT %d", query, limit)
195+
}
196+
197+
return query
198+
}

server/router.go

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package server
22

33
import (
4+
"database/sql"
45
"net/http"
56

67
"github.com/src-d/gitbase-playground/server/handler"
@@ -17,6 +18,7 @@ func Router(
1718
logger *logrus.Logger,
1819
static *handler.Static,
1920
version string,
21+
db *sql.DB,
2022
) http.Handler {
2123

2224
// cors options
@@ -33,6 +35,8 @@ func Router(
3335
r.Use(cors.New(corsOptions).Handler)
3436
r.Use(lg.RequestLogger(logger))
3537

38+
r.Post("/query", handler.APIHandlerFunc(handler.Query(db)))
39+
3640
r.Get("/version", handler.APIHandlerFunc(handler.Version(version)))
3741

3842
r.Get("/static/*", static.ServeHTTP)

server/serializer/serializers.go

+25-7
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ type HTTPError interface {
1515
type Response struct {
1616
Status int `json:"status"`
1717
Data interface{} `json:"data,omitempty"`
18+
Meta interface{} `json:"meta,omitempty"`
1819
Errors []HTTPError `json:"errors,omitempty"`
1920
}
2021

2122
type httpError struct {
22-
Status int `json:"status"`
23-
Title string `json:"title"`
24-
Details string `json:"details,omitempty"`
23+
Status int `json:"status"`
24+
Title string `json:"title"`
25+
Details string `json:"details,omitempty"`
26+
MySQLCode uint16 `json:"mysqlCode,omitempty"`
2527
}
2628

2729
// StatusCode returns the Status of the httpError
@@ -47,16 +49,22 @@ func NewHTTPError(statusCode int, msg ...string) HTTPError {
4749
return httpError{Status: statusCode, Title: strings.Join(msg, " ")}
4850
}
4951

50-
func newResponse(c interface{}) *Response {
51-
if c == nil {
52+
// NewHTTPError returns an Error with the MySQL error code
53+
func NewMySQLError(statusCode int, mysqlCode uint16, msg ...string) HTTPError {
54+
return httpError{Status: statusCode, MySQLCode: mysqlCode, Title: strings.Join(msg, " ")}
55+
}
56+
57+
func newResponse(data interface{}, meta interface{}) *Response {
58+
if data == nil {
5259
return &Response{
5360
Status: http.StatusNoContent,
5461
}
5562
}
5663

5764
return &Response{
5865
Status: http.StatusOK,
59-
Data: c,
66+
Data: data,
67+
Meta: meta,
6068
}
6169
}
6270

@@ -71,5 +79,15 @@ type versionResponse struct {
7179

7280
// NewVersionResponse returns a Response with current version of the server
7381
func NewVersionResponse(version string) *Response {
74-
return newResponse(versionResponse{version})
82+
return newResponse(versionResponse{version}, nil)
83+
}
84+
85+
type queryMetaResponse struct {
86+
Headers []string `json:"headers"`
87+
Types []string `json:"types"`
88+
}
89+
90+
// NewQueryResponse returns a Response with table headers and row contents
91+
func NewQueryResponse(rows []map[string]interface{}, columnNames, columnTypes []string) *Response {
92+
return newResponse(rows, queryMetaResponse{columnNames, columnTypes})
7593
}

0 commit comments

Comments
 (0)