Repository: genjidb/genji
Pull request #213 (2020-10-01 18:18:23)

Implement driver.DriverContext and driver.Connector interfaces in sql/driver
This PR partially addresses issue #210 by implementing the approach that relies either on Go support (see golang/go#41710) or runtime.SetFinalizer to close the underlying database. Note that the latter is non-deterministic and we can’t handle an error.



Patch:
diff --git a/cmd/genji/shell/shell.go b/cmd/genji/shell/shell.go
index becd7338..d9d07adb 100644
--- a/cmd/genji/shell/shell.go
+++ b/cmd/genji/shell/shell.go
@@ -116,13 +116,40 @@ func Run(opts *Options) error {
 		return nil
 	}
 
-	e := prompt.New(
-		sh.execute,
-		sh.completer,
+	promptOpts := []prompt.Option{
 		prompt.OptionPrefix("genji> "),
 		prompt.OptionTitle("genji"),
 		prompt.OptionLivePrefix(sh.changelivePrefix),
 		prompt.OptionHistory(history),
+	}
+
+	// If NO_COLOR env var is present, disable color. See https://no-color.org
+	if _, ok := os.LookupEnv("NO_COLOR"); ok {
+		// A list of color options we have to reset.
+		colorOpts := []func(prompt.Color) prompt.Option{
+			prompt.OptionPrefixTextColor,
+			prompt.OptionPreviewSuggestionTextColor,
+			prompt.OptionSuggestionTextColor,
+			prompt.OptionSuggestionBGColor,
+			prompt.OptionSelectedSuggestionTextColor,
+			prompt.OptionSelectedSuggestionBGColor,
+			prompt.OptionDescriptionTextColor,
+			prompt.OptionDescriptionBGColor,
+			prompt.OptionSelectedDescriptionTextColor,
+			prompt.OptionSelectedDescriptionBGColor,
+			prompt.OptionScrollbarThumbColor,
+			prompt.OptionScrollbarBGColor,
+		}
+		for _, opt := range colorOpts {
+			resetColor := opt(prompt.DefaultColor)
+			promptOpts = append(promptOpts, resetColor)
+		}
+	}
+
+	e := prompt.New(
+		sh.execute,
+		sh.completer,
+		promptOpts...,
 	)
 
 	e.Run()
@@ -146,6 +173,9 @@ func (sh *Shell) loadCommandSuggestions() {
 }
 
 func (sh *Shell) loadHistory() ([]string, error) {
+	if _, ok := os.LookupEnv("NO_HISTORY"); ok {
+		return nil, nil
+	}
 	homeDir, err := os.UserHomeDir()
 	if err != nil {
 		return nil, err
@@ -174,6 +204,9 @@ func (sh *Shell) loadHistory() ([]string, error) {
 }
 
 func (sh *Shell) dumpHistory() error {
+	if _, ok := os.LookupEnv("NO_HISTORY"); ok {
+		return nil
+	}
 	homeDir, err := os.UserHomeDir()
 	if err != nil {
 		return err
diff --git a/document/scan.go b/document/scan.go
index bdfc9cd8..42392f1d 100644
--- a/document/scan.go
+++ b/document/scan.go
@@ -244,6 +244,12 @@ func scanValue(v Value, ref reflect.Value) error {
 		ref = reflect.Indirect(ref)
 	}
 
+	// Scan nulls as Go zero values.
+	if v.Type == NullValue {
+		ref.Set(reflect.Zero(ref.Type()))
+		return nil
+	}
+
 	switch ref.Kind() {
 	case reflect.String:
 		v, err := v.CastAsText()
diff --git a/sql/driver/driver.go b/sql/driver/driver.go
index 26af9fb2..d7d9cb6c 100644
--- a/sql/driver/driver.go
+++ b/sql/driver/driver.go
@@ -6,6 +6,7 @@ import (
 	"database/sql/driver"
 	"errors"
 	"io"
+	"runtime"
 	"sync"
 
 	"github.com/genjidb/genji"
@@ -20,58 +21,68 @@ func init() {
 	sql.Register("genji", sqlDriver{})
 }
 
+var (
+	_ driver.Driver        = (*sqlDriver)(nil)
+	_ driver.DriverContext = (*sqlDriver)(nil)
+)
+
 // sqlDriver is a driver.Driver that can open a new connection to a Genji database.
 // It is the driver used to register Genji against the database/sql package.
 type sqlDriver struct{}
 
 func (d sqlDriver) Open(name string) (driver.Conn, error) {
+	return nil, errors.New("requires go1.10 or greater")
+}
+
+func (d sqlDriver) OpenConnector(name string) (driver.Connector, error) {
 	db, err := genji.Open(name)
 	if err != nil {
 		return nil, err
 	}
 
-	return &conn{db: db}, nil
-}
-
-// proxyDriver is used to turn an existing DB into a driver.Driver.
-type proxyDriver struct {
-	db *genji.DB
-}
-
-func newDriver(db *genji.DB) driver.Driver {
-	return proxyDriver{
-		db: db,
+	c := &connector{
+		db:     db,
+		driver: d,
 	}
-}
+	runtime.SetFinalizer(c, (*connector).Close)
 
-func (d proxyDriver) Open(name string) (driver.Conn, error) {
-	return &conn{db: d.db}, nil
+	return c, nil
 }
 
-type proxyConnector struct {
+var (
+	_ driver.Connector = (*connector)(nil)
+	_ io.Closer        = (*connector)(nil)
+)
+
+type connector struct {
 	driver driver.Driver
-}
 
-func newProxyConnector(db *genji.DB) driver.Connector {
-	return proxyConnector{
-		driver: newDriver(db),
-	}
+	db *genji.DB
+
+	closeOnce sync.Once
 }
 
-func (c proxyConnector) Connect(ctx context.Context) (driver.Conn, error) {
-	return c.driver.Open("")
+func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
+	return &conn{db: c.db}, nil
 }
 
-func (c proxyConnector) Driver() driver.Driver {
+func (c *connector) Driver() driver.Driver {
 	return c.driver
 }
 
+func (c *connector) Close() error {
+	var err error
+	c.closeOnce.Do(func() {
+		err = c.db.Close()
+	})
+	return err
+}
+
 // conn represents a connection to the Genji database.
 // It implements the database/sql/driver.Conn interface.
 type conn struct {
-	db            *genji.DB
-	tx            *genji.Tx
-	nonPromotable bool
+	db *genji.DB
+	tx *genji.Tx
 }
 
 // Prepare returns a prepared statement, bound to this connection.


Test patch:
diff --git a/document/scan_test.go b/document/scan_test.go
index a137b486..3c5a3998 100644
--- a/document/scan_test.go
+++ b/document/scan_test.go
@@ -65,6 +65,7 @@ func TestScan(t *testing.T) {
 				Add("foo", document.NewTextValue("foo")).
 				Add("bar", document.NewTextValue("bar")),
 		)).
+		Add("o", document.NewNullValue()).
 		Add("p", document.NewTextValue(now.Format(time.RFC3339Nano))).
 		Add("r", document.NewDocumentValue(codec.NewDocument(buf.Bytes())))
 
@@ -88,10 +89,11 @@ func TestScan(t *testing.T) {
 	var l *foo = new(foo)
 	var m *foo
 	var n map[string]string
+	var o []int = []int{1, 2, 3}
 	var p time.Time
 	var r map[string]interface{}
 
-	err = document.Scan(doc, &a, &b, &c, &d, &e, &f, &g, &h, &i, &j, &k, &l, &m, &n, &p, &r)
+	err = document.Scan(doc, &a, &b, &c, &d, &e, &f, &g, &h, &i, &j, &k, &l, &m, &n, &o, &p, &r)
 	require.NoError(t, err)
 	require.Equal(t, a, []byte("foo"))
 	require.Equal(t, b, "bar")
@@ -108,6 +110,7 @@ func TestScan(t *testing.T) {
 	require.Equal(t, &foo{Foo: "foo", Pub: &bar}, l)
 	require.Equal(t, &foo{Foo: "foo", Pub: &bar}, m)
 	require.Equal(t, map[string]string{"foo": "foo", "bar": "bar"}, n)
+	require.Equal(t, []int(nil), o)
 	require.Equal(t, now.Format(time.RFC3339Nano), p.Format(time.RFC3339Nano))
 	require.Equal(t, map[string]interface{}{
 		"foo": map[string]interface{}{
@@ -133,14 +136,14 @@ func TestScan(t *testing.T) {
 		m := make(map[string]interface{})
 		err := document.MapScan(doc, m)
 		require.NoError(t, err)
-		require.Len(t, m, 16)
+		require.Len(t, m, 17)
 	})
 
 	t.Run("MapPtr", func(t *testing.T) {
 		var m map[string]interface{}
 		err := document.MapScan(doc, &m)
 		require.NoError(t, err)
-		require.Len(t, m, 16)
+		require.Len(t, m, 17)
 	})
 
 	t.Run("Small Slice", func(t *testing.T) {