├── .travis.yml ├── LICENSE ├── README.md ├── cmd └── prq │ └── prq.go ├── conn.go ├── conn_test.go └── types.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go_import_path: github.com/avct/prestgo 3 | go: 4 | - 1.6.x 5 | - 1.7.x 6 | - 1.8 7 | 8 | script: 9 | - go test github.com/avct/prestgo/... 10 | - go test -race github.com/avct/prestgo/... 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Avocet Systems Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This driver is now deprecated 2 | 3 | Please use [github.com/prestodb/presto-go-client](https://github.com/prestodb/presto-go-client) instead. 4 | 5 | # prestgo 6 | 7 | A pure [Go](http://golang.org/) database driver for the [Presto](http://prestodb.io/) query engine. 8 | 9 | ## Installation 10 | 11 | Simply run 12 | 13 | go get github.com/avct/prestgo/... 14 | 15 | This will install the Presto database driver and the `prq` tool for running queries from the command line. 16 | 17 | Documentation is at http://godoc.org/github.com/avct/prestgo 18 | 19 | ## Usage 20 | 21 | Prestgo conforms to the Go standard library [Driver interface](http://golang.org/pkg/database/sql/driver/#Driver). This means it works transparently with the [`database/sql`](http://golang.org/pkg/database/sql/) package. Simply import the `github.com/avct/prestgo` package to auto-register the driver: 22 | 23 | ``` 24 | import "github.com/avct/prestgo" 25 | ``` 26 | 27 | If you don't intend to use any prestgo-specific functions directly, you can import using the blank identifier which will still register the driver: 28 | 29 | ``` 30 | import _ "github.com/avct/prestgo" 31 | ``` 32 | 33 | The driver name is `prestgo` and it supports the standard Presto data source name format `presto://user@hostname:port/catalog/schema`. All parts of the data source name are optional, defaulting to port 8080 on localhost with `hive` catalog, `default` schema and a user of `prestgo`. 34 | 35 | Here's how to get a list of tables from a Presto server: 36 | 37 | ```Go 38 | package main 39 | 40 | import ( 41 | "database/sql" 42 | "fmt" 43 | "log" 44 | 45 | _ "github.com/avct/prestgo" 46 | ) 47 | 48 | func main() { 49 | db, err := sql.Open("prestgo", "presto://example:8080/hive/default") 50 | if err != nil { 51 | log.Fatalf("failed to connect to presto: %v", err) 52 | } 53 | 54 | rows, err := db.Query("SHOW TABLES") 55 | if err != nil { 56 | log.Fatalf("failed to run query: %v", err) 57 | } 58 | 59 | defer rows.Close() 60 | 61 | for rows.Next() { 62 | var name string 63 | if err := rows.Scan(&name); err != nil { 64 | log.Fatal(err.Error()) 65 | } 66 | 67 | fmt.Printf("%s\n", name) 68 | } 69 | if err := rows.Err(); err != nil { 70 | log.Fatal(err.Error()) 71 | } 72 | } 73 | ``` 74 | 75 | The included command line query tool `prq` can be used like this: 76 | 77 | ``` 78 | prq "presto://example:8080/hive/default" "SHOW TABLES" 79 | ``` 80 | 81 | ## Features 82 | 83 | * SELECT, SHOW, DESCRIBE 84 | * Pagination of results 85 | * `varchar`, `bigint`, `boolean`, `double` and `timestamp` datatypes 86 | * Custom HTTP clients 87 | 88 | ## Future 89 | 90 | (aka: Things you could help with) 91 | 92 | * Parameterised queries 93 | * INSERT queries 94 | * DDL (ALTER/CREATE/DROP TABLE) 95 | * Cancelling of queries 96 | * User authentication 97 | * `json`, `date`, `time`, `interval`, `array`, `row` and `map` datatypes 98 | 99 | 100 | ## Authors 101 | 102 | Originally written by [Ian Davis](http://iandavis.com). 103 | 104 | ## Contributors 105 | 106 | Your name here... 107 | 108 | ## Contributing 109 | 110 | * Do submit your changes as a pull request 111 | * Do your best to adhere to the existing coding conventions and idioms. 112 | * Do supply unit tests if possible. 113 | * Do run `go fmt` on the code before committing 114 | * Do feel free to add yourself to the Contributors list in 115 | the [`README.md`](README.md). Alphabetical order applies. 116 | * Don't touch the Authors section. An existing author will add you if 117 | your contributions are significant enough. 118 | 119 | ## License 120 | 121 | This software is released under the MIT License, please see the accompanying [`LICENSE`](LICENSE) file. 122 | 123 | -------------------------------------------------------------------------------- /cmd/prq/prq.go: -------------------------------------------------------------------------------- 1 | // Command prq is a command line interface for running presto queries 2 | package main 3 | 4 | import ( 5 | "database/sql" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | "text/tabwriter" 11 | 12 | _ "github.com/avct/prestgo" 13 | ) 14 | 15 | var outformat = flag.String("o", "tabular", "set output format: tabular (default) or tsv") 16 | 17 | func main() { 18 | flag.Parse() 19 | if len(flag.Args()) < 1 { 20 | fatal("missing required data source argument") 21 | } 22 | 23 | if len(flag.Args()) < 2 { 24 | fatal("missing required query argument") 25 | } 26 | 27 | db, err := sql.Open("prestgo", flag.Args()[0]) 28 | if err != nil { 29 | fatal(fmt.Sprintf("failed to connect to presto: %v", err)) 30 | } 31 | rows, err := db.Query(flag.Args()[1]) 32 | if err != nil { 33 | fatal(fmt.Sprintf("failed query presto: %v", err)) 34 | } 35 | defer rows.Close() 36 | 37 | cols, err := rows.Columns() 38 | if err != nil { 39 | fatal(fmt.Sprintf("failed to read columns: %v", err)) 40 | } 41 | var w io.Writer 42 | switch *outformat { 43 | case "tsv": 44 | w = os.Stdout 45 | default: 46 | tw := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0) 47 | defer tw.Flush() 48 | w = tw 49 | } 50 | for i := range cols { 51 | if i > 0 { 52 | fmt.Fprint(w, "\t") 53 | } 54 | fmt.Fprintf(w, "%v", cols[i]) 55 | } 56 | fmt.Fprint(w, "\n") 57 | 58 | data := make([]interface{}, len(cols)) 59 | args := make([]interface{}, len(data)) 60 | for i := range data { 61 | args[i] = &data[i] 62 | } 63 | for rows.Next() { 64 | if err := rows.Scan(args...); err != nil { 65 | fatal(err.Error()) 66 | } 67 | for i := range data { 68 | if i > 0 { 69 | fmt.Fprint(w, "\t") 70 | } 71 | fmt.Fprintf(w, "%v", data[i]) 72 | } 73 | fmt.Fprint(w, "\n") 74 | } 75 | if err := rows.Err(); err != nil { 76 | fatal(err.Error()) 77 | } 78 | } 79 | 80 | func fatal(msg string) { 81 | fmt.Fprintln(os.Stderr, msg) 82 | os.Exit(1) 83 | } 84 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | package prestgo 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "database/sql/driver" 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "math" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // Name of the driver to use when calling `sql.Open` 20 | const DriverName = "prestgo" 21 | 22 | // Default data source parameters 23 | const ( 24 | DefaultPort = "8080" 25 | DefaultCatalog = "hive" 26 | DefaultSchema = "default" 27 | DefaultUsername = "prestgo" 28 | 29 | TimestampFormat = "2006-01-02 15:04:05.000" 30 | ) 31 | 32 | var ( 33 | // ErrNotSupported is returned when an unsupported feature is requested. 34 | ErrNotSupported = errors.New(DriverName + ": not supported") 35 | 36 | // ErrQueryFailed indicates that a network or server failure prevented the driver obtaining a query result. 37 | ErrQueryFailed = errors.New(DriverName + ": query failed") 38 | 39 | // ErrQueryCanceled indicates that a query was canceled before results could be retrieved. 40 | ErrQueryCanceled = errors.New(DriverName + ": query canceled") 41 | ) 42 | 43 | func init() { 44 | sql.Register(DriverName, &drv{}) 45 | } 46 | 47 | type drv struct{} 48 | 49 | func (*drv) Open(name string) (driver.Conn, error) { 50 | return Open(name) 51 | } 52 | 53 | // Open creates a connection to the specified data source name which should be 54 | // of the form "presto://hostname:port/catalog/schema?source=x&session=y". http.DefaultClient will 55 | // be used for communicating with the Presto server. 56 | func Open(name string) (driver.Conn, error) { 57 | return ClientOpen(http.DefaultClient, name) 58 | } 59 | 60 | // ClientOpen creates a connection to the specified data source name using the supplied 61 | // HTTP client. The data source name should be of the form 62 | // "presto://hostname:port/catalog/schema?source=x&session=y". 63 | func ClientOpen(client *http.Client, name string) (driver.Conn, error) { 64 | 65 | conf := make(config) 66 | conf.parseDataSource(name) 67 | 68 | cn := &conn{ 69 | client: client, 70 | addr: conf["addr"], 71 | catalog: conf["catalog"], 72 | schema: conf["schema"], 73 | user: conf["user"], 74 | source: conf["source"], 75 | session: conf["session"], 76 | } 77 | return cn, nil 78 | } 79 | 80 | type conn struct { 81 | client *http.Client 82 | addr string 83 | catalog string 84 | schema string 85 | user string 86 | source string 87 | session string 88 | } 89 | 90 | var _ driver.Conn = &conn{} 91 | 92 | func (c *conn) Prepare(query string) (driver.Stmt, error) { 93 | st := &stmt{ 94 | conn: c, 95 | query: query, 96 | } 97 | return st, nil 98 | } 99 | 100 | func (c *conn) Close() error { 101 | return nil 102 | } 103 | 104 | func (c *conn) Begin() (driver.Tx, error) { 105 | return nil, ErrNotSupported 106 | } 107 | 108 | type stmt struct { 109 | conn *conn 110 | query string 111 | } 112 | 113 | var _ driver.Stmt = &stmt{} 114 | 115 | func (s *stmt) Close() error { 116 | return nil 117 | } 118 | 119 | func (s *stmt) NumInput() int { 120 | return -1 // TODO: parse query for parameters 121 | } 122 | 123 | func (s *stmt) Exec(args []driver.Value) (driver.Result, error) { 124 | return nil, ErrNotSupported 125 | } 126 | 127 | func (s *stmt) Query(args []driver.Value) (driver.Rows, error) { 128 | // TODO: support query argument substitution 129 | if len(args) > 0 { 130 | return nil, ErrNotSupported 131 | } 132 | queryURL := fmt.Sprintf("http://%s/v1/statement", s.conn.addr) 133 | 134 | req, err := http.NewRequest("POST", queryURL, strings.NewReader(s.query)) 135 | if err != nil { 136 | return nil, err 137 | } 138 | req.Header.Add("X-Presto-User", s.conn.user) 139 | req.Header.Add("X-Presto-Catalog", s.conn.catalog) 140 | req.Header.Add("X-Presto-Schema", s.conn.schema) 141 | if s.conn.source != "" { 142 | req.Header.Add("X-Presto-Source", s.conn.source) 143 | } 144 | if s.conn.session != "" { 145 | req.Header.Add("X-Presto-Session", s.conn.session) 146 | } 147 | 148 | resp, err := s.conn.client.Do(req) 149 | if err != nil { 150 | return nil, err 151 | } 152 | defer resp.Body.Close() 153 | 154 | // Presto doesn't use the http response code, parse errors come back as 200 155 | if resp.StatusCode != 200 { 156 | return nil, ErrQueryFailed 157 | } 158 | 159 | var sresp stmtResponse 160 | err = json.NewDecoder(resp.Body).Decode(&sresp) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | if sresp.Stats.State == "FAILED" { 166 | return nil, sresp.Error 167 | } 168 | 169 | r := &rows{ 170 | conn: s.conn, 171 | nextURI: sresp.NextURI, 172 | } 173 | 174 | return r, nil 175 | } 176 | 177 | type rows struct { 178 | conn *conn 179 | nextURI string 180 | fetched bool 181 | rowindex int 182 | columns []string 183 | types []driver.ValueConverter 184 | data []queryData 185 | } 186 | 187 | var _ driver.Rows = &rows{} 188 | 189 | func (r *rows) fetch() error { 190 | // TODO: timeout 191 | for { 192 | qresp, gotData, err := r.waitForData() 193 | if err != nil { 194 | return err 195 | } 196 | if !gotData { 197 | time.Sleep(800 * time.Millisecond) // TODO: make this interval configurable 198 | continue 199 | } 200 | 201 | r.rowindex = 0 202 | r.data = qresp.Data 203 | 204 | // Note: qresp.Stats.State will be FINISHED when last page is retrieved 205 | r.nextURI = qresp.NextURI 206 | 207 | if !r.fetched { 208 | r.columns = make([]string, len(qresp.Columns)) 209 | r.types = make([]driver.ValueConverter, len(qresp.Columns)) 210 | for i, col := range qresp.Columns { 211 | r.columns[i] = col.Name 212 | switch { 213 | case strings.HasPrefix(col.Type, VarChar): 214 | r.types[i] = driver.String 215 | case col.Type == BigInt, col.Type == Integer: 216 | r.types[i] = bigIntConverter 217 | case col.Type == Boolean: 218 | r.types[i] = driver.Bool 219 | case col.Type == Double: 220 | r.types[i] = doubleConverter 221 | case col.Type == Timestamp: 222 | r.types[i] = timestampConverter 223 | case col.Type == TimestampWithTimezone: 224 | r.types[i] = timestampWithTimezoneConverter 225 | case col.Type == MapVarchar: 226 | r.types[i] = mapVarcharConverter 227 | case col.Type == VarBinary: 228 | r.types[i] = varbinaryConverter 229 | case col.Type == ArrayVarchar: 230 | r.types[i] = arrayVarcharConverter 231 | 232 | default: 233 | return fmt.Errorf("unsupported column type: %s", col.Type) 234 | } 235 | } 236 | r.fetched = true 237 | } 238 | 239 | if len(qresp.Data) == 0 { 240 | return io.EOF 241 | } 242 | 243 | return nil 244 | } 245 | } 246 | 247 | func (r *rows) waitForData() (*queryResponse, bool, error) { 248 | nextReq, err := http.NewRequest("GET", r.nextURI, nil) 249 | if err != nil { 250 | return nil, false, err 251 | } 252 | 253 | nextResp, err := r.conn.client.Do(nextReq) 254 | if err != nil { 255 | return nil, false, err 256 | } 257 | 258 | if nextResp.StatusCode != 200 { 259 | nextResp.Body.Close() 260 | return nil, false, ErrQueryFailed 261 | } 262 | 263 | var qresp queryResponse 264 | err = json.NewDecoder(nextResp.Body).Decode(&qresp) 265 | nextResp.Body.Close() 266 | if err != nil { 267 | return nil, false, err 268 | } 269 | 270 | switch qresp.Stats.State { 271 | case QueryStateFailed: 272 | return nil, false, qresp.Error 273 | case QueryStateCanceled: 274 | return nil, false, ErrQueryCanceled 275 | case QueryStatePlanning, QueryStateQueued, QueryStateRunning, QueryStateStarting: 276 | if len(qresp.Data) == 0 { 277 | r.nextURI = qresp.NextURI 278 | return nil, false, nil 279 | } 280 | } 281 | 282 | return &qresp, true, nil 283 | } 284 | 285 | func (r *rows) Columns() []string { 286 | if !r.fetched { 287 | if err := r.fetch(); err != nil { 288 | return []string{} 289 | } 290 | } 291 | return r.columns 292 | } 293 | 294 | func (r *rows) Close() error { 295 | return nil 296 | } 297 | 298 | func (r *rows) Next(dest []driver.Value) error { 299 | if !r.fetched || r.rowindex >= len(r.data) { 300 | if r.nextURI == "" { 301 | return io.EOF 302 | } 303 | if err := r.fetch(); err != nil { 304 | return err 305 | } 306 | } 307 | 308 | for i, v := range r.types { 309 | val, err := v.ConvertValue(r.data[r.rowindex][i]) 310 | if err != nil { 311 | return err // TODO: more context in error 312 | } 313 | dest[i] = val 314 | } 315 | r.rowindex++ 316 | return nil 317 | } 318 | 319 | type config map[string]string 320 | 321 | func (c config) parseDataSource(ds string) error { 322 | u, err := url.Parse(ds) 323 | if err != nil { 324 | return err 325 | } 326 | 327 | if u.User != nil { 328 | c["user"] = u.User.Username() 329 | } else { 330 | c["user"] = DefaultUsername 331 | } 332 | 333 | if strings.IndexRune(u.Host, ':') == -1 { 334 | c["addr"] = u.Host + ":" + DefaultPort 335 | } else { 336 | c["addr"] = u.Host 337 | } 338 | 339 | c["catalog"] = DefaultCatalog 340 | c["schema"] = DefaultSchema 341 | 342 | 343 | pathSegments := strings.FieldsFunc(u.Path, func(c rune) bool { return c == '/' }) 344 | if len(pathSegments) > 0 { 345 | c["catalog"] = pathSegments[0] 346 | } 347 | if len(pathSegments) > 1 { 348 | c["schema"] = pathSegments[1] 349 | } 350 | 351 | m, _ := url.ParseQuery(u.RawQuery) 352 | for k, v := range m { 353 | c[k] = strings.Join(v, ",") 354 | } 355 | return nil 356 | } 357 | 358 | type valueConverterFunc func(v interface{}) (driver.Value, error) 359 | 360 | func (fn valueConverterFunc) ConvertValue(v interface{}) (driver.Value, error) { 361 | return fn(v) 362 | } 363 | 364 | // bigIntConverter converts a value from the underlying json response into an int64. 365 | // The Go JSON decoder uses float64 for generic numeric values 366 | var bigIntConverter = valueConverterFunc(func(val interface{}) (driver.Value, error) { 367 | if val == nil { 368 | return nil, nil 369 | } 370 | 371 | if vv, ok := val.(float64); ok { 372 | return int64(vv), nil 373 | } 374 | return nil, fmt.Errorf("%s: failed to convert %v (%T) into type int64", DriverName, val, val) 375 | }) 376 | 377 | // doubleConverter converts a value from the underlying json response into an int64. 378 | // The Go JSON decoder uses float64 for generic numeric values 379 | var doubleConverter = valueConverterFunc(func(val interface{}) (driver.Value, error) { 380 | if val == nil { 381 | return nil, nil 382 | } 383 | 384 | switch vv := val.(type) { 385 | case float64: 386 | return vv, nil 387 | case string: 388 | switch vv { 389 | case "Infinity": 390 | return math.Inf(1), nil 391 | case "NaN": 392 | return math.NaN(), nil 393 | } 394 | 395 | } 396 | return nil, fmt.Errorf("%s: failed to convert %v (%T) into type float64", DriverName, val, val) 397 | }) 398 | 399 | // timestampConverter converts a value from the underlying json response into a time.Time. 400 | var timestampConverter = valueConverterFunc(func(val interface{}) (driver.Value, error) { 401 | if val == nil { 402 | return nil, nil 403 | } 404 | if vv, ok := val.(string); ok { 405 | // BUG: should parse using session time zone. 406 | if ts, err := time.ParseInLocation(TimestampFormat, vv, time.Local); err == nil { 407 | return ts, nil 408 | } 409 | } 410 | return nil, fmt.Errorf("%s: failed to convert %v (%T) into type time.Time", DriverName, val, val) 411 | }) 412 | 413 | // timestampWithTimezoneConverter converts a value from the underlying json response into a time.Time including timezone. 414 | var timestampWithTimezoneConverter = valueConverterFunc(func(val interface{}) (driver.Value, error) { 415 | if val == nil { 416 | return nil, nil 417 | } 418 | if vv, ok := val.(string); ok { 419 | if len(vv) <= len(TimestampFormat) { 420 | return timestampConverter(val) 421 | } 422 | tzOffset := strings.LastIndex(vv, " ") 423 | if tzOffset == -1 { 424 | return timestampConverter(val) 425 | } 426 | tz, err := time.LoadLocation(strings.TrimSpace(vv[tzOffset:])) 427 | if err != nil { 428 | return nil, err 429 | } 430 | ts, err := time.ParseInLocation(TimestampFormat, vv[:tzOffset], tz) 431 | if err != nil { 432 | return nil, err 433 | } 434 | return ts, nil 435 | } 436 | return nil, fmt.Errorf("%s: failed to convert %v (%T) into type time.Time", DriverName, val, val) 437 | }) 438 | 439 | // varbinaryConverter converts varbinary to a byte slice 440 | var varbinaryConverter = valueConverterFunc(func(val interface{}) (driver.Value, error) { 441 | if val == nil { 442 | return nil, nil 443 | } 444 | 445 | // varbinary values are returned as base64 encoded strings 446 | if vv, ok := val.(string); ok { 447 | // decode the base64 string into a byte slice 448 | dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(vv)) 449 | 450 | var buf bytes.Buffer 451 | if _, err := io.Copy(&buf, dec); err != nil { 452 | return nil, fmt.Errorf("failed to decode base64 string: %s: %s", vv, err) 453 | } 454 | 455 | return buf.Bytes(), nil 456 | } 457 | 458 | return nil, fmt.Errorf("%s: failed to convert %v (%T) into type []byte", DriverName, val, val) 459 | }) 460 | 461 | // mapVarcharConverter converts a value from map[string]interface{} into a map[string]string. 462 | var mapVarcharConverter = valueConverterFunc(func(val interface{}) (driver.Value, error) { 463 | if val == nil { 464 | return nil, nil 465 | } 466 | 467 | if vv, ok := val.(map[string]interface{}); ok { 468 | // All map values should be strings 469 | outMap := map[string]string{} 470 | 471 | for k, v := range vv { 472 | vstr, ok := v.(string) 473 | if !ok { 474 | return nil, fmt.Errorf("unexpected non-string value in map: %v", v) 475 | } 476 | outMap[k] = vstr 477 | } 478 | 479 | return outMap, nil 480 | } 481 | 482 | return nil, fmt.Errorf("%s: failed to convert %v (%T) into type map[string]string", DriverName, val, val) 483 | }) 484 | 485 | // arrayVarcharConverter converts a value from the underlying json response into an []string 486 | var arrayVarcharConverter = valueConverterFunc(func(val interface{}) (driver.Value, error) { 487 | if val == nil { 488 | return nil, nil 489 | } 490 | 491 | if vv, ok := val.([]interface{}); ok { 492 | var outSlice []string 493 | 494 | for _, v := range vv { 495 | vstr, ok := v.(string) 496 | if !ok { 497 | return nil, fmt.Errorf("unexpected non-string value in array: %v", v) 498 | } 499 | 500 | outSlice = append(outSlice, vstr) 501 | } 502 | 503 | return outSlice, nil 504 | } 505 | 506 | return nil, fmt.Errorf("%s: failed to convert %v (%T) into type []string", DriverName, val, val) 507 | }) 508 | -------------------------------------------------------------------------------- /conn_test.go: -------------------------------------------------------------------------------- 1 | package prestgo 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "io" 7 | "math" 8 | "net/http" 9 | "net/http/httptest" 10 | "reflect" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestConfigParseDataSource(t *testing.T) { 16 | testCases := []struct { 17 | ds string 18 | expected config 19 | error bool 20 | }{ 21 | 22 | { 23 | ds: "", 24 | expected: config{"addr": ":8080", "catalog": "hive", "schema": "default", "user": "prestgo"}, 25 | error: false, 26 | }, 27 | 28 | { 29 | ds: "presto://example:9000/", 30 | expected: config{"addr": "example:9000", "catalog": "hive", "schema": "default", "user": "prestgo"}, 31 | error: false, 32 | }, 33 | 34 | { 35 | ds: "presto://example/", 36 | expected: config{"addr": "example:8080", "catalog": "hive", "schema": "default", "user": "prestgo"}, 37 | error: false, 38 | }, 39 | 40 | { 41 | ds: "presto://example/tree", 42 | expected: config{"addr": "example:8080", "catalog": "tree", "schema": "default", "user": "prestgo"}, 43 | error: false, 44 | }, 45 | 46 | { 47 | ds: "presto://example/tree/", 48 | expected: config{"addr": "example:8080", "catalog": "tree", "schema": "default", "user": "prestgo"}, 49 | error: false, 50 | }, 51 | 52 | { 53 | ds: "presto://example/tree/birch", 54 | expected: config{"addr": "example:8080", "catalog": "tree", "schema": "birch", "user": "prestgo"}, 55 | error: false, 56 | }, 57 | 58 | { 59 | ds: "presto://name@example/", 60 | expected: config{"addr": "example:8080", "catalog": "hive", "schema": "default", "user": "name"}, 61 | error: false, 62 | }, 63 | 64 | { 65 | ds: "presto://name:pwd@example/", 66 | expected: config{"addr": "example:8080", "catalog": "hive", "schema": "default", "user": "name"}, 67 | error: false, 68 | }, 69 | 70 | { 71 | ds: "presto://name@example:9000/", 72 | expected: config{"addr": "example:9000", "catalog": "hive", "schema": "default", "user": "name"}, 73 | error: false, 74 | }, 75 | 76 | { 77 | ds: "presto://name:pwd@example:9000/", 78 | expected: config{"addr": "example:9000", "catalog": "hive", "schema": "default", "user": "name"}, 79 | error: false, 80 | }, 81 | 82 | { 83 | ds: "presto://name@example/tree/birch", 84 | expected: config{"addr": "example:8080", "catalog": "tree", "schema": "birch", "user": "name"}, 85 | error: false, 86 | }, 87 | { 88 | ds: "presto://name@example:9000/tree/birch?source=leaf", 89 | expected: config{"addr": "example:9000", "catalog": "tree", "schema": "birch", "user": "name", "source": "leaf"}, 90 | error: false, 91 | }, 92 | { 93 | ds: "presto://name@example:9000/tree/birch?source=leaf&session=flower", 94 | expected: config{"addr": "example:9000", "catalog": "tree", "schema": "birch", "user": "name", "source": "leaf", "session":"flower"}, 95 | error: false, 96 | }, 97 | } 98 | 99 | for _, tc := range testCases { 100 | conf := make(config) 101 | err := conf.parseDataSource(tc.ds) 102 | 103 | gotError := err != nil 104 | if gotError != tc.error { 105 | t.Errorf("got error=%v, wanted error=%v", gotError, tc.error) 106 | continue 107 | } 108 | 109 | if !reflect.DeepEqual(conf, tc.expected) { 110 | t.Errorf("%s: got %#v, wanted %#v", tc.ds, conf, tc.expected) 111 | } 112 | 113 | } 114 | } 115 | 116 | var oneRowColResponse = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 117 | switch r.URL.Path { 118 | case "/v1/query/abcd/1": 119 | fmt.Fprintln(w, fmt.Sprintf(`{ 120 | "id": "abcd", 121 | "infoUri": "http://%[1]s/v1/query/abcd", 122 | "partialCancelUri": "http://%[1]s/v1/query/abcd.0", 123 | "columns": [ 124 | { 125 | "name": "col0", "type": "varchar", "typeSignature": { "rawType": "varchar", "typeArguments": [], "literalArguments": [] } 126 | } 127 | ], 128 | "data": [ 129 | [ "c0r0" ] 130 | ] 131 | }`, r.Host)) 132 | default: 133 | http.NotFound(w, r) 134 | } 135 | }) 136 | 137 | var failingQueryResult = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 138 | switch r.URL.Path { 139 | case "/v1/query/abcd/1": 140 | fmt.Fprintln(w, fmt.Sprintf(`{ 141 | "id": "abcd", 142 | "infoUri": "http://%[1]s/v1/query/abcd", 143 | "partialCancelUri": "http://%[1]s/v1/query/abcd.0", 144 | "stats":{"state":"FAILED"} 145 | }`, r.Host)) 146 | default: 147 | http.NotFound(w, r) 148 | } 149 | }) 150 | 151 | var supportedDatatypesResponse = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 152 | switch r.URL.Path { 153 | case "/v1/query/abcd/1": 154 | fmt.Fprintln(w, fmt.Sprintf(`{ 155 | "id": "abcd", 156 | "infoUri": "http://%[1]s/v1/query/abcd", 157 | "partialCancelUri": "http://%[1]s/v1/query/abcd.0", 158 | "columns": [ 159 | { "name": "col0", "type": "varchar", "typeSignature": { "rawType": "varchar", "typeArguments": [], "literalArguments": [] } }, 160 | { "name": "col1", "type": "bigint", "typeSignature": { "rawType": "varchar", "typeArguments": [], "literalArguments": [] } }, 161 | { "name": "col2", "type": "double", "typeSignature": { "rawType": "varchar", "typeArguments": [], "literalArguments": [] } }, 162 | { "name": "col3", "type": "boolean", "typeSignature": { "rawType": "varchar", "typeArguments": [], "literalArguments": [] } }, 163 | { "name": "col4", "type": "timestamp", "typeSignature": { "rawType": "varchar", "typeArguments": [], "literalArguments": [] } }, 164 | { "name": "col5", "type": "integer", "typeSignature": { "rawType": "integer", "typeArguments": [], "literalArguments": [] } } 165 | ], 166 | "data": [ 167 | [ "c0r0", 12345, 12.45, true, "2015-02-09 18:26:02.013", 12 ] 168 | ] 169 | }`, r.Host)) 170 | default: 171 | http.NotFound(w, r) 172 | } 173 | }) 174 | 175 | var multiRowResponse = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 176 | switch r.URL.Path { 177 | case "/v1/query/abcd/1": 178 | fmt.Fprintln(w, fmt.Sprintf(`{ 179 | "id": "abcd", 180 | "infoUri": "http://%[1]s/v1/query/abcd", 181 | "partialCancelUri": "http://%[1]s/v1/query/abcd.0", 182 | "columns": [ 183 | { 184 | "name": "col0", "type": "varchar", "typeSignature": { "rawType": "varchar", "typeArguments": [], "literalArguments": [] } 185 | } 186 | ], 187 | "data": [ 188 | [ "c0r0" ], 189 | [ "c0r1" ], 190 | [ "c0r2" ], 191 | [ "c0r3" ], 192 | [ "c0r4" ], 193 | [ "c0r5" ] 194 | ] 195 | }`, r.Host)) 196 | default: 197 | http.NotFound(w, r) 198 | } 199 | }) 200 | 201 | var multiPageResponse = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 202 | switch r.URL.Path { 203 | case "/v1/query/abcd/1": 204 | fmt.Fprintln(w, fmt.Sprintf(`{ 205 | "id": "abcd", 206 | "infoUri": "http://%[1]s/v1/query/abcd", 207 | "nextUri": "http://%[1]s/v1/query/abcd/2", 208 | "partialCancelUri": "http://%[1]s/v1/query/abcd.0", 209 | "columns": [ 210 | { 211 | "name": "col0", "type": "varchar", "typeSignature": { "rawType": "varchar", "typeArguments": [], "literalArguments": [] } 212 | } 213 | ], 214 | "data": [ 215 | [ "c0r0" ], 216 | [ "c0r1" ], 217 | [ "c0r2" ] 218 | ] 219 | }`, r.Host)) 220 | case "/v1/query/abcd/2": 221 | fmt.Fprintln(w, fmt.Sprintf(`{ 222 | "id": "abcd", 223 | "infoUri": "http://%[1]s/v1/query/abcd", 224 | "partialCancelUri": "http://%[1]s/v1/query/abcd.0", 225 | "columns": [ 226 | { 227 | "name": "col0", "type": "varchar", "typeSignature": { "rawType": "varchar", "typeArguments": [], "literalArguments": [] } 228 | } 229 | ], 230 | "data": [ 231 | [ "c0r3" ], 232 | [ "c0r4" ], 233 | [ "c0r5" ] 234 | ] 235 | }`, r.Host)) 236 | default: 237 | http.NotFound(w, r) 238 | } 239 | }) 240 | 241 | func TestRowsFetchBasic(t *testing.T) { 242 | ts := httptest.NewServer(oneRowColResponse) 243 | defer ts.Close() 244 | 245 | r := &rows{ 246 | conn: &conn{ 247 | client: http.DefaultClient, 248 | }, 249 | nextURI: ts.URL + "/v1/query/abcd/1", 250 | } 251 | 252 | err := r.fetch() 253 | if err != nil { 254 | t.Fatal(err.Error()) 255 | } 256 | 257 | cols := r.Columns() 258 | if len(cols) != 1 { 259 | t.Fatalf("got %d cols, wanted %d", len(cols), 1) 260 | } 261 | if cols[0] != "col0" { 262 | t.Errorf("got %v, wanted %v", cols[0], "col0") 263 | } 264 | 265 | values := make([]driver.Value, len(cols)) 266 | 267 | if err := r.Next(values); err != nil { 268 | t.Fatal(err.Error()) 269 | } 270 | 271 | if err := r.Next(values); err != io.EOF { 272 | t.Fatalf("got %v, wanted io.EOF", err) 273 | } 274 | } 275 | 276 | func TestRowsFetchResultFail(t *testing.T) { 277 | ts := httptest.NewServer(failingQueryResult) 278 | defer ts.Close() 279 | 280 | r := &rows{ 281 | conn: &conn{ 282 | client: http.DefaultClient, 283 | }, 284 | nextURI: ts.URL + "/v1/query/abcd/1", 285 | } 286 | 287 | err := r.fetch() 288 | if err == nil { 289 | t.Fatal("got no error, wanted one") 290 | } 291 | } 292 | 293 | func TestRowsFetchSupportedTypes(t *testing.T) { 294 | ts := httptest.NewServer(supportedDatatypesResponse) 295 | defer ts.Close() 296 | 297 | r := &rows{ 298 | conn: &conn{ 299 | client: http.DefaultClient, 300 | }, 301 | nextURI: ts.URL + "/v1/query/abcd/1", 302 | } 303 | 304 | err := r.fetch() 305 | if err != nil { 306 | t.Fatal(err.Error()) 307 | } 308 | 309 | cols := r.Columns() 310 | values := make([]driver.Value, len(cols)) 311 | 312 | if err := r.Next(values); err != nil { 313 | t.Fatal(err.Error()) 314 | } 315 | 316 | expected := []interface{}{"c0r0", int64(12345), float64(12.45), true, time.Date(2015, 2, 9, 18, 26, 02, 13000000, time.Local), int64(12)} 317 | 318 | if len(values) != len(expected) { 319 | t.Fatalf("got %d values, wanted %d", len(values), len(expected)) 320 | } 321 | for i := range expected { 322 | if values[i] != expected[i] { 323 | t.Errorf("col%d: got %#v, wanted %#v", i, values[i], expected[i]) 324 | } 325 | } 326 | } 327 | 328 | func TestRowsColumnsPerformsFetch(t *testing.T) { 329 | ts := httptest.NewServer(oneRowColResponse) 330 | defer ts.Close() 331 | 332 | r := &rows{ 333 | conn: &conn{ 334 | client: http.DefaultClient, 335 | }, 336 | nextURI: ts.URL + "/v1/query/abcd/1", 337 | } 338 | 339 | cols := r.Columns() 340 | if len(cols) != 1 { 341 | t.Fatalf("got %d cols, wanted %d", len(cols), 1) 342 | } 343 | if cols[0] != "col0" { 344 | t.Errorf("got %v, wanted %v", cols[0], "col0") 345 | } 346 | } 347 | 348 | func TestRowsNextPerformsFetch(t *testing.T) { 349 | ts := httptest.NewServer(oneRowColResponse) 350 | defer ts.Close() 351 | 352 | r := &rows{ 353 | conn: &conn{ 354 | client: http.DefaultClient, 355 | }, 356 | nextURI: ts.URL + "/v1/query/abcd/1", 357 | } 358 | 359 | values := make([]driver.Value, 1) 360 | 361 | if err := r.Next(values); err != nil { 362 | t.Fatal(err.Error()) 363 | } 364 | if values[0] != "c0r0" { 365 | t.Errorf("got %v, wanted %v", values[0], "c0r0") 366 | } 367 | } 368 | 369 | func TestRowsNextMultipleRows(t *testing.T) { 370 | ts := httptest.NewServer(multiRowResponse) 371 | defer ts.Close() 372 | 373 | r := &rows{ 374 | conn: &conn{ 375 | client: http.DefaultClient, 376 | }, 377 | nextURI: ts.URL + "/v1/query/abcd/1", 378 | } 379 | 380 | values := make([]driver.Value, 1) 381 | 382 | // Fetch 6 rows 383 | for i := 0; i < 6; i++ { 384 | if err := r.Next(values); err != nil { 385 | t.Fatalf("row %d: %v", i, err.Error()) 386 | } 387 | } 388 | 389 | if err := r.Next(values); err != io.EOF { 390 | t.Fatalf("got %v, wanted io.EOF", err) 391 | } 392 | } 393 | 394 | func TestRowsNextMultiplePages(t *testing.T) { 395 | ts := httptest.NewServer(multiPageResponse) 396 | defer ts.Close() 397 | 398 | r := &rows{ 399 | conn: &conn{ 400 | client: http.DefaultClient, 401 | }, 402 | nextURI: ts.URL + "/v1/query/abcd/1", 403 | } 404 | 405 | values := make([]driver.Value, 1) 406 | 407 | // Fetch 6 rows 408 | for i := 0; i < 6; i++ { 409 | if err := r.Next(values); err != nil { 410 | t.Fatalf("row %d: %v", i, err.Error()) 411 | } 412 | } 413 | 414 | if err := r.Next(values); err != io.EOF { 415 | t.Fatalf("got %v, wanted io.EOF", err) 416 | } 417 | } 418 | 419 | func TestDoubleConverter(t *testing.T) { 420 | testCases := []struct { 421 | val interface{} 422 | expected driver.Value 423 | err bool 424 | }{ 425 | 426 | { 427 | val: 0.91, 428 | expected: driver.Value(0.91), 429 | err: false, 430 | }, 431 | 432 | { 433 | val: "foo", 434 | expected: nil, 435 | err: true, 436 | }, 437 | 438 | { 439 | val: "Infinity", 440 | expected: math.Inf(1), 441 | err: false, 442 | }, 443 | 444 | { 445 | val: "NaN", 446 | expected: math.NaN(), 447 | err: false, 448 | }, 449 | 450 | { 451 | val: nil, 452 | expected: nil, 453 | err: false, 454 | }, 455 | } 456 | 457 | for _, tc := range testCases { 458 | v, err := doubleConverter(tc.val) 459 | 460 | if tc.err == (err == nil) { 461 | t.Errorf("%v: got error %v, wanted %v", tc.val, err, tc.err) 462 | } 463 | 464 | if ef, ok := tc.expected.(float64); ok && math.IsNaN(ef) { 465 | vf, ok := v.(float64) 466 | if !ok { 467 | t.Errorf("%v: got type %T, wanted a float64", tc.val, v) 468 | continue 469 | } 470 | 471 | if !math.IsNaN(vf) { 472 | t.Errorf("%v: wanted NaN", tc.val) 473 | } 474 | continue 475 | } 476 | 477 | if v != tc.expected { 478 | t.Errorf("%v: got %v, wanted %v", tc.val, v, tc.expected) 479 | } 480 | 481 | } 482 | } 483 | 484 | func TestBigIntConverter(t *testing.T) { 485 | testCases := []struct { 486 | val interface{} 487 | expected driver.Value 488 | err bool 489 | }{ 490 | 491 | { 492 | val: 1000.0, 493 | expected: driver.Value(int64(1000)), 494 | err: false, 495 | }, 496 | 497 | { 498 | val: "foo", 499 | expected: nil, 500 | err: true, 501 | }, 502 | 503 | { 504 | val: "Infinity", 505 | expected: nil, 506 | err: true, 507 | }, 508 | 509 | { 510 | val: "NaN", 511 | expected: nil, 512 | err: true, 513 | }, 514 | 515 | { 516 | val: nil, 517 | expected: nil, 518 | err: false, 519 | }, 520 | } 521 | 522 | for _, tc := range testCases { 523 | v, err := bigIntConverter(tc.val) 524 | 525 | if tc.err == (err == nil) { 526 | t.Errorf("%v: got error %v, wanted %v", tc.val, err, tc.err) 527 | } 528 | 529 | if v != tc.expected { 530 | t.Errorf("%v: got %v, wanted %v", tc.val, v, tc.expected) 531 | } 532 | 533 | } 534 | } 535 | 536 | func TestTimestampConverter(t *testing.T) { 537 | testCases := []struct { 538 | val interface{} 539 | expected driver.Value 540 | err bool 541 | }{ 542 | 543 | { 544 | val: "2015-04-23 10:00:08.123", 545 | expected: time.Date(2015, 04, 23, 10, 0, 8, int(123*time.Millisecond), time.Local), 546 | err: false, 547 | }, 548 | 549 | { 550 | val: 1000.0, 551 | expected: nil, 552 | err: true, 553 | }, 554 | 555 | { 556 | val: "foo", 557 | expected: nil, 558 | err: true, 559 | }, 560 | 561 | { 562 | val: "Infinity", 563 | expected: nil, 564 | err: true, 565 | }, 566 | 567 | { 568 | val: "NaN", 569 | expected: nil, 570 | err: true, 571 | }, 572 | 573 | { 574 | val: nil, 575 | expected: nil, 576 | err: false, 577 | }, 578 | } 579 | 580 | for _, tc := range testCases { 581 | v, err := timestampConverter(tc.val) 582 | 583 | if tc.err == (err == nil) { 584 | t.Errorf("%v: got error %v, wanted %v", tc.val, err, tc.err) 585 | } 586 | 587 | if v != tc.expected { 588 | t.Errorf("%v: got %v, wanted %v", tc.val, v, tc.expected) 589 | } 590 | 591 | } 592 | } 593 | 594 | func TestTimestampWithTimezoneConverter(t *testing.T) { 595 | europeLondon, err := time.LoadLocation("Europe/London") 596 | if err != nil { 597 | t.Fatal(err) 598 | } 599 | 600 | testCases := []struct { 601 | val interface{} 602 | expected driver.Value 603 | err bool 604 | }{ 605 | 606 | { 607 | val: "2015-04-23 10:00:08.123 UTC", 608 | expected: time.Date(2015, 04, 23, 10, 0, 8, int(123*time.Millisecond), time.UTC), 609 | err: false, 610 | }, 611 | 612 | { 613 | val: "2015-04-23 10:00:08.123 Europe/London", 614 | expected: time.Date(2015, 04, 23, 10, 0, 8, int(123*time.Millisecond), europeLondon), 615 | err: false, 616 | }, 617 | 618 | { 619 | val: "2015-04-23 10:00:08.123", 620 | expected: time.Date(2015, 04, 23, 10, 0, 8, int(123*time.Millisecond), time.Local), 621 | err: false, 622 | }, 623 | 624 | { 625 | val: "2015-04-23 10:00:08.123 ", 626 | expected: time.Date(2015, 04, 23, 10, 0, 8, int(123*time.Millisecond), time.UTC), 627 | err: false, 628 | }, 629 | 630 | { 631 | val: "2015-04-23 10:00:08.123 Nowhere", 632 | expected: nil, 633 | err: true, 634 | }, 635 | 636 | { 637 | val: 1000.0, 638 | expected: nil, 639 | err: true, 640 | }, 641 | 642 | { 643 | val: "foo", 644 | expected: nil, 645 | err: true, 646 | }, 647 | 648 | { 649 | val: "Infinity", 650 | expected: nil, 651 | err: true, 652 | }, 653 | 654 | { 655 | val: "NaN", 656 | expected: nil, 657 | err: true, 658 | }, 659 | 660 | { 661 | val: nil, 662 | expected: nil, 663 | err: false, 664 | }, 665 | } 666 | 667 | for _, tc := range testCases { 668 | v, err := timestampWithTimezoneConverter(tc.val) 669 | 670 | if tc.err == (err == nil) { 671 | t.Errorf("%v: got error %v, wanted %v", tc.val, err, tc.err) 672 | } 673 | 674 | if !reflect.DeepEqual(v, tc.expected) { 675 | t.Errorf("%v: got %v, wanted %v", tc.val, v, tc.expected) 676 | } 677 | 678 | } 679 | } 680 | 681 | func TestVarBinaryConverter(t *testing.T) { 682 | testCases := []struct { 683 | val interface{} 684 | expected driver.Value 685 | err bool 686 | }{ 687 | { 688 | val: "AAAAAAAAAAAAAP//2V9/MQ==", 689 | expected: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 217, 95, 127, 49}, 690 | err: false, 691 | }, 692 | { 693 | val: "AAAAAAAAAAAAAP//2V9/MQ==InvalidBase64!", 694 | expected: nil, 695 | err: true, 696 | }, 697 | { 698 | val: 1000.0, 699 | expected: nil, 700 | err: true, 701 | }, 702 | { 703 | val: nil, 704 | expected: nil, 705 | err: false, 706 | }, 707 | } 708 | 709 | for _, tc := range testCases { 710 | v, err := varbinaryConverter(tc.val) 711 | 712 | if tc.err == (err == nil) { 713 | t.Errorf("%v: got error %v, wanted %v", tc.val, err, tc.err) 714 | } 715 | 716 | if !reflect.DeepEqual(v, tc.expected) { 717 | t.Errorf("%v: got %v, wanted %v", tc.val, v, tc.expected) 718 | } 719 | } 720 | } 721 | 722 | func TestMapVarcharConverter(t *testing.T) { 723 | testCases := []struct { 724 | val interface{} 725 | expected driver.Value 726 | err bool 727 | }{ 728 | { 729 | val: map[string]interface{}{"testKey": "testVal"}, 730 | expected: map[string]string{"testKey": "testVal"}, 731 | err: false, 732 | }, 733 | { 734 | val: "InvalidMap", 735 | expected: nil, 736 | err: true, 737 | }, 738 | { 739 | val: nil, 740 | expected: nil, 741 | err: false, 742 | }, 743 | } 744 | 745 | for _, tc := range testCases { 746 | v, err := mapVarcharConverter(tc.val) 747 | 748 | if tc.err == (err == nil) { 749 | t.Errorf("%v: got error %v, wanted %v", tc.val, err, tc.err) 750 | } 751 | 752 | if !reflect.DeepEqual(v, tc.expected) { 753 | t.Errorf("%v: got %v, wanted %v", tc.val, v, tc.expected) 754 | } 755 | 756 | } 757 | } 758 | 759 | func TestArrayVarcharConverter(t *testing.T) { 760 | testCases := []struct { 761 | val interface{} 762 | expected driver.Value 763 | err bool 764 | }{ 765 | { 766 | val: []interface{}{"testVal1", "testVal2"}, 767 | expected: []string{"testVal1", "testVal2"}, 768 | err: false, 769 | }, 770 | { 771 | val: []interface{}{1, 2}, 772 | expected: nil, 773 | err: true, 774 | }, 775 | { 776 | val: "InvalidArray", 777 | expected: nil, 778 | err: true, 779 | }, 780 | { 781 | val: nil, 782 | expected: nil, 783 | err: false, 784 | }, 785 | } 786 | 787 | for _, tc := range testCases { 788 | v, err := arrayVarcharConverter(tc.val) 789 | 790 | if tc.err == (err == nil) { 791 | t.Errorf("%v: got error %v, wanted %v", tc.val, err, tc.err) 792 | } 793 | 794 | if !reflect.DeepEqual(v, tc.expected) { 795 | t.Errorf("%v: got %v, wanted %v", tc.val, v, tc.expected) 796 | } 797 | 798 | } 799 | } 800 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package prestgo 2 | 3 | const ( 4 | // This type captures boolean values true and false 5 | Boolean = "boolean" 6 | 7 | // A 64-bit signed two’s complement integer with a minimum value of -2^63 and a maximum value of 2^63 - 1. 8 | BigInt = "bigint" 9 | 10 | // Integer assumed to be an alias for BigInt. 11 | Integer = "integer" 12 | 13 | // A double is a 64-bit inexact, variable-precision implementing the IEEE Standard 754 for Binary Floating-Point Arithmetic. 14 | Double = "double" 15 | 16 | // Variable length character data. 17 | VarChar = "varchar" 18 | 19 | // Variable length binary data. 20 | VarBinary = "varbinary" 21 | 22 | // Variable length json data. 23 | JSON = "json" 24 | 25 | // Calendar date (year, month, day). 26 | // Example: DATE '2001-08-22' 27 | Date = "date" 28 | 29 | // Time of day (hour, minute, second, millisecond) without a time zone. Values of this type are parsed and rendered in the session time zone. 30 | // Example: TIME '01:02:03.456' 31 | Time = "time" 32 | 33 | // Instant in time that includes the date and time of day without a time zone. Values of this type are parsed and rendered in the session time zone. 34 | // Example: TIMESTAMP '2001-08-22 03:04:05.321' 35 | Timestamp = "timestamp" 36 | 37 | // Instant in time that includes the date and time of day with a time zone. Values of this type are parsed and rendered in the provided time zone. 38 | // Example: TIMESTAMP '2001-08-22 03:04:05.321' AT TIME ZONE 'America/Los_Angeles' 39 | TimestampWithTimezone = "timestamp with time zone" 40 | 41 | // MapVarchar is a map from string-keys to string-values. 42 | MapVarchar = "map(varchar,varchar)" 43 | 44 | // Array of variable length character data. 45 | ArrayVarchar = "array(varchar)" 46 | ) 47 | 48 | type stmtResponse struct { 49 | ID string `json:"id"` 50 | InfoURI string `json:"infoUri"` 51 | NextURI string `json:"nextUri"` 52 | Stats stmtStats `json:"stats"` 53 | Error stmtError `json:"error"` 54 | } 55 | 56 | type stmtStats struct { 57 | State string `json:"state"` 58 | Scheduled bool `json:"scheduled"` 59 | Nodes int `json:"nodes"` 60 | TotalSplits int `json:"totalSplits"` 61 | QueuesSplits int `json:"queuedSplits"` 62 | RunningSplits int `json:"runningSplits"` 63 | CompletedSplits int `json:"completedSplits"` 64 | UserTimeMillis int `json:"userTimeMillis"` 65 | CPUTimeMillis int `json:"cpuTimeMillis"` 66 | WallTimeMillis int `json:"wallTimeMillis"` 67 | ProcessedRows int `json:"processedRows"` 68 | ProcessedBytes int `json:"processedBytes"` 69 | RootStage stmtStage `json:"rootStage"` 70 | } 71 | 72 | type stmtError struct { 73 | Message string `json:"message"` 74 | ErrorCode int `json:"errorCode"` 75 | ErrorLocation stmtErrorLocation `json:"errorLocation"` 76 | FailureInfo stmtErrorFailureInfo `json:"failureInfo"` 77 | // Other fields omitted 78 | } 79 | 80 | type stmtErrorLocation struct { 81 | LineNumber int `json:"lineNumber"` 82 | ColumnNumber int `json:"columnNumber"` 83 | } 84 | 85 | type stmtErrorFailureInfo struct { 86 | Type string `json:"type"` 87 | // Other fields omitted 88 | } 89 | 90 | func (e stmtError) Error() string { 91 | return e.FailureInfo.Type + ": " + e.Message 92 | } 93 | 94 | type stmtStage struct { 95 | StageID string `json:"stageId"` 96 | State string `json:"state"` 97 | Done bool `json:"done"` 98 | Nodes int `json:"nodes"` 99 | TotalSplits int `json:"totalSplits"` 100 | QueuedSplits int `json:"queuedSplits"` 101 | RunningSplits int `json:"runningSplits"` 102 | CompletedSplits int `json:"completedSplits"` 103 | UserTimeMillis int `json:"userTimeMillis"` 104 | CPUTimeMillis int `json:"cpuTimeMillis"` 105 | WallTimeMillis int `json:"wallTimeMillis"` 106 | ProcessedRows int `json:"processedRows"` 107 | ProcessedBytes int `json:"processedBytes"` 108 | SubStages []stmtStage `json:"subStages"` 109 | } 110 | 111 | type queryResponse struct { 112 | ID string `json:"id"` 113 | InfoURI string `json:"infoUri"` 114 | PartialCancelURI string `json:"partialCancelUri"` 115 | NextURI string `json:"nextUri"` 116 | Columns []queryColumn `json:"columns"` 117 | Data []queryData `json:"data"` 118 | Stats stmtStats `json:"stats"` 119 | Error stmtError `json:"error"` 120 | } 121 | 122 | type queryColumn struct { 123 | Name string `json:"name"` 124 | Type string `json:"type"` 125 | TypeSignature typeSignature `json:"typeSignature"` 126 | } 127 | 128 | type queryData []interface{} 129 | 130 | type typeSignature struct { 131 | RawType string `json:"rawType"` 132 | TypeArguments []interface{} `json:"typeArguments"` 133 | LiteralArguments []interface{} `json:"literalArguments"` 134 | } 135 | 136 | type infoResponse struct { 137 | QueryID string `json:"queryId"` 138 | State string `json:"state"` 139 | } 140 | 141 | const ( 142 | QueryStateQueued = "QUEUED" 143 | QueryStatePlanning = "PLANNING" 144 | QueryStateStarting = "STARTING" 145 | QueryStateRunning = "RUNNING" 146 | QueryStateFinished = "FINISHED" 147 | QueryStateCanceled = "CANCELED" 148 | QueryStateFailed = "FAILED" 149 | ) 150 | --------------------------------------------------------------------------------