Skip to content

Commit 9fe1767

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

File tree

4 files changed

+238
-9
lines changed

4 files changed

+238
-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

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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"`
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+
err = json.Unmarshal(body, &queryRequest)
55+
}
56+
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
query := addLimit(queryRequest.Query, queryRequest.Limit)
62+
rows, err := db.Query(query)
63+
if err != nil {
64+
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
65+
return nil, serializer.NewMySQLError(
66+
http.StatusBadRequest,
67+
mysqlErr.Number,
68+
mysqlErr.Message)
69+
}
70+
71+
return nil, serializer.NewHTTPError(http.StatusBadRequest, err.Error())
72+
}
73+
defer rows.Close()
74+
75+
columnNames, columnTypes, err := columnsInfo(rows)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
columnValsPtr := genericVals(columnTypes)
81+
82+
tableData := make([]map[string]interface{}, 0)
83+
84+
for rows.Next() {
85+
if err := rows.Scan(columnValsPtr...); err != nil {
86+
return nil, err
87+
}
88+
89+
colData := make(map[string]interface{}, len(columnTypes))
90+
91+
for i, val := range columnValsPtr {
92+
colData[columnNames[i]] = nil
93+
94+
switch val.(type) {
95+
case *sql.NullBool:
96+
sqlVal, _ := val.(*sql.NullBool)
97+
if sqlVal.Valid {
98+
colData[columnNames[i]] = sqlVal.Bool
99+
}
100+
case *mysql.NullTime:
101+
sqlVal, _ := val.(*mysql.NullTime)
102+
if sqlVal.Valid {
103+
colData[columnNames[i]] = sqlVal.Time
104+
}
105+
case *sql.NullInt64:
106+
sqlVal, _ := val.(*sql.NullInt64)
107+
if sqlVal.Valid {
108+
colData[columnNames[i]] = sqlVal.Int64
109+
}
110+
case *sql.NullString:
111+
sqlVal, _ := val.(*sql.NullString)
112+
if sqlVal.Valid {
113+
colData[columnNames[i]] = sqlVal.String
114+
}
115+
case *[]byte:
116+
// DatabaseTypeName JSON is used for arrays of uast nodes and
117+
// arrays of strings, but we don't know the exact type.
118+
// We try with arry of uast nodes first and any JSON later
119+
nodes, err := unmarshallUAST(val)
120+
if err == nil {
121+
colData[columnNames[i]] = nodes
122+
} else {
123+
var data interface{}
124+
125+
if err := json.Unmarshal(*val.(*[]byte), &data); err != nil {
126+
return nil, err
127+
}
128+
colData[columnNames[i]] = data
129+
}
130+
}
131+
}
132+
133+
tableData = append(tableData, colData)
134+
}
135+
136+
if err := rows.Err(); err != nil {
137+
return nil, err
138+
}
139+
140+
return serializer.NewQueryResponse(tableData, columnNames, columnTypes), nil
141+
}
142+
}
143+
144+
// columnsInfo returns the column names and column types, or error
145+
func columnsInfo(rows *sql.Rows) ([]string, []string, error) {
146+
names, err := rows.Columns()
147+
if err != nil {
148+
return nil, nil, err
149+
}
150+
151+
types, err := rows.ColumnTypes()
152+
if err != nil {
153+
return nil, nil, err
154+
}
155+
156+
typesStr := make([]string, len(types))
157+
for i, colType := range types {
158+
typesStr[i] = colType.DatabaseTypeName()
159+
}
160+
161+
return names, typesStr, nil
162+
}
163+
164+
// unmarshallUAST tries to cast data as [][]byte and unmarshall uast nodes
165+
func unmarshallUAST(data interface{}) ([]*uast.Node, error) {
166+
var protobufs [][]byte
167+
if err := json.Unmarshal(*data.(*[]byte), &protobufs); err != nil {
168+
return nil, err
169+
}
170+
171+
nodes := make([]*uast.Node, len(protobufs))
172+
173+
for i, v := range protobufs {
174+
node := uast.NewNode()
175+
if err := node.Unmarshal(v); err != nil {
176+
return nil, err
177+
}
178+
nodes[i] = node
179+
}
180+
181+
return nodes, nil
182+
}
183+
184+
// addLimit adds LIMIT to the query, performing basic tests to skip it
185+
// for DESCRIBE TABLE, SHOW TABLES, and avoid '; limit'
186+
func addLimit(query string, limit int) string {
187+
query = strings.TrimRight(strings.TrimSpace(query), ";")
188+
if strings.HasPrefix(strings.ToUpper(query), "SELECT") {
189+
return fmt.Sprintf("%s LIMIT %d", query, limit)
190+
}
191+
return query
192+
}

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)