Skip to content

Commit

Permalink
Implement Columntype interfaces (#14)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
arjenderijke authored Jan 3, 2024
1 parent 3af6987 commit 3b9d9d8
Show file tree
Hide file tree
Showing 17 changed files with 1,245 additions and 661 deletions.
50 changes: 50 additions & 0 deletions implementation.MD
Original file line number Diff line number Diff line change
@@ -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)
29 changes: 13 additions & 16 deletions src/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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)
}
112 changes: 0 additions & 112 deletions src/converter_test.go

This file was deleted.

153 changes: 1 addition & 152 deletions src/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ package monetdb
import (
"database/sql"
"database/sql/driver"
"fmt"
"regexp"
"strconv"
"strings"
)

func init() {
Expand All @@ -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<username>[^:]+?)(:(?P<password>[^@]+?))?@)?\[(?P<hostname>(([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<port>\d+?))?\/(?P<database>.+?)$`)

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
}
Loading

0 comments on commit 3b9d9d8

Please sign in to comment.