Previously in database basics:
1. SELECT, INSERT, CREATE and a REPL
2. binary expressions and WHERE filters
3. indexes
In this post, we'll extend gosql
to implement the database/sql driver interface. This will
allow us to interact with gosql the same way we would interact with
any other database.
Here is an example familiar program (stored in
cmd/sqlexample/main.go) we'll be able to run:
package main
import (
"database/sql"
"fmt"
_ "github.com/eatonphil/gosql"
)
func main() {
db, err := sql.Open("postgres", "")
if err != nil {
panic(err)
}
defer db.Close()
_, err = db.Query("CREATE TABLE users (name TEXT, age INT);")
if err != nil {
panic(err)
}
_, err = db.Query("INSERT INTO users VALUES ('Terry', 45);")
if err != nil {
panic(err)
}
_, err = db.Query("INSERT INTO users VALUES ('Anette', 57);")
if err != nil {
panic(err)
}
rows, err := db.Query("SELECT name, age FROM users;")
if err != nil {
panic(err)
}
var name string
var age uint64
defer rows.Close()
for rows.Next() {
err := rows.Scan(&name, &age)
if err != nil {
panic(err)
}
fmt.Printf("Name: %s, Age: %d\n", name, age)
}
if err = rows.Err(); err != nil {
panic(err)
}
}
Our gosql driver will use a single instance of the
Backend for all connections.
Aside from that, it is a simple matter of wrapping our existing APIs
in structs that implement the database/sql/driver.Driver
interface.
This post is largely a discussion of this commit.
Implementing the driver
A driver is registered by calling sql.Register with a
driver instance.
We'll add the registration code to an init function in a
new file, driver.go:
struct Driver {
bkd Backend
}
func init() {
sql.Register("postgres", &Driver{NewMemoryBackend()})
}
According to the Driver
interface, we
need only implement Open to return an connection instance
that implements the database/sql/driver.Conn interface.
type Driver struct {
bkd Backend
}
func (d *Driver) Open(name string) (driver.Conn, error) {
return &Conn{d.bkd}, nil
}
func init() {
sql.Register("postgres", &Driver{NewMemoryBackend()})
}
Implementing the connection
According to the Conn interface, we must implement:
Prepare(query string) (driver.Stmt, error)to handle prepared statementsCloseto handle cleanup- and
Beginto start a transaction
The connection can also optionally implement Query and
Exec.
To simplify things we'll panic on Prepare and on
Begin (we don't have transactions yet). There's no
cleanup required so we'll do nothing in Close.
type Conn struct {
bkd Backend
}
func (dc *Conn) Prepare(query string) (driver.Stmt, error) {
panic("Prepare not implemented")
}
func (dc *Conn) Begin() (driver.Tx, error) {
panic("Begin not implemented")
}
func (dc *Conn) Close() error {
return nil
}
The only method we actually need, Query, is not required
by the interface. It takes a query string and array of query
parameters, returning an instance implementing
the database/sql/driver.Rows interface.
To implement Query, we basically copy the logic we had in
the cmd/main.go REPL. The only change is that when we
return results when handling SELECT, we'll return a
struct that implements the database/sql/driver.Rows
interface.
database/sql/driver.Rows is not the same type as
database/sql.Rows, which may sound more
familiar. database/sql/driver.Rows is a simpler,
lower-level interface.
If we receive parameterized query arguments, we'll ignore them for now. And if the query involves multiple statements, we'll process only the first statement.
func (dc *Conn) Query(query string, args []driver.Value) (driver.Rows, error) {
if len(args) > 0 {
// TODO: support parameterization
panic("Parameterization not supported")
}
parser := Parser{}
ast, err := parser.Parse(query)
if err != nil {
return nil, fmt.Errorf("Error while parsing: %s", err)
}
// NOTE: ignorning all but the first statement
stmt := ast.Statements[0]
switch stmt.Kind {
case CreateIndexKind:
err = dc.bkd.CreateIndex(stmt.CreateIndexStatement)
if err != nil {
return nil, fmt.Errorf("Error adding index on table: %s", err)
}
case CreateTableKind:
err = dc.bkd.CreateTable(stmt.CreateTableStatement)
if err != nil {
return nil, fmt.Errorf("Error creating table: %s", err)
}
case DropTableKind:
err = dc.bkd.DropTable(stmt.DropTableStatement)
if err != nil {
return nil, fmt.Errorf("Error dropping table: %s", err)
}
case InsertKind:
err = dc.bkd.Insert(stmt.InsertStatement)
if err != nil {
return nil, fmt.Errorf("Error inserting values: %s", err)
}
case SelectKind:
results, err := dc.bkd.Select(stmt.SelectStatement)
if err != nil {
return nil, err
}
return &Rows{
rows: results.Rows,
columns: results.Columns,
index: 0,
}, nil
}
return nil, nil
}
Implementing results
According to the Rows interface we must implement:
Columns() []stringto return an array of columns namesNext(dest []Value) errorto populate an row array with the next row's worth of cells- and
Close() error
Our Rows struct will contain the rows and colums as
returned from Backend, and will also contain an
index field we can use in Next to populate
the next row of cells.
type Rows struct {
columns []ResultColumn
index uint64
rows [][]Cell
}
func (r *Rows) Columns() []string {}
func (r *Rows) Close() error {}
func (r *Rows) Next(dest []driver.Value) error {}
For Columns we simply need to extract and
return the column names from ResultColumn.
func (r *Rows) Columns() []string {
columns := []string{}
for _, c := range r.columns {
columns = append(columns, c.Name)
}
return columns
}
For Next we need to iterate over each cell in the current
row and retrieve its Go value, storing it in dest. The
dest argument is simply a fixed-length array of
interface{}, so we'll need no manual conversion.
Once we've reached the last row, the Next contract is to
return an io.EOF.
func (r *Rows) Next(dest []driver.Value) error {
if r.index >= uint64(len(r.rows)) {
return io.EOF
}
row := r.rows[r.index]
for idx, cell := range row {
typ := r.columns[idx].Type
switch typ {
case IntType:
i := cell.AsInt()
if i == nil {
dest[idx] = i
} else {
dest[idx] = *i
}
case TextType:
s := cell.AsText()
if s == nil {
dest[idx] = s
} else {
dest[idx] = *s
}
case BoolType:
b := cell.AsBool()
if b == nil {
dest[idx] = b
} else {
dest[idx] = b
}
}
}
r.index++
return nil
}
Finally in Close we'll set index higher than
the number of rows to force Next to only ever
return io.EOF.
func (r *Rows) Close() error {
r.index = uint64(len(r.rows))
return nil
}
And that's all the changes needed to implement a
database/sql driver! See
here
for driver.go in full.
Running the example
With the driver in place we can try out the example:
$ go build ./cmd/sqlexample/main.go
$ ./main
Name: Terry, Age: 45
Name: Anette, Age: 57
Next post in the database basics series, implementing a database/sql driver for more seamless interactions in Go.https://t.co/AUZfUByNGE
— Phil Eaton (@phil_eaton) May 10, 2020