From 3b9d9d82777c73f3e9a5a4a52ca5ad62996db562 Mon Sep 17 00:00:00 2001 From: Arjen de Rijke Date: Wed, 3 Jan 2024 08:02:31 +0100 Subject: [PATCH] Implement Columntype interfaces (#14) * refactor, start mapi package and move config struct from driver implementation * refactoring, mover sourcefile mapi.go to package mapi * refactoring, move most of the mapi specific code to the resultset class * refactoring, prepare for adding new features * start implementing the columntype interfaces and fix the mapi protocol code * implement the bulk of the columntype functionality * add tests for the columntype implementation and fix the issues that where found * return the correct type for blob fields --- implementation.MD | 50 ++++ src/conn.go | 29 +- src/converter_test.go | 112 -------- src/driver.go | 153 +---------- src/mapi/config.go | 155 +++++++++++ src/{driver_test.go => mapi/config_test.go} | 22 +- src/{ => mapi}/converter.go | 179 ++++++------ src/mapi/converter_test.go | 111 ++++++++ src/{ => mapi}/mapi.go | 89 +++--- src/mapi/resultset.go | 222 +++++++++++++++ src/mapi/resultset_test.go | 101 +++++++ src/{ => mapi}/types.go | 2 +- src/{ => mapi}/types_test.go | 2 +- src/row_integration_test.go | 10 +- src/rows.go | 121 ++++++-- src/rows_integration_test.go | 289 +++++++++++++++++++- src/stmt.go | 259 +++--------------- 17 files changed, 1245 insertions(+), 661 deletions(-) create mode 100644 implementation.MD delete mode 100644 src/converter_test.go create mode 100644 src/mapi/config.go rename src/{driver_test.go => mapi/config_test.go} (79%) rename src/{ => mapi}/converter.go (56%) create mode 100644 src/mapi/converter_test.go rename src/{ => mapi}/mapi.go (79%) create mode 100644 src/mapi/resultset.go create mode 100644 src/mapi/resultset_test.go rename src/{ => mapi}/types.go (98%) rename src/{ => mapi}/types_test.go (99%) diff --git a/implementation.MD b/implementation.MD new file mode 100644 index 0000000..5b96309 --- /dev/null +++ b/implementation.MD @@ -0,0 +1,50 @@ +# Implementation + +## Go sql library + +The [go sql library](https://pkg.go.dev/database/sql) and the [go sql driver library](https://pkg.go.dev/database/sql/driver) + +## MonetDB driver + +The current version, v1.1.0, implemented the Go v1.7 library interfaces. The latest version (go 1.21.5) has more functionality, specifically the column types and supporting the context interface. + +### Mapi + +[Mapi](https://www.monetdb.org/documentation-Jun2023/user-guide/client-interfaces/libraries-drivers/mapi-library/) is the API that provides the communication protocol with the MonetDB database. To improve the mapi protocol implementation in this library, check the documentation in the [PHP](https://github.com/MonetDB/MonetDB-PHP/tree/master/protocol_doc) driver. + +### Refactoring + +We could also use the new [builtin min function](https://pkg.go.dev/builtin#min) + +We will create resultset, resultset schema and resultset metadata types. The code in the current implementation, for example the description type and the statement.storeResult function, will be moved to this go source file. We will move all monetdb specific code out of the implementation of the sql driver interfaces. With the resultset types implemented, it will be relatively easy to implement the columnt type interfaces. + +We will also move the code for handling the connection config out of the driver.go source file. We want to separate the MonetDB specific implementation details from the more generic sql library code as much as possible. This will make the it easier to understand the implementation and easier to implement new interfaces. + +We will prefix the error messages with the package name. That way it is clear where the error message comes from, the driver implementation or the mapi library. + +#### Todo +- [X] in driver.go move parsedsn function call inside newConn +- [X] move tests from driver_test.go to new file after change to driver.open +- [X] move config type from driver.go +- [X] Conn struct doesn't need a config field +- [ ] set_autocommit (see: [pymonetdb](https://github.com/MonetDB/pymonetdb/blob/master/pymonetdb/sql/connections.py#L156C16-L156C16)) +- [ ] change_replysize +- [ ] set_timezone +- [ ] set_uploader +- [ ] set_downloader + +### Logging + +## driver package and sql package latest version + +We need to add context everywhere. We need to implement the RowsColumnType interfaces. We can implement transaction isolation level. + +### context library + +The [go context library](https://pkg.go.dev/context) + +### New interfaces + +### Testing + +The [go testing library](https://pkg.go.dev/testing) diff --git a/src/conn.go b/src/conn.go index 5636285..1754db5 100644 --- a/src/conn.go +++ b/src/conn.go @@ -7,26 +7,30 @@ package monetdb import ( "database/sql/driver" "fmt" + + "github.com/MonetDB/MonetDB-Go/src/mapi" ) type Conn struct { - config config - mapi *MapiConn + mapi *mapi.MapiConn } -func newConn(c config) (*Conn, error) { +func newConn(name string) (*Conn, error) { conn := &Conn{ - config: c, mapi: nil, } - m := NewMapi(c.Hostname, c.Port, c.Username, c.Password, c.Database, "sql") - err := m.Connect() + m, err := mapi.NewMapi(name) if err != nil { return conn, err } + errConn := m.Connect() + if errConn != nil { + return conn, errConn + } conn.mapi = m + m.SetSizeHeader(true) return conn, nil } @@ -51,16 +55,9 @@ func (c *Conn) Begin() (driver.Tx, error) { return t, t.err } -func (c *Conn) cmd(cmd string) (string, error) { +func (c *Conn) execute(query string) (string, error) { if c.mapi == nil { - //lint:ignore ST1005 - return "", fmt.Errorf("Database connection closed") + return "", fmt.Errorf("monetdb: database connection is closed") } - - return c.mapi.Cmd(cmd) -} - -func (c *Conn) execute(q string) (string, error) { - cmd := fmt.Sprintf("s%s;", q) - return c.cmd(cmd) + return c.mapi.Execute(query) } diff --git a/src/converter_test.go b/src/converter_test.go deleted file mode 100644 index dd42cee..0000000 --- a/src/converter_test.go +++ /dev/null @@ -1,112 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package monetdb - -import ( - "bytes" - "database/sql/driver" - "testing" - "time" -) - -func TestConvertToMonet(t *testing.T) { - type tc struct { - v driver.Value - e string - } - var tcs = []tc{ - tc{1, "1"}, - tc{"string", "'string'"}, - tc{"quoted 'string'", "'quoted \\'string\\''"}, - tc{"quoted \"string\"", "'quoted \"string\"'"}, - tc{"back\\slashed", "'back\\\\slashed'"}, - tc{"quoted \\'string\\'", "'quoted \\\\\\'string\\\\\\''"}, - tc{int8(8), "8"}, - tc{int16(16), "16"}, - tc{int32(32), "32"}, - tc{int64(64), "64"}, - tc{float32(3.2), "3.2"}, - tc{float64(6.4), "6.4"}, - tc{true, "true"}, - tc{false, "false"}, - tc{nil, "NULL"}, - tc{[]byte{1, 2, 3}, "'" + string([]byte{1, 2, 3}) + "'"}, - tc{Time{10, 20, 30}, "'10:20:30'"}, - tc{Date{2001, time.January, 2}, "'2001-01-02'"}, - tc{time.Date(2001, time.January, 2, 10, 20, 30, 0, time.FixedZone("CET", 3600)), - "'2001-01-02 10:20:30 +0100 CET'"}, - } - - for _, c := range tcs { - s, err := convertToMonet(c.v) - if err != nil { - t.Errorf("Error converting value: %v -> %v", c.v, err) - } else if s != c.e { - t.Errorf("Invalid value: %s, expected: %s", s, c.e) - } - } -} - -func TestConvertToGo(t *testing.T) { - type tc struct { - v string - t string - e driver.Value - } - var tcs = []tc{ - tc{"8", "tinyint", int8(8)}, - tc{"16", "smallint", int16(16)}, - tc{"16", "shortint", int16(16)}, - tc{"32", "int", int32(32)}, - tc{"32", "mediumint", int32(32)}, - tc{"64", "bigint", int64(64)}, - tc{"64", "longint", int64(64)}, - tc{"64", "hugeint", int64(64)}, - tc{"64", "serial", int64(64)}, - tc{"3.2", "float", float32(3.2)}, - tc{"3.2", "real", float32(3.2)}, - tc{"6.4", "double", float64(6.4)}, - tc{"6.4", "decimal", float64(6.4)}, - tc{"true", "boolean", true}, - tc{"false", "boolean", false}, - tc{"10:20:30", "time", Time{10, 20, 30}}, - tc{"2001-01-02", "date", Date{2001, time.January, 2}}, - tc{"'string'", "char", "string"}, - tc{"'string'", "varchar", "string"}, - tc{"'quoted \"string\"'", "char", "quoted \"string\""}, - tc{"'quoted \\'string\\''", "char", "quoted 'string'"}, - tc{"'quoted \\\\\\'string\\\\\\''", "char", "quoted \\'string\\'"}, - tc{"'back\\\\slashed'", "char", "back\\slashed"}, - tc{"'ABC'", "blob", []uint8{0x41, 0x42, 0x43}}, - } - - for _, c := range tcs { - v, err := convertToGo(c.v, c.t) - if err != nil { - t.Errorf("Error converting value: %v (%s) -> %v", c.v, c.t, err) - } else { - ok := true - switch val := v.(type) { - case []byte: - ok = compareByteArray(t, val, c.e) - default: - ok = v == c.e - } - if !ok { - t.Errorf("Invalid value: %v (%v - %s), expected: %v", v, c.v, c.t, c.e) - } - } - } -} - -func compareByteArray(t *testing.T, val []byte, e driver.Value) bool { - switch exp := e.(type) { - case []byte: - //lint:ignore S1004 prepare to enable staticchecks - return bytes.Compare(val, exp) == 0 - default: - return false - } -} diff --git a/src/driver.go b/src/driver.go index 3dfb56a..ef0a647 100644 --- a/src/driver.go +++ b/src/driver.go @@ -7,10 +7,6 @@ package monetdb import ( "database/sql" "database/sql/driver" - "fmt" - "regexp" - "strconv" - "strings" ) func init() { @@ -20,154 +16,7 @@ func init() { type Driver struct { } -type config struct { - Username string - Password string - Hostname string - Database string - Port int -} - func (*Driver) Open(name string) (driver.Conn, error) { - c, err := parseDSN(name) - if err != nil { - return nil, err - } - - return newConn(c) -} - -func parseDSN(name string) (config, error) { - ipv6_re := regexp.MustCompile(`^((?P[^:]+?)(:(?P[^@]+?))?@)?\[(?P(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))+?)\](:(?P\d+?))?\/(?P.+?)$`) - - if ipv6_re.MatchString(name) { - //lint:ignore SA4006 prepare to enable staticchecks - m := make([]string, 0) - //lint:ignore SA4006 prepare to enable staticchecks - n := make([]string, 0) - m = ipv6_re.FindAllStringSubmatch(name, -1)[0] - n = ipv6_re.SubexpNames() - return getConfig(m, n, true), nil - } - - c := config{ - Hostname: "localhost", - Port: 50000, - } - - reversed := reverse(name) - - host, creds, _ := Cut(reversed, "@") // host, creds, found - - configWithHost, err := parseHost(reverse(host), c) - - if err != nil { - //lint:ignore ST1005 prepare to enable staticchecks - return config{}, fmt.Errorf("Invalid DSN") - } - - newConfig, err := parseCreds(reverse(creds), configWithHost) - - if err != nil { - //lint:ignore ST1005 prepare to enable staticchecks - return config{}, fmt.Errorf("Invalid DSN") - } - - return newConfig, nil -} - -func parseCreds(creds string, c config) (config, error) { - username, password, found := Cut(creds, ":") - - c.Username = username - c.Password = "" - - if found { - if username == "" { - //lint:ignore ST1005 prepare to enable staticchecks - return c, fmt.Errorf("Invalid DSN") - } - - c.Password = password - } - - return c, nil + return newConn(name) } -func parseHost(host string, c config) (config, error) { - host, dbName, found := Cut(host, "/") - - if !found { - //lint:ignore ST1005 prepare to enable staticchecks - return c, fmt.Errorf("Invalid DSN") - } - - if host == "" { - //lint:ignore ST1005 prepare to enable staticchecks - return c, fmt.Errorf("Invalid DSN") - } - - c.Database = dbName - - hostname, port, found := Cut(host, ":") - - if !found { - return c, nil - } - - c.Hostname = hostname - - port_num, err := strconv.Atoi(port) - - if err != nil { - //lint:ignore ST1005 prepare to enable staticchecks - return c, fmt.Errorf("Invalid DSN") - } - - c.Port = port_num - - return c, nil -} - -func getConfig(m []string, n []string, ipv6 bool) config { - c := config{ - Hostname: "localhost", - Port: 50000, - } - for i, v := range m { - if n[i] == "username" { - c.Username = v - } else if n[i] == "password" { - c.Password = v - } else if n[i] == "hostname" { - if ipv6 { - c.Hostname = fmt.Sprintf("[%s]", v) - continue - } - - c.Hostname = v - } else if n[i] == "port" && v != "" { - c.Port, _ = strconv.Atoi(v) - } else if n[i] == "database" { - c.Database = v - } - } - - return c -} - -func reverse(in string) string { - var sb strings.Builder - runes := []rune(in) - for i := len(runes) - 1; 0 <= i; i-- { - sb.WriteRune(runes[i]) - } - return sb.String() -} - -func Cut(s, sep string) (before, after string, found bool) { - if i := strings.Index(s, sep); i >= 0 { - return s[:i], s[i+len(sep):], true - } - return s, "", false -} diff --git a/src/mapi/config.go b/src/mapi/config.go new file mode 100644 index 0000000..9938696 --- /dev/null +++ b/src/mapi/config.go @@ -0,0 +1,155 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mapi + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +type config struct { + Username string + Password string + Hostname string + Database string + Port int +} + +func parseDSN(name string) (config, error) { + ipv6_re := regexp.MustCompile(`^((?P[^:]+?)(:(?P[^@]+?))?@)?\[(?P(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))+?)\](:(?P\d+?))?\/(?P.+?)$`) + + if ipv6_re.MatchString(name) { + //lint:ignore SA4006 prepare to enable staticchecks + m := make([]string, 0) + //lint:ignore SA4006 prepare to enable staticchecks + n := make([]string, 0) + m = ipv6_re.FindAllStringSubmatch(name, -1)[0] + n = ipv6_re.SubexpNames() + return getConfig(m, n, true), nil + } + + c := config{ + Hostname: "localhost", + Port: 50000, + } + + reversed := reverse(name) + + host, creds, _ := Cut(reversed, "@") // host, creds, found + + configWithHost, err := parseHost(reverse(host), c) + + if err != nil { + //lint:ignore ST1005 prepare to enable staticchecks + return config{}, fmt.Errorf("Invalid DSN") + } + + newConfig, err := parseCreds(reverse(creds), configWithHost) + + if err != nil { + //lint:ignore ST1005 prepare to enable staticchecks + return config{}, fmt.Errorf("Invalid DSN") + } + + return newConfig, nil +} + +func parseCreds(creds string, c config) (config, error) { + username, password, found := Cut(creds, ":") + + c.Username = username + c.Password = "" + + if found { + if username == "" { + //lint:ignore ST1005 prepare to enable staticchecks + return c, fmt.Errorf("Invalid DSN") + } + + c.Password = password + } + + return c, nil +} + +func parseHost(host string, c config) (config, error) { + host, dbName, found := Cut(host, "/") + + if !found { + //lint:ignore ST1005 prepare to enable staticchecks + return c, fmt.Errorf("Invalid DSN") + } + + if host == "" { + //lint:ignore ST1005 prepare to enable staticchecks + return c, fmt.Errorf("Invalid DSN") + } + + c.Database = dbName + + hostname, port, found := Cut(host, ":") + + if !found { + return c, nil + } + + c.Hostname = hostname + + port_num, err := strconv.Atoi(port) + + if err != nil { + //lint:ignore ST1005 prepare to enable staticchecks + return c, fmt.Errorf("Invalid DSN") + } + + c.Port = port_num + + return c, nil +} + +func getConfig(m []string, n []string, ipv6 bool) config { + c := config{ + Hostname: "localhost", + Port: 50000, + } + for i, v := range m { + if n[i] == "username" { + c.Username = v + } else if n[i] == "password" { + c.Password = v + } else if n[i] == "hostname" { + if ipv6 { + c.Hostname = fmt.Sprintf("[%s]", v) + continue + } + + c.Hostname = v + } else if n[i] == "port" && v != "" { + c.Port, _ = strconv.Atoi(v) + } else if n[i] == "database" { + c.Database = v + } + } + + return c +} + +func reverse(in string) string { + var sb strings.Builder + runes := []rune(in) + for i := len(runes) - 1; 0 <= i; i-- { + sb.WriteRune(runes[i]) + } + return sb.String() +} + +func Cut(s, sep string) (before, after string, found bool) { + if i := strings.Index(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true + } + return s, "", false +} diff --git a/src/driver_test.go b/src/mapi/config_test.go similarity index 79% rename from src/driver_test.go rename to src/mapi/config_test.go index 34b9dbd..ff3b902 100644 --- a/src/driver_test.go +++ b/src/mapi/config_test.go @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package monetdb +package mapi import ( "strconv" @@ -11,16 +11,16 @@ import ( func TestParseDSN(t *testing.T) { tcs := [][]string{ - []string{"me:secret@localhost:1234/testdb", "me", "secret", "localhost", "1234", "testdb"}, - []string{"me@localhost:1234/testdb", "me", "", "localhost", "1234", "testdb"}, - []string{"localhost:1234/testdb", "", "", "localhost", "1234", "testdb"}, - []string{"user:P@sswordWith@@localhost:50000/db", "user", "P@sswordWith@", "localhost", "50000", "db"}, - []string{"localhost/testdb", "", "", "localhost", "50000", "testdb"}, - []string{"localhost"}, - []string{"/testdb"}, - []string{"/"}, - []string{""}, - []string{":secret@localhost:1234/testdb"}, + {"me:secret@localhost:1234/testdb", "me", "secret", "localhost", "1234", "testdb"}, + {"me@localhost:1234/testdb", "me", "", "localhost", "1234", "testdb"}, + {"localhost:1234/testdb", "", "", "localhost", "1234", "testdb"}, + {"user:P@sswordWith@@localhost:50000/db", "user", "P@sswordWith@", "localhost", "50000", "db"}, + {"localhost/testdb", "", "", "localhost", "50000", "testdb"}, + {"localhost"}, + {"/testdb"}, + {"/"}, + {""}, + {":secret@localhost:1234/testdb"}, } for _, tc := range tcs { diff --git a/src/converter.go b/src/mapi/converter.go similarity index 56% rename from src/converter.go rename to src/mapi/converter.go index f091a04..14c88b3 100644 --- a/src/converter.go +++ b/src/mapi/converter.go @@ -2,10 +2,9 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package monetdb +package mapi import ( - "database/sql/driver" "fmt" "reflect" "strconv" @@ -15,50 +14,50 @@ import ( ) const ( - mdb_CHAR = "char" // (L) character string with length L - mdb_VARCHAR = "varchar" // (L) string with atmost length L - mdb_CLOB = "clob" - mdb_BLOB = "blob" - mdb_DECIMAL = "decimal" // (P,S) - mdb_SMALLINT = "smallint" // 16 bit integer - mdb_INT = "int" // 32 bit integer - mdb_BIGINT = "bigint" // 64 bit integer - mdb_HUGEINT = "hugeint" // 64 bit integer - mdb_SERIAL = "serial" // special 64 bit integer sequence generator - mdb_REAL = "real" // 32 bit floating point - mdb_DOUBLE = "double" // 64 bit floating point - mdb_BOOLEAN = "boolean" - mdb_DATE = "date" - mdb_NULL = "NULL" - mdb_TIME = "time" // (T) time of day - mdb_TIMESTAMP = "timestamp" // (T) date concatenated with unique time - mdb_INTERVAL = "interval" // (Q) a temporal interval - - mdb_MONTH_INTERVAL = "month_interval" - mdb_SEC_INTERVAL = "sec_interval" - mdb_WRD = "wrd" - mdb_TINYINT = "tinyint" + MDB_CHAR = "char" // (L) character string with length L + MDB_VARCHAR = "varchar" // (L) string with atmost length L + MDB_CLOB = "clob" + MDB_BLOB = "blob" + MDB_DECIMAL = "decimal" // (P,S) + MDB_SMALLINT = "smallint" // 16 bit integer + MDB_INT = "int" // 32 bit integer + MDB_BIGINT = "bigint" // 64 bit integer + MDB_HUGEINT = "hugeint" // 64 bit integer + MDB_SERIAL = "serial" // special 64 bit integer sequence generator + MDB_REAL = "real" // 32 bit floating point + MDB_DOUBLE = "double" // 64 bit floating point + MDB_BOOLEAN = "boolean" + MDB_DATE = "date" + MDB_NULL = "NULL" + MDB_TIME = "time" // (T) time of day + MDB_TIMESTAMP = "timestamp" // (T) date concatenated with unique time + MDB_INTERVAL = "interval" // (Q) a temporal interval + + MDB_MONTH_INTERVAL = "month_interval" + MDB_SEC_INTERVAL = "sec_interval" + MDB_WRD = "wrd" + MDB_TINYINT = "tinyint" // Not on the website: - mdb_SHORTINT = "shortint" - mdb_MEDIUMINT = "mediumint" - mdb_LONGINT = "longint" - mdb_FLOAT = "float" - mdb_TIMESTAMPTZ = "timestamptz" + MDB_SHORTINT = "shortint" + MDB_MEDIUMINT = "mediumint" + MDB_LONGINT = "longint" + MDB_FLOAT = "float" + MDB_TIMESTAMPTZ = "timestamptz" // full names and aliases, spaces are replaced with underscores //lint:ignore U1000 prepare to enable staticchecks - mdb_CHARACTER = mdb_CHAR + mdb_CHARACTER = MDB_CHAR //lint:ignore U1000 prepare to enable staticchecks - mdb_CHARACTER_VARYING = mdb_VARCHAR + mdb_CHARACTER_VARYING = MDB_VARCHAR //lint:ignore U1000 prepare to enable staticchecks - mdb_CHARACHTER_LARGE_OBJECT = mdb_CLOB + mdb_CHARACHTER_LARGE_OBJECT = MDB_CLOB //lint:ignore U1000 prepare to enable staticchecks - mdb_BINARY_LARGE_OBJECT = mdb_BLOB + mdb_BINARY_LARGE_OBJECT = MDB_BLOB //lint:ignore U1000 prepare to enable staticchecks - mdb_NUMERIC = mdb_DECIMAL + mdb_NUMERIC = MDB_DECIMAL //lint:ignore U1000 prepare to enable staticchecks - mdb_DOUBLE_PRECISION = mdb_DOUBLE + mdb_DOUBLE_PRECISION = MDB_DOUBLE ) var timeFormats = []string{ @@ -67,13 +66,14 @@ var timeFormats = []string{ "2006-01-02 15:04:05 -0700", "2006-01-02 15:04:05 -0700 MST", "Mon Jan 2 15:04:05 -0700 MST 2006", + "2006-01-02 15:04:05.999999+00:00", "15:04:05", } -type toGoConverter func(string) (driver.Value, error) -type toMonetConverter func(driver.Value) (string, error) +type toGoConverter func(string) (Value, error) +type toMonetConverter func(Value) (string, error) -func strip(v string) (driver.Value, error) { +func strip(v string) (Value, error) { return unquote(strings.TrimSpace(v[1 : len(v)-1])) } @@ -114,15 +114,15 @@ func unquote(s string) (string, error) { return string(buf), nil } -func toByteArray(v string) (driver.Value, error) { +func toByteArray(v string) (Value, error) { return []byte(v[1 : len(v)-1]), nil } -func toDouble(v string) (driver.Value, error) { +func toDouble(v string) (Value, error) { return strconv.ParseFloat(v, 64) } -func toFloat(v string) (driver.Value, error) { +func toFloat(v string) (Value, error) { var r float32 i, err := strconv.ParseFloat(v, 32) if err == nil { @@ -131,7 +131,7 @@ func toFloat(v string) (driver.Value, error) { return r, err } -func toInt8(v string) (driver.Value, error) { +func toInt8(v string) (Value, error) { var r int8 i, err := strconv.ParseInt(v, 10, 8) if err == nil { @@ -140,7 +140,7 @@ func toInt8(v string) (driver.Value, error) { return r, err } -func toInt16(v string) (driver.Value, error) { +func toInt16(v string) (Value, error) { var r int16 i, err := strconv.ParseInt(v, 10, 16) if err == nil { @@ -149,7 +149,7 @@ func toInt16(v string) (driver.Value, error) { return r, err } -func toInt32(v string) (driver.Value, error) { +func toInt32(v string) (Value, error) { var r int32 i, err := strconv.ParseInt(v, 10, 32) if err == nil { @@ -159,7 +159,7 @@ func toInt32(v string) (driver.Value, error) { return r, err } -func toInt64(v string) (driver.Value, error) { +func toInt64(v string) (Value, error) { var r int64 i, err := strconv.ParseInt(v, 10, 64) if err == nil { @@ -179,15 +179,15 @@ func parseTime(v string) (t time.Time, err error) { return } -func toNil(v string) (driver.Value, error) { +func toNil(v string) (Value, error) { return "NULL", nil } -func toBool(v string) (driver.Value, error) { +func toBool(v string) (Value, error) { return strconv.ParseBool(v) } -func toDate(v string) (driver.Value, error) { +func toDate(v string) (Value, error) { t, err := parseTime(v) if err != nil { return nil, err @@ -196,7 +196,7 @@ func toDate(v string) (driver.Value, error) { return Date{year, month, day}, nil } -func toTime(v string) (driver.Value, error) { +func toTime(v string) (Value, error) { t, err := parseTime(v) if err != nil { return nil, err @@ -204,69 +204,68 @@ func toTime(v string) (driver.Value, error) { hour, min, sec := t.Clock() return Time{hour, min, sec}, nil } -func toTimestamp(v string) (driver.Value, error) { +func toTimestamp(v string) (Value, error) { return parseTime(v) } -func toTimestampTz(v string) (driver.Value, error) { +func toTimestampTz(v string) (Value, error) { return parseTime(v) } var toGoMappers = map[string]toGoConverter{ - mdb_CHAR: strip, - mdb_VARCHAR: strip, - mdb_CLOB: strip, - mdb_BLOB: toByteArray, - mdb_DECIMAL: toDouble, - mdb_NULL: toNil, - mdb_SMALLINT: toInt16, - mdb_INT: toInt32, - mdb_WRD: toInt32, - mdb_BIGINT: toInt64, - mdb_HUGEINT: toInt64, - mdb_SERIAL: toInt64, - mdb_REAL: toFloat, - mdb_DOUBLE: toDouble, - mdb_BOOLEAN: toBool, - mdb_DATE: toDate, - mdb_TIME: toTime, - mdb_TIMESTAMP: toTimestamp, - mdb_TIMESTAMPTZ: toTimestampTz, - mdb_INTERVAL: strip, - mdb_MONTH_INTERVAL: strip, - mdb_SEC_INTERVAL: strip, - mdb_TINYINT: toInt8, - mdb_SHORTINT: toInt16, - mdb_MEDIUMINT: toInt32, - mdb_LONGINT: toInt64, - mdb_FLOAT: toFloat, + MDB_CHAR: strip, + MDB_VARCHAR: strip, + MDB_CLOB: strip, + MDB_BLOB: toByteArray, + MDB_DECIMAL: toDouble, + MDB_NULL: toNil, + MDB_SMALLINT: toInt16, + MDB_INT: toInt32, + MDB_WRD: toInt32, + MDB_BIGINT: toInt64, + MDB_HUGEINT: toInt64, + MDB_SERIAL: toInt64, + MDB_REAL: toFloat, + MDB_DOUBLE: toDouble, + MDB_BOOLEAN: toBool, + MDB_DATE: toDate, + MDB_TIME: toTime, + MDB_TIMESTAMP: toTimestamp, + MDB_TIMESTAMPTZ: toTimestampTz, + MDB_INTERVAL: strip, + MDB_MONTH_INTERVAL: strip, + MDB_SEC_INTERVAL: strip, + MDB_TINYINT: toInt8, + MDB_SHORTINT: toInt16, + MDB_MEDIUMINT: toInt32, + MDB_LONGINT: toInt64, + MDB_FLOAT: toFloat, } -func toString(v driver.Value) (string, error) { +func toString(v Value) (string, error) { return fmt.Sprintf("%v", v), nil } -func toQuotedString(v driver.Value) (string, error) { +func toQuotedString(v Value) (string, error) { s := fmt.Sprintf("%v", v) s = strings.Replace(s, "\\", "\\\\", -1) s = strings.Replace(s, "'", "\\'", -1) return fmt.Sprintf("'%v'", s), nil } -func toNull(v driver.Value) (string, error) { +func toNull(v Value) (string, error) { return "NULL", nil } -func toByteString(v driver.Value) (string, error) { +func toByteString(v Value) (string, error) { switch val := v.(type) { case []uint8: return toQuotedString(string(val)) default: - //lint:ignore ST1005 prepare to enable staticchecks - return "", fmt.Errorf("Unsupported type") + return "", fmt.Errorf("unsupported type") } } -func toDateTimeString(v driver.Value) (string, error) { +func toDateTimeString(v Value) (string, error) { switch val := v.(type) { case Time: return toQuotedString(fmt.Sprintf("%02d:%02d:%02d", val.Hour, val.Min, val.Sec)) @@ -293,11 +292,11 @@ var toMonetMappers = map[string]toMonetConverter{ "null": toNull, "[]uint8": toByteString, "time.Time": toQuotedString, - "monetdb.Time": toDateTimeString, - "monetdb.Date": toDateTimeString, + "mapi.Time": toDateTimeString, + "mapi.Date": toDateTimeString, } -func convertToGo(value, dataType string) (driver.Value, error) { +func convertToGo(value, dataType string) (Value, error) { if strings.TrimSpace(value) == "NULL" { dataType = "NULL" } @@ -310,7 +309,7 @@ func convertToGo(value, dataType string) (driver.Value, error) { return nil, fmt.Errorf("Type not supported: %s", dataType) } -func convertToMonet(value driver.Value) (string, error) { +func ConvertToMonet(value Value) (string, error) { t := reflect.TypeOf(value) n := "nil" if t != nil { diff --git a/src/mapi/converter_test.go b/src/mapi/converter_test.go new file mode 100644 index 0000000..7108722 --- /dev/null +++ b/src/mapi/converter_test.go @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mapi + +import ( + "bytes" + "testing" + "time" +) + +func TestConvertToMonet(t *testing.T) { + type tc struct { + v Value + e string + } + var tcs = []tc{ + {1, "1"}, + {"string", "'string'"}, + {"quoted 'string'", "'quoted \\'string\\''"}, + {"quoted \"string\"", "'quoted \"string\"'"}, + {"back\\slashed", "'back\\\\slashed'"}, + {"quoted \\'string\\'", "'quoted \\\\\\'string\\\\\\''"}, + {int8(8), "8"}, + {int16(16), "16"}, + {int32(32), "32"}, + {int64(64), "64"}, + {float32(3.2), "3.2"}, + {float64(6.4), "6.4"}, + {true, "true"}, + {false, "false"}, + {nil, "NULL"}, + {[]byte{1, 2, 3}, "'" + string([]byte{1, 2, 3}) + "'"}, + {Time{10, 20, 30}, "'10:20:30'"}, + {Date{2001, time.January, 2}, "'2001-01-02'"}, + {time.Date(2001, time.January, 2, 10, 20, 30, 0, time.FixedZone("CET", 3600)), + "'2001-01-02 10:20:30 +0100 CET'"}, + } + + for _, c := range tcs { + s, err := ConvertToMonet(c.v) + if err != nil { + t.Errorf("Error converting value: %v -> %v", c.v, err) + } else if s != c.e { + t.Errorf("Invalid value: %s, expected: %s", s, c.e) + } + } +} + +func TestConvertToGo(t *testing.T) { + type tc struct { + v string + t string + e Value + } + var tcs = []tc{ + {"8", "tinyint", int8(8)}, + {"16", "smallint", int16(16)}, + {"16", "shortint", int16(16)}, + {"32", "int", int32(32)}, + {"32", "mediumint", int32(32)}, + {"64", "bigint", int64(64)}, + {"64", "longint", int64(64)}, + {"64", "hugeint", int64(64)}, + {"64", "serial", int64(64)}, + {"3.2", "float", float32(3.2)}, + {"3.2", "real", float32(3.2)}, + {"6.4", "double", float64(6.4)}, + {"6.4", "decimal", float64(6.4)}, + {"true", "boolean", true}, + {"false", "boolean", false}, + {"10:20:30", "time", Time{10, 20, 30}}, + {"2001-01-02", "date", Date{2001, time.January, 2}}, + {"'string'", "char", "string"}, + {"'string'", "varchar", "string"}, + {"'quoted \"string\"'", "char", "quoted \"string\""}, + {"'quoted \\'string\\''", "char", "quoted 'string'"}, + {"'quoted \\\\\\'string\\\\\\''", "char", "quoted \\'string\\'"}, + {"'back\\\\slashed'", "char", "back\\slashed"}, + {"'ABC'", "blob", []uint8{0x41, 0x42, 0x43}}, + } + + for _, c := range tcs { + v, err := convertToGo(c.v, c.t) + if err != nil { + t.Errorf("Error converting value: %v (%s) -> %v", c.v, c.t, err) + } else { + ok := true + switch val := v.(type) { + case []byte: + ok = compareByteArray(t, val, c.e) + default: + ok = v == c.e + } + if !ok { + t.Errorf("Invalid value: %v (%v - %s), expected: %v", v, c.v, c.t, c.e) + } + } + } +} + +func compareByteArray(t *testing.T, val []byte, e Value) bool { + switch exp := e.(type) { + case []byte: + //lint:ignore S1004 prepare to enable staticchecks + return bytes.Compare(val, exp) == 0 + default: + return false + } +} diff --git a/src/mapi.go b/src/mapi/mapi.go similarity index 79% rename from src/mapi.go rename to src/mapi/mapi.go index 59ab5d8..06416f6 100644 --- a/src/mapi.go +++ b/src/mapi/mapi.go @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package monetdb +package mapi import ( "bytes" @@ -39,10 +39,10 @@ const ( ) // MAPI connection is established. -const MAPI_STATE_READY = 1 +const mapi_STATE_READY = 1 // MAPI connection is NOT established. -const MAPI_STATE_INIT = 0 +const mapi_STATE_INIT = 0 var ( mapi_MSG_MORE = string([]byte{1, 2, 10}) @@ -73,33 +73,59 @@ type MapiConn struct { // NewMapi returns a MonetDB's MAPI connection handle. // // To establish the connection, call the Connect() function. -func NewMapi(hostname string, port int, username, password, database, language string) *MapiConn { +func NewMapi(name string) (*MapiConn, error) { + var language = "sql" + c, err := parseDSN(name) + if err != nil { + return nil, err + } + return &MapiConn{ - Hostname: hostname, - Port: port, - Username: username, - Password: password, - Database: database, + Hostname: c.Hostname, + Port: c.Port, + Username: c.Username, + Password: c.Password, + Database: c.Database, Language: language, - State: MAPI_STATE_INIT, - } + State: mapi_STATE_INIT, + }, nil } // Disconnect closes the connection. func (c *MapiConn) Disconnect() { - c.State = MAPI_STATE_INIT + c.State = mapi_STATE_INIT if c.conn != nil { c.conn.Close() c.conn = nil } } +func (c *MapiConn) Execute(query string) (string, error) { + cmd := fmt.Sprintf("s%s;", query) + return c.cmd(cmd) +} + +func (c *MapiConn) FetchNext(queryId int, offset int, amount int) (string, error) { + cmd := fmt.Sprintf("Xexport %d %d %d", queryId, offset, amount) + return c.cmd(cmd) +} + +func (c *MapiConn) SetSizeHeader(enable bool) (string, error) { + var sizeheader int + if enable { + sizeheader = 1 + } else { + sizeheader = 0 + } + cmd := fmt.Sprintf("Xsizeheader %d", sizeheader) + return c.cmd(cmd) +} + // Cmd sends a MAPI command to MonetDB. -func (c *MapiConn) Cmd(operation string) (string, error) { - if c.State != MAPI_STATE_READY { - //lint:ignore ST1005 prepare to enable staticchecks - return "", fmt.Errorf("Database not connected") +func (c *MapiConn) cmd(operation string) (string, error) { + if c.State != mapi_STATE_READY { + return "", fmt.Errorf("mapi: database is not connected") } if err := c.putBlock([]byte(operation)); err != nil { @@ -120,18 +146,16 @@ func (c *MapiConn) Cmd(operation string) (string, error) { } else if resp == mapi_MSG_MORE { // tell server it isn't going to get more - return c.Cmd("") + return c.cmd("") } else if strings.HasPrefix(resp, mapi_MSG_Q) || strings.HasPrefix(resp, mapi_MSG_HEADER) || strings.HasPrefix(resp, mapi_MSG_TUPLE) { return resp, nil } else if strings.HasPrefix(resp, mapi_MSG_ERROR) { - //lint:ignore ST1005 prepare to enable staticchecks - return "", fmt.Errorf("Operational error: %s", resp[1:]) + return "", fmt.Errorf("mapi: operational error: %s", resp[1:]) } else { - //lint:ignore ST1005 prepare to enable staticchecks - return "", fmt.Errorf("Unknown state: %s", resp) + return "", fmt.Errorf("mapi: unknown state: %s", resp) } } @@ -201,8 +225,7 @@ func (c *MapiConn) tryLogin(iteration int) error { } else if strings.HasPrefix(prompt, mapi_MSG_ERROR) { // TODO log error - //lint:ignore ST1005 prepare to enable staticchecks - return fmt.Errorf("Database error: %s", prompt[1:]) + return fmt.Errorf("mapi: database error: %s", prompt[1:]) } else if strings.HasPrefix(prompt, mapi_MSG_REDIRECT) { t := strings.Split(prompt, " ") @@ -213,8 +236,7 @@ func (c *MapiConn) tryLogin(iteration int) error { if iteration <= 10 { c.tryLogin(iteration + 1) } else { - //lint:ignore ST1005 prepare to enable staticchecks - return fmt.Errorf("Maximal number of redirects reached (10)") + return fmt.Errorf("mapi: maximal number of redirects reached (10)") } } else if r[1] == "monetdb" { @@ -227,15 +249,13 @@ func (c *MapiConn) tryLogin(iteration int) error { c.Connect() } else { - //lint:ignore ST1005 prepare to enable staticchecks - return fmt.Errorf("Unknown redirect: %s", prompt) + return fmt.Errorf("mapi: unknown redirect: %s", prompt) } } else { - //lint:ignore ST1005 prepare to enable staticchecks - return fmt.Errorf("Unknown state: %s", prompt) + return fmt.Errorf("mapi: unknown state: %s", prompt) } - c.State = MAPI_STATE_READY + c.State = mapi_STATE_READY return nil } @@ -249,8 +269,7 @@ func (c *MapiConn) challengeResponse(challenge []byte) (string, error) { algo := t[5] if protocol != "9" { - //lint:ignore ST1005 prepare to enable staticchecks - return "", fmt.Errorf("We only speak protocol v9") + return "", fmt.Errorf("mapi: we only speak protocol v9") } var h hash.Hash @@ -258,8 +277,7 @@ func (c *MapiConn) challengeResponse(challenge []byte) (string, error) { h = crypto.SHA512.New() } else { // TODO support more algorithm - //lint:ignore ST1005 prepare to enable staticchecks - return "", fmt.Errorf("Unsupported algorithm: %s", algo) + return "", fmt.Errorf("mapi: unsupported algorithm: %s", algo) } io.WriteString(h, c.Password) p := fmt.Sprintf("%x", h.Sum(nil)) @@ -279,8 +297,7 @@ func (c *MapiConn) challengeResponse(challenge []byte) (string, error) { pwhash = fmt.Sprintf("{MD5}%x", h.Sum(nil)) } else { - //lint:ignore ST1005 prepare to enable staticchecks - return "", fmt.Errorf("Unsupported hash algorithm required for login %s", hashes) + return "", fmt.Errorf("mapi: unsupported hash algorithm required for login %s", hashes) } r := fmt.Sprintf("BIG:%s:%s:%s:%s:", c.Username, pwhash, c.Language, c.Database) diff --git a/src/mapi/resultset.go b/src/mapi/resultset.go new file mode 100644 index 0000000..a98777d --- /dev/null +++ b/src/mapi/resultset.go @@ -0,0 +1,222 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mapi + +import ( + "bytes" + "fmt" + "strconv" + "strings" +) + +type TableElement struct { + ColumnName string + ColumnType string + DisplaySize int + InternalSize int + Precision int + Scale int + NullOk int +} + +type Metadata struct { + ExecId int + LastRowId int + RowCount int + QueryId int + Offset int + ColumnCount int +} + +type Value interface{} + +type ResultSet struct { + Metadata Metadata + Schema []TableElement + Rows [][]Value +} + +func (s *ResultSet) StoreResult(r string) error { + var columnNames []string + var columnTypes []string + var displaySizes []int + var internalSizes []int + var precisions []int + var scales []int + var nullOks []int + + for _, line := range strings.Split(r, "\n") { + if strings.HasPrefix(line, mapi_MSG_INFO) { + // TODO log + + } else if strings.HasPrefix(line, mapi_MSG_QPREPARE) { + t := strings.Split(strings.TrimSpace(line[2:]), " ") + s.Metadata.ExecId, _ = strconv.Atoi(t[0]) + return nil + + } else if strings.HasPrefix(line, mapi_MSG_QTABLE) { + t := strings.Split(strings.TrimSpace(line[2:]), " ") + s.Metadata.QueryId, _ = strconv.Atoi(t[0]) + s.Metadata.RowCount, _ = strconv.Atoi(t[1]) + s.Metadata.ColumnCount, _ = strconv.Atoi(t[2]) + + columnNames = make([]string, s.Metadata.ColumnCount) + columnTypes = make([]string, s.Metadata.ColumnCount) + displaySizes = make([]int, s.Metadata.ColumnCount) + internalSizes = make([]int, s.Metadata.ColumnCount) + precisions = make([]int, s.Metadata.ColumnCount) + scales = make([]int, s.Metadata.ColumnCount) + nullOks = make([]int, s.Metadata.ColumnCount) + + } else if strings.HasPrefix(line, mapi_MSG_TUPLE) { + v, err := s.parseTuple(line) + if err != nil { + return err + } + s.Rows = append(s.Rows, v) + + } else if strings.HasPrefix(line, mapi_MSG_QBLOCK) { + s.Rows = make([][]Value, 0) + + } else if strings.HasPrefix(line, mapi_MSG_QSCHEMA) { + s.Metadata.Offset = 0 + s.Rows = make([][]Value, 0) + s.Metadata.LastRowId = 0 + s.Schema = nil + s.Metadata.RowCount = 0 + + } else if strings.HasPrefix(line, mapi_MSG_QUPDATE) { + t := strings.Split(strings.TrimSpace(line[2:]), " ") + s.Metadata.RowCount, _ = strconv.Atoi(t[0]) + s.Metadata.LastRowId, _ = strconv.Atoi(t[1]) + + } else if strings.HasPrefix(line, mapi_MSG_QTRANS) { + s.Metadata.Offset = 0 + s.Rows = make([][]Value, 0) + s.Metadata.LastRowId = 0 + s.Schema = nil + s.Metadata.RowCount = 0 + + } else if strings.HasPrefix(line, mapi_MSG_HEADER) { + t := strings.Split(line[1:], "#") + data := strings.TrimSpace(t[0]) + identity := strings.TrimSpace(t[1]) + + values := make([]string, 0) + for _, value := range strings.Split(data, ",") { + values = append(values, strings.TrimSpace(value)) + } + + if identity == "name" { + columnNames = values + + } else if identity == "type" { + columnTypes = values + + } else if identity == "typesizes" { + sizes := make([][]int, len(values)) + for i, value := range values { + s := make([]int, 0) + for _, v := range strings.Split(value, " ") { + val, _ := strconv.Atoi(v) + s = append(s, val) + } + internalSizes[i] = s[0] + sizes[i] = s + } + for j, t := range columnTypes { + if t == "decimal" { + precisions[j] = sizes[j][0] + scales[j] = sizes[j][1] + } + } + } else if identity == "length" { + for i, value := range values { + s := make([]int, 0) + for _, v := range strings.Split(value, " ") { + val, _ := strconv.Atoi(v) + s = append(s, val) + } + displaySizes[i] = s[0] + } + } + + s.updateSchema(columnNames, columnTypes, displaySizes, + internalSizes, precisions, scales, nullOks) + s.Metadata.Offset = 0 + s.Metadata.LastRowId = 0 + + } else if strings.HasPrefix(line, mapi_MSG_PROMPT) { + return nil + + } else if strings.HasPrefix(line, mapi_MSG_ERROR) { + return fmt.Errorf("mapi: database error: %s", line[1:]) + } + } + + return fmt.Errorf("mapi: unknown state: %s", r) +} + +func (s *ResultSet) parseTuple(d string) ([]Value, error) { + items := strings.Split(d[1:len(d)-1], ",\t") + if len(items) != len(s.Schema) { + return nil, fmt.Errorf("mapi: length of row doesn't match header") + } + + v := make([]Value, len(items)) + for i, value := range items { + vv, err := s.convert(value, s.Schema[i].ColumnType) + if err != nil { + return nil, err + } + v[i] = vv + } + return v, nil +} + +func (s *ResultSet) updateSchema( + columnNames, columnTypes []string, displaySizes, + internalSizes, precisions, scales, nullOks []int) { + + d := make([]TableElement, len(columnNames)) + for i, columnName := range columnNames { + desc := TableElement{ + ColumnName: columnName, + ColumnType: columnTypes[i], + DisplaySize: displaySizes[i], + InternalSize: internalSizes[i], + Precision: precisions[i], + Scale: scales[i], + NullOk: nullOks[i], + } + d[i] = desc + } + + s.Schema = d +} + +func (s *ResultSet) convert(value, dataType string) (Value, error) { + val, err := convertToGo(value, dataType) + return val, err +} + +func (s *ResultSet) CreateExecString(args []Value) (string, error) { + var b bytes.Buffer + b.WriteString(fmt.Sprintf("EXEC %d (", s.Metadata.ExecId)) + + for i, v := range args { + str, err := ConvertToMonet(v) + if err != nil { + return "", nil + } + if i > 0 { + b.WriteString(", ") + } + b.WriteString(str) + } + + b.WriteString(")") + return b.String(), nil +} \ No newline at end of file diff --git a/src/mapi/resultset_test.go b/src/mapi/resultset_test.go new file mode 100644 index 0000000..af2db2b --- /dev/null +++ b/src/mapi/resultset_test.go @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mapi + +import ( + "testing" +) + +func TestResultSet(t *testing.T) { + t.Run("Verify createExecString with empty arg list", func(t *testing.T) { + var r ResultSet + arglist := []Value{} + r.Metadata.ExecId = 1 + val, err := r.CreateExecString(arglist) + if err != nil { + t.Error(err) + } + if val != "EXEC 1 ()" { + t.Error("Function did not return expexted value") + } + }) + +} + +func TestResultSetStoreResult(t *testing.T) { + t.Run("Verify StoreResult with empty result", func(t *testing.T) { + var r ResultSet + var response = "" + err := r.StoreResult(response) + if err != nil { + t.Error(err) + } + }) + + t.Run("Verify StoreResult from create table", func(t *testing.T) { + var r ResultSet + var response = `&5 0 0 6 0 0 0 0 20 +% .prepare, .prepare, .prepare, .prepare, .prepare, .prepare # table_name +% type, digits, scale, schema, table, column # name +% varchar, int, int, varchar, varchar, varchar # type +% 0, 1, 1, 0, 0, 0 # length +% 0 0, 1 0, 1 0, 0 0, 0 0, 0 0 # typesizes + +&3 128 127 + +` + err := r.StoreResult(response) + if err != nil { + t.Error(err) + } + }) + + t.Run("Verify StoreResult from prepare select star", func(t *testing.T) { + var r ResultSet + var response = `&5 2 1 6 1 0 0 0 36 +% .prepare, .prepare, .prepare, .prepare, .prepare, .prepare # table_name +% type, digits, scale, schema, table, column # name +% varchar, int, int, varchar, varchar, varchar # type +% 7, 2, 1, 0, 5, 4 # length +% 7 0, 2 0, 1 0, 0 0, 0 0, 4 0 # typesizes +[ "varchar", 16, 0, "", "test1", "name" ] + +` + err := r.StoreResult(response) + if err != nil { + t.Error(err) + } + //if r.Schema[0].DisplaySize != 5 { + // t.Error("unexpected displaysize") + //} + //if r.Schema[0].InternalSize != 16 { + // t.Error("Unexpected internalsize") + //} + }) + + t.Run("Verify StoreResult from prepare select star", func(t *testing.T) { + var r ResultSet + var response = `&1 2 1 1 1 0 201 169 7 +% sys.test1 # table_name +% name # name +% varchar # type +% 5 # length +% 16 0 # typesizes +[ "name1" ] + +` + err := r.StoreResult(response) + if err != nil { + t.Error(err) + } + if r.Schema[0].DisplaySize != 5 { + t.Error("unexpected displaysize") + } + if r.Schema[0].InternalSize != 16 { + t.Error("Unexpected internalsize") + } + }) + +} diff --git a/src/types.go b/src/mapi/types.go similarity index 98% rename from src/types.go rename to src/mapi/types.go index fbff6b6..5d68678 100644 --- a/src/types.go +++ b/src/mapi/types.go @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package monetdb +package mapi import ( "fmt" diff --git a/src/types_test.go b/src/mapi/types_test.go similarity index 99% rename from src/types_test.go rename to src/mapi/types_test.go index 03ee0d9..c38486b 100644 --- a/src/types_test.go +++ b/src/mapi/types_test.go @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package monetdb +package mapi import ( "testing" diff --git a/src/row_integration_test.go b/src/row_integration_test.go index 5d9a2a3..a29c70e 100644 --- a/src/row_integration_test.go +++ b/src/row_integration_test.go @@ -2,12 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - package monetdb +package monetdb - import ( - "database/sql" - "testing" - ) +import ( + "database/sql" + "testing" +) func TestRowIntegration(t *testing.T) { if testing.Short() { diff --git a/src/rows.go b/src/rows.go index 589773e..0e86690 100644 --- a/src/rows.go +++ b/src/rows.go @@ -8,22 +8,26 @@ import ( "database/sql/driver" "fmt" "io" + "math" + "strings" + "reflect" + "time" + + "github.com/MonetDB/MonetDB-Go/src/mapi" ) type Rows struct { - stmt *Stmt - active bool - - queryId int - - err error + stmt *Stmt + active bool + queryId int + err error rowNum int offset int lastRowId int rowCount int rows [][]driver.Value - description []description + schema []mapi.TableElement columns []string } @@ -40,9 +44,9 @@ func newRows(s *Stmt) *Rows { func (r *Rows) Columns() []string { if r.columns == nil { - r.columns = make([]string, len(r.description)) - for i, d := range r.description { - r.columns[i] = d.columnName + r.columns = make([]string, len(r.schema)) + for i, d := range r.schema { + r.columns[i] = d.ColumnName } } return r.columns @@ -55,10 +59,10 @@ func (r *Rows) Close() error { func (r *Rows) Next(dest []driver.Value) error { if !r.active { - return fmt.Errorf("Rows closed") + return fmt.Errorf("monetdb: rows closed") } if r.queryId == -1 { - return fmt.Errorf("Query didn't result in a resultset") + return fmt.Errorf("monetdb: query didn't result in a resultset") } if r.rowNum >= r.rowCount { @@ -105,15 +109,98 @@ func (r *Rows) fetchNext() error { end := min(r.rowCount, r.rowNum+c_ARRAY_SIZE) amount := end - r.offset - cmd := fmt.Sprintf("Xexport %d %d %d", r.queryId, r.offset, amount) - res, err := r.stmt.conn.cmd(cmd) + res, err := r.stmt.conn.mapi.FetchNext(r.queryId, r.offset, amount) if err != nil { return err } - r.stmt.storeResult(res) - r.rows = r.stmt.rows - r.description = r.stmt.description + r.stmt.resultset.StoreResult(res) + r.rows = r.stmt.copyRows(r.stmt.resultset.Rows) + r.schema = r.stmt.resultset.Schema return nil } + +// See https://pkg.go.dev/database/sql/driver#RowsColumnTypeLength for what to implement +// This implies that we need to return the InternalSize value, not the DisplaySize +func (r *Rows) ColumnTypeLength(index int) (length int64, ok bool) { + switch r.schema[index].ColumnType { + case mapi.MDB_VARCHAR, + mapi.MDB_CHAR : + return int64(r.schema[index].InternalSize), true + case mapi.MDB_BLOB, + mapi.MDB_CLOB : + return math.MaxInt64, true + default: + return 0, false + } +} + +// See https://pkg.go.dev/database/sql/driver#RowsColumnTypeDatabaseTypeName for what to implement +func (r *Rows) ColumnTypeDatabaseTypeName(index int) string { + return strings.ToUpper(r.schema[index].ColumnType) +} + +// For now it seems that the mapi protocol does not provide the required information +func (r *Rows) ColumnTypeNullable(index int) (nullable, ok bool) { + return false, false +} + +// See https://pkg.go.dev/database/sql/driver#RowsColumnTypePrecisionScale for what to implement +func (r *Rows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { + switch r.schema[index].ColumnType { + case mapi.MDB_DECIMAL : + return int64(r.schema[index].Precision), int64(r.schema[index].Scale), true + default: + return 0, 0, false + } +} + +// See https://pkg.go.dev/database/sql/driver#RowsColumnTypeScanType for what to implement +func (r *Rows) ColumnTypeScanType(index int) reflect.Type { + var scantype reflect.Type + + switch r.schema[index].ColumnType { + case mapi.MDB_VARCHAR, + mapi.MDB_CHAR, + mapi.MDB_CLOB, + mapi.MDB_INTERVAL, + mapi.MDB_MONTH_INTERVAL, + mapi.MDB_SEC_INTERVAL : + scantype = reflect.TypeOf("") + case mapi.MDB_NULL : + scantype = reflect.TypeOf(nil) + case mapi.MDB_BLOB : + scantype = reflect.TypeOf([]uint8{0}) + case mapi.MDB_BOOLEAN : + scantype = reflect.TypeOf(true) + case mapi.MDB_REAL, + mapi.MDB_FLOAT : + scantype = reflect.TypeOf(float32(0)) + case mapi.MDB_DECIMAL, + mapi.MDB_DOUBLE : + scantype = reflect.TypeOf(float64(0)) + case mapi.MDB_TINYINT : + scantype = reflect.TypeOf(int8(0)) + case mapi.MDB_SHORTINT, + mapi.MDB_SMALLINT : + scantype = reflect.TypeOf(int16(0)) + case mapi.MDB_INT, + mapi.MDB_MEDIUMINT, + mapi.MDB_WRD : + scantype = reflect.TypeOf(int32(0)) + case mapi.MDB_BIGINT, + mapi.MDB_HUGEINT, + mapi.MDB_SERIAL, + mapi.MDB_LONGINT : + scantype = reflect.TypeOf(int64(0)) + case mapi.MDB_DATE, + mapi.MDB_TIME, + mapi.MDB_TIMESTAMP, + mapi.MDB_TIMESTAMPTZ : + scantype = reflect.TypeOf(time.Time{}) + default: + scantype = reflect.TypeOf(nil) + } + return scantype +} diff --git a/src/rows_integration_test.go b/src/rows_integration_test.go index af5fe26..794e2c9 100644 --- a/src/rows_integration_test.go +++ b/src/rows_integration_test.go @@ -2,12 +2,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - package monetdb +package monetdb - import ( - "database/sql" - "testing" - ) +import ( + "database/sql" + "fmt" + "math" + "testing" +) func TestRowsIntegration(t *testing.T) { if testing.Short() { @@ -144,4 +146,279 @@ func TestRowsIntegration(t *testing.T) { }) defer db.Close() -} \ No newline at end of file +} + + +func TestColumnTypesIntegration(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + db, err := sql.Open("monetdb", "monetdb:monetdb@localhost:50000/monetdb") + if err != nil { + t.Fatal(err) + } + if pingErr := db.Ping(); pingErr != nil { + t.Fatal(pingErr) + } + + type coltypetest struct { + ct string // create table query + it string // insert into query + cs string // select star query + cn []string // column names + lok []bool // length value available + cl []int64 // column lengths + nok []bool // nullable available + ctn []string // column type name + st []string // go type of column + ds []bool // decimal size available + dsp []int64 // decimal precision + dss []int64 // decimal scale + dt string // drop table query + } + + var ctl = []coltypetest{ + { + "create table test1 ( name varchar(16))", + "insert into test1 values ( 'name1' )", + "select * from test1", + []string{"name"}, + []bool{true}, + []int64{16}, + []bool{false}, + []string{"VARCHAR"}, + []string{"string"}, + []bool{false}, + []int64{0, 0}, + []int64{0, 0}, + "drop table test1", + }, + { + "create table test1 ( value int)", + "insert into test1 values ( 25 )", + "select * from test1", + []string{"value"}, + []bool{false}, + []int64{0}, + []bool{false}, + []string{"INT"}, + []string{"int32"}, + []bool{false}, + []int64{0, 0}, + []int64{0, 0}, + "drop table test1", + }, + { + "create table test1 ( name varchar(16), value int)", + "insert into test1 values ( 'name1', 25 )", + "select * from test1", + []string{"name", "value"}, + []bool{true, false}, + []int64{16, 0}, + []bool{false, false}, + []string{"VARCHAR", "INT"}, + []string{"string", "int32"}, + []bool{false, false}, + []int64{0, 0}, + []int64{0, 0}, + "drop table test1", + }, + { + "create table test1 ( name varchar(32), value bigint)", + "insert into test1 values ( 'name1', 25 )", + "select * from test1", + []string{"name", "value"}, + []bool{true, false}, + []int64{32, 0}, + []bool{false, false}, + []string{"VARCHAR", "BIGINT"}, + []string{"string", "int64"}, + []bool{false, false}, + []int64{0, 0}, + []int64{0, 0}, + "drop table test1", + }, + { + "create table test1 ( name blob, value boolean )", + "insert into test1 values ( x'1a2b3c4d5e', true )", + "select * from test1", + []string{"name", "value"}, + []bool{true, false}, + []int64{math.MaxInt64, 0}, + []bool{false, false}, + []string{"BLOB", "BOOLEAN"}, + []string{"[]uint8", "bool"}, + []bool{false, false}, + []int64{0, 0}, + []int64{0, 0}, + "drop table test1", + }, + { + "create table test1 ( name real, value boolean)", + "insert into test1 values ( 1.2345, true )", + "select * from test1", + []string{"name", "value"}, + []bool{false, false}, + []int64{0, 0}, + []bool{false, false}, + []string{"REAL", "BOOLEAN"}, + []string{"float32", "bool"}, + []bool{false, false}, + []int64{0, 0}, + []int64{0, 0}, + "drop table test1", + }, + { + "create table test1 ( name smallint, value double)", + "insert into test1 values ( 12, 1.2345 )", + "select * from test1", + []string{"name", "value"}, + []bool{false, false}, + []int64{0, 0}, + []bool{false, false}, + []string{"SMALLINT", "DOUBLE"}, + []string{"int16", "float64"}, + []bool{false, false}, + []int64{0, 0}, + []int64{0, 0}, + "drop table test1", + }, + { + "create table test1 ( name decimal, value decimal(10, 5))", + "insert into test1 values ( 1.2345, 67.890 )", + "select * from test1", + []string{"name", "value"}, + []bool{false, false}, + []int64{0, 0}, + []bool{false, false}, + []string{"DECIMAL", "DECIMAL"}, + []string{"float64", "float64"}, + []bool{true, true}, + []int64{18, 10}, + []int64{3, 5}, + "drop table test1", + }, + { + "create table test1 ( name timestamptz)", + "insert into test1 values ( current_timestamp() )", + "select * from test1", + []string{"name"}, + []bool{false}, + []int64{0}, + []bool{false}, + []string{"TIMESTAMPTZ"}, + []string{"Time"}, + []bool{false}, + []int64{0}, + []int64{0}, + "drop table test1", + }, + } + + for i := range ctl { + t.Run("Exec create table", func(t *testing.T) { + _, err := db.Exec(ctl[i].ct) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("Exec insert row", func(t *testing.T) { + _, err := db.Exec(ctl[i].it) + if err != nil { + t.Fatal(err) + } + }) + + t.Run("Get Columns", func(t *testing.T) { + rows, err := db.Query(ctl[i].cs) + if err != nil { + t.Fatal(err) + } + if rows == nil { + t.Fatal("empty result") + } + columnlist, err := rows.Columns() + if err != nil { + t.Error(err) + } + for j, column := range columnlist { + if column != ctl[i].cn[j]{ + t.Errorf("unexpected column name in Columns: %s", column) + } + } + columntypes, err := rows.ColumnTypes() + if err != nil { + t.Error(err) + } + for j, column := range columntypes { + if column.Name() != ctl[i].cn[j] { + t.Errorf("unexpected column name in ColumnTypes") + } + length, length_ok := column.Length() + if length_ok != ctl[i].lok[j] { + t.Errorf("unexpected value for length_ok") + } else { + if length_ok { + if length != ctl[i].cl[j] { + t.Errorf("unexpected column length in ColumnTypes") + } + } + } + _, nullable_ok := column.Nullable() + if nullable_ok != ctl[i].nok[j]{ + t.Errorf("not expected that nullable was provided") + } + coltype := column.DatabaseTypeName() + if coltype != ctl[i].ctn[j] { + t.Errorf("unexpected column typename") + } + scantype := column.ScanType() + // Not every type has a name. Then the name is the empty string. In that case, compare the types + if scantype.Name() != "" { + if scantype.Name() != ctl[i].st[j] { + t.Errorf("unexpected scan type: %s instead of %s", ctl[i].st[j], scantype.Name()) + } + } else { + if fmt.Sprintf("%v", scantype) != ctl[i].st[j] { + t.Errorf("unexpected scan type: %s instead of %v", ctl[i].st[j], scantype) + } + } + precision, scale, ok := column.DecimalSize() + if ok != ctl[i].ds[j]{ + t.Errorf("not expected that decimal size was provided") + } else { + if ok { + if precision != ctl[i].dsp[j] { + t.Errorf("Unexpected value for precision") + } + if scale != ctl[i].dss[j] { + t.Errorf("unexpected value for scale") + } + } + } + } + /* + for rows.Next() { + name := make([]driver.Value, colcount) + if err := rows.Scan(&name); err != nil { + t.Error(err) + } + } + if err := rows.Err(); err != nil { + t.Error(err) + } + */ + defer rows.Close() + }) + + t.Run("Exec drop table", func(t *testing.T) { + _, err := db.Exec(ctl[i].dt) + if err != nil { + t.Fatal(err) + } + }) + } + defer db.Close() +} diff --git a/src/stmt.go b/src/stmt.go index d2b7837..edd424d 100644 --- a/src/stmt.go +++ b/src/stmt.go @@ -5,45 +5,24 @@ package monetdb import ( - "bytes" "database/sql/driver" "fmt" - "strconv" - "strings" + + "github.com/MonetDB/MonetDB-Go/src/mapi" ) type Stmt struct { conn *Conn query string - - execId int - - lastRowId int - rowCount int - queryId int - offset int - columnCount int - - rows [][]driver.Value - description []description -} - -type description struct { - columnName string - columnType string - displaySize int - internalSize int - precision int - scale int - nullOk int + resultset mapi.ResultSet } func newStmt(c *Conn, q string) *Stmt { s := &Stmt{ conn: c, query: q, - execId: -1, } + s.resultset.Metadata.ExecId = -1 return s } @@ -65,14 +44,33 @@ func (s *Stmt) Exec(args []driver.Value) (driver.Result, error) { return res, res.err } - err = s.storeResult(r) - res.lastInsertId = s.lastRowId - res.rowsAffected = s.rowCount + err = s.resultset.StoreResult(r) + res.lastInsertId = s.resultset.Metadata.LastRowId + res.rowsAffected = s.resultset.Metadata.RowCount res.err = err return res, res.err } +func (s *Stmt) copyRows(rowwlist [][]mapi.Value)([][]driver.Value) { + res := make([][]driver.Value, s.resultset.Metadata.RowCount) + for i, row := range rowwlist { + res[i] = make([]driver.Value, s.resultset.Metadata.ColumnCount) + for j, col := range row { + res[i][j] = col + } + } + return res +} + +func (s *Stmt) copyArgs(arglist []driver.Value)([]mapi.Value) { + res := make([]mapi.Value, len(arglist)) + for i, arg := range arglist { + res[i] = arg + } + return res +} + func (s *Stmt) Query(args []driver.Value) (driver.Rows, error) { rows := newRows(s) @@ -82,42 +80,35 @@ func (s *Stmt) Query(args []driver.Value) (driver.Rows, error) { return rows, rows.err } - //lint:ignore SA4006 prepare to enable staticchecks - err = s.storeResult(r) - rows.queryId = s.queryId - rows.lastRowId = s.lastRowId - rows.rowCount = s.rowCount - rows.offset = s.offset - rows.rows = s.rows - rows.description = s.description + err = s.resultset.StoreResult(r) + if err != nil { + rows.err = err + return rows, rows.err + } + rows.queryId = s.resultset.Metadata.QueryId + rows.lastRowId = s.resultset.Metadata.LastRowId + rows.rowCount = s.resultset.Metadata.RowCount + rows.offset = s.resultset.Metadata.Offset + rows.rows = s.copyRows(s.resultset.Rows) + rows.schema = s.resultset.Schema return rows, rows.err } func (s *Stmt) exec(args []driver.Value) (string, error) { - if s.execId == -1 { + if s.resultset.Metadata.ExecId == -1 { err := s.prepareQuery() if err != nil { return "", err } } - var b bytes.Buffer - b.WriteString(fmt.Sprintf("EXEC %d (", s.execId)) - - for i, v := range args { - str, err := convertToMonet(v) - if err != nil { - return "", nil - } - if i > 0 { - b.WriteString(", ") - } - b.WriteString(str) - } - - b.WriteString(")") - return s.conn.execute(b.String()) + arglist := s.copyArgs(args) + execStr, err := s.resultset.CreateExecString(arglist) + if err != nil { + return "", err + } + return s.conn.execute(execStr) } func (s *Stmt) prepareQuery() error { @@ -127,165 +118,5 @@ func (s *Stmt) prepareQuery() error { return err } - return s.storeResult(r) -} - -func (s *Stmt) storeResult(r string) error { - var columnNames []string - var columnTypes []string - var displaySizes []int - var internalSizes []int - var precisions []int - var scales []int - var nullOks []int - - for _, line := range strings.Split(r, "\n") { - if strings.HasPrefix(line, mapi_MSG_INFO) { - // TODO log - - } else if strings.HasPrefix(line, mapi_MSG_QPREPARE) { - t := strings.Split(strings.TrimSpace(line[2:]), " ") - s.execId, _ = strconv.Atoi(t[0]) - return nil - - } else if strings.HasPrefix(line, mapi_MSG_QTABLE) { - t := strings.Split(strings.TrimSpace(line[2:]), " ") - s.queryId, _ = strconv.Atoi(t[0]) - s.rowCount, _ = strconv.Atoi(t[1]) - s.columnCount, _ = strconv.Atoi(t[2]) - - columnNames = make([]string, s.columnCount) - columnTypes = make([]string, s.columnCount) - displaySizes = make([]int, s.columnCount) - internalSizes = make([]int, s.columnCount) - precisions = make([]int, s.columnCount) - scales = make([]int, s.columnCount) - nullOks = make([]int, s.columnCount) - - } else if strings.HasPrefix(line, mapi_MSG_TUPLE) { - v, err := s.parseTuple(line) - if err != nil { - return err - } - s.rows = append(s.rows, v) - - } else if strings.HasPrefix(line, mapi_MSG_QBLOCK) { - s.rows = make([][]driver.Value, 0) - - } else if strings.HasPrefix(line, mapi_MSG_QSCHEMA) { - s.offset = 0 - s.rows = make([][]driver.Value, 0) - s.lastRowId = 0 - s.description = nil - s.rowCount = 0 - - } else if strings.HasPrefix(line, mapi_MSG_QUPDATE) { - t := strings.Split(strings.TrimSpace(line[2:]), " ") - s.rowCount, _ = strconv.Atoi(t[0]) - s.lastRowId, _ = strconv.Atoi(t[1]) - - } else if strings.HasPrefix(line, mapi_MSG_QTRANS) { - s.offset = 0 - //lint:ignore S1019 prepare to enable staticchecks - s.rows = make([][]driver.Value, 0, 0) - s.lastRowId = 0 - s.description = nil - s.rowCount = 0 - - } else if strings.HasPrefix(line, mapi_MSG_HEADER) { - t := strings.Split(line[1:], "#") - data := strings.TrimSpace(t[0]) - identity := strings.TrimSpace(t[1]) - - values := make([]string, 0) - for _, value := range strings.Split(data, ",") { - values = append(values, strings.TrimSpace(value)) - } - - if identity == "name" { - columnNames = values - - } else if identity == "type" { - columnTypes = values - - } else if identity == "typesizes" { - sizes := make([][]int, len(values)) - for i, value := range values { - s := make([]int, 0) - for _, v := range strings.Split(value, " ") { - val, _ := strconv.Atoi(v) - s = append(s, val) - } - internalSizes[i] = s[0] - sizes = append(sizes, s) - } - for j, t := range columnTypes { - if t == "decimal" { - precisions[j] = sizes[j][0] - scales[j] = sizes[j][1] - } - } - } - - s.updateDescription(columnNames, columnTypes, displaySizes, - internalSizes, precisions, scales, nullOks) - s.offset = 0 - s.lastRowId = 0 - - } else if strings.HasPrefix(line, mapi_MSG_PROMPT) { - return nil - - } else if strings.HasPrefix(line, mapi_MSG_ERROR) { - //lint:ignore ST1005 prepare to enable staticchecks - return fmt.Errorf("Database error: %s", line[1:]) - } - } - - //lint:ignore ST1005 prepare to enable staticchecks - return fmt.Errorf("Unknown state: %s", r) -} - -func (s *Stmt) parseTuple(d string) ([]driver.Value, error) { - items := strings.Split(d[1:len(d)-1], ",\t") - if len(items) != len(s.description) { - //lint:ignore ST1005 prepare to enable staticchecks - return nil, fmt.Errorf("Length of row doesn't match header") - } - - v := make([]driver.Value, len(items)) - for i, value := range items { - vv, err := s.convert(value, s.description[i].columnType) - if err != nil { - return nil, err - } - v[i] = vv - } - return v, nil -} - -func (s *Stmt) updateDescription( - columnNames, columnTypes []string, displaySizes, - internalSizes, precisions, scales, nullOks []int) { - - d := make([]description, len(columnNames)) - //lint:ignore S1005 prepare to enable staticchecks - for i, _ := range columnNames { - desc := description{ - columnName: columnNames[i], - columnType: columnTypes[i], - displaySize: displaySizes[i], - internalSize: internalSizes[i], - precision: precisions[i], - scale: scales[i], - nullOk: nullOks[i], - } - d[i] = desc - } - - s.description = d -} - -func (s *Stmt) convert(value, dataType string) (driver.Value, error) { - val, err := convertToGo(value, dataType) - return val, err + return s.resultset.StoreResult(r) }