Repository: tomarrell/lbadd
Pull request #67 (2020-03-03 15:46:17)

sql driver
Implement the skeleton of a driver. Database and db server has to be functional to continue testable implementation.


Patch:
diff --git a/cmd/repl/main.go b/cmd/repl/main.go
index e7c1fdd..85acb9f 100644
--- a/cmd/repl/main.go
+++ b/cmd/repl/main.go
@@ -7,13 +7,14 @@ import (
 	"io"
 	"os"
 	"os/signal"
+	"strconv"
 	"syscall"
-	"time"
 
 	"github.com/rs/zerolog"
 	"github.com/rs/zerolog/diode"
 	"github.com/tomarrell/lbadd/internal/cli"
 	"github.com/tomarrell/lbadd/internal/executor"
+	"github.com/tomarrell/lbadd/internal/server"
 )
 
 const (
@@ -36,17 +37,76 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
 	// setup flags
 	flags := flag.NewFlagSet(args[0], flag.ExitOnError)
 	var (
-		verbose = flags.Bool("verbose", false, "enable verbose output")
-		logfile = flags.String("logfile", "lbadd.cli.log", "define a log file to write messages to")
+		verbose   = flags.Bool("verbose", false, "enable verbose output")
+		logfile   = flags.String("logfile", "lbadd.log", "define a log file to write messages to")
+		port      = flags.Int("port", 34213, "publish the database server on this port")
+		host      = flags.String("host", "", "publish the database server on this host")
+		headless  = flags.Bool("headless", false, "don't use a cli, only start the server")
+		noConsole = flags.Bool("quiet", false, "don't print logs to stdout")
 	)
 
 	if err := flags.Parse(args[1:]); err != nil {
 		return fmt.Errorf("parse flags: %w", err)
 	}
 
+	// open the log file
+	file, err := os.OpenFile(*logfile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
+	if err != nil {
+		return fmt.Errorf("open logfile: %w", err)
+	}
+	defer func() {
+		_ = file.Close()
+	}()
+
 	// programCtx is the context, that all components should run on. When
 	// invoking cancel, all started components should stop processing.
 	programCtx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	// initialize all writers
+	writers := []io.Writer{
+		// performant file writer
+		diode.NewWriter(
+			file, // output writer
+			1000, // pool size
+			0,    // poll interval, use a waiter
+			func(missed int) {
+				_, _ = fmt.Fprintf(stderr, "Logger is falling behind, skipping %d messages\n", missed)
+			},
+		),
+	}
+	if !*noConsole {
+		writers = append(writers,
+			// unperformant console writer
+			zerolog.ConsoleWriter{
+				Out: stdout,
+			},
+		)
+	}
+
+	// initialize the root logger
+	log := zerolog.New(io.MultiWriter(writers...)).With().
+		Timestamp().
+		Logger().
+		Level(zerolog.InfoLevel)
+
+	log.Info().
+		Bool("headless", *headless).
+		Msg("start new application session")
+
+	// apply the verbose flag
+	if *verbose {
+		log = log.Level(zerolog.TraceLevel)
+	}
+
+	// print all flags on debug level
+	log.Debug().
+		Bool("verbose", *verbose).
+		Str("logfile", *logfile).
+		Int("publish", *port).
+		Bool("headless", *headless).
+		Bool("quiet", *noConsole).
+		Msg("settings")
 
 	// start listening for signals
 	signalChan := make(chan os.Signal, 1)
@@ -57,45 +117,48 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
 	}()
 	go func() {
 		select {
-		case <-signalChan: // first signal, cancel context
+		case sig := <-signalChan: // first signal, cancel context
+			log.Info().
+				Str("signal", sig.String()).
+				Msg("Attempting graceful shutdown, press again to force quit")
 			cancel()
-			_, _ = fmt.Fprintln(stdout, "Attempting graceful shutdown, press again to force quit")
 		case <-programCtx.Done():
 		}
 		<-signalChan // second signal, hard exit
+		log.Info().
+			Msg("Forcing shutdown")
 		os.Exit(ExitInterrupt)
 	}()
 
-	// Initialize a root logger
-	file, err := os.OpenFile(*logfile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600)
-	if err != nil {
-		return fmt.Errorf("open logfile: %w", err)
+	// setup the executor
+	executor := executor.New(log.With().Str("component", "executor").Logger())
+
+	// setup server endpoint
+	server := server.New(log.With().Str("component", "server").Logger(), executor)
+	runServer := func() {
+		if err := server.ListenAndServe(programCtx, *host+":"+strconv.Itoa(int(*port))); err != nil {
+			log.Error().
+				Err(err).
+				Msg("listen and serve failed")
+		}
 	}
 
-	log := zerolog.New(
-		diode.NewWriter(
-			file,                // output writer
-			1000,                // pool size
-			10*time.Millisecond, // poll interval
-			func(missed int) {
-				_, _ = fmt.Fprintf(stderr, "Logger is falling behind, skipping %d messages\n", missed)
-			},
-		),
-	).With().
-		Timestamp().
-		Logger().
-		Level(zerolog.InfoLevel)
+	/*
+		Handle headless.
 
-	log.Info().Msg("start new session")
+		If headless, start and wait for the server, otherwise start the server
+		in the background and start the cli. stdin will not be used if
+		headless=true.
+	*/
+	if *headless {
+		runServer()
+	} else {
+		go runServer()
 
-	// apply the verbose flag
-	if *verbose {
-		log = log.Level(zerolog.TraceLevel)
+		// run the cli
+		cli := cli.New(programCtx, stdin, stdout, executor)
+		cli.Start()
 	}
 
-	// run the cli
-	cli := cli.New(programCtx, stdin, stdout, executor.New(log.With().Str("component", "executor").Logger()))
-	cli.Start()
-
 	return nil
 }
diff --git a/driver/conn.go b/driver/conn.go
new file mode 100644
index 0000000..3f857ec
--- /dev/null
+++ b/driver/conn.go
@@ -0,0 +1,89 @@
+package driver
+
+import (
+	"context"
+	"database/sql/driver"
+	"fmt"
+)
+
+var _ driver.Conn = (*Conn)(nil)
+var _ driver.ConnBeginTx = (*Conn)(nil)
+var _ driver.ExecerContext = (*Conn)(nil)
+var _ driver.Pinger = (*Conn)(nil)
+var _ driver.QueryerContext = (*Conn)(nil)
+
+// Conn represents a connection to the database. It can be used to prepare and
+// execute statements.
+type Conn struct {
+}
+
+// Prepare prepares a statement. The returned Stmt is an SQL prepared statement,
+// that has placeholders for parameters that need to be set. Do NOT set
+// parameters directly when creating the statement with this method.
+//
+//  stmt, err := conn.Prepare(`INSERT INTO users VALUES ("jdoe")`) // WRONG
+//
+//  stmt, err := conn.Prepare(`INSERT INTO users VALUES (?)`) // CORRECT
+//  result, err := stmt.Exec("jdoe")
+func (c *Conn) Prepare(query string) (driver.Stmt, error) {
+	stmt, err := parse(query)
+	if err != nil {
+		return nil, fmt.Errorf("parse: %w", err)
+	}
+	return stmt, nil
+}
+
+// Close closes this connection. If a connection is closed, it cannot be used as
+// idle connection in the connection pool and a new connection needs to be
+// established.
+func (c *Conn) Close() error {
+	return nil // TODO(TimSatke): implement
+}
+
+// Begin is deprecated. Use BeginTx instead.
+func (c *Conn) Begin() (driver.Tx, error) {
+	return c.BeginTx(context.Background(), driver.TxOptions{})
+}
+
+// BeginTx creates a transaction that can be either committed or rolled back.
+func (c *Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
+	return nil, fmt.Errorf("unimplemented") // TODO(TimSatke): implement
+}
+
+// ExecContext executes the given query with the given arguments under the given
+// context and returns an exec result. The statement must contain placeholders,
+// one for each element of the given arguments.
+func (c *Conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {
+	rawStmt, err := c.Prepare(query)
+	if err != nil {
+		return nil, fmt.Errorf("prepare: %w", err)
+	}
+
+	if stmt, ok := rawStmt.(*Stmt); ok {
+		return stmt.ExecContext(ctx, args)
+	}
+
+	return nil, fmt.Errorf("cannot execute Stmt of type %T, expected %T", rawStmt, &Stmt{})
+}
+
+// Ping pings the database, failing if the connection is closed or the database
+// failed.
+func (c *Conn) Ping(ctx context.Context) error {
+	return nil // TODO(TimSatke): implement
+}
+
+// QueryContext executes the given query with the given arguments under the
+// given context and returns a query result. The query must contain
+// placeholders, one for each element of the given arguments.
+func (c *Conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) {
+	rawStmt, err := c.Prepare(query)
+	if err != nil {
+		return nil, fmt.Errorf("prepare: %w", err)
+	}
+
+	if stmt, ok := rawStmt.(*Stmt); ok {
+		return stmt.QueryContext(ctx, args)
+	}
+
+	return nil, fmt.Errorf("cannot execute query Stmt of type %T, expected %T", rawStmt, &Stmt{})
+}
diff --git a/driver/connector.go b/driver/connector.go
new file mode 100644
index 0000000..89615a5
--- /dev/null
+++ b/driver/connector.go
@@ -0,0 +1,28 @@
+package driver
+
+import (
+	"context"
+	"database/sql/driver"
+)
+
+var _ driver.Connector = (*Connector)(nil)
+
+// Connector implements a component that is able to open a connection to the
+// database that is remembered by the connector. This connection can then be
+// used to prepare and execute statements.
+type Connector struct {
+	driver *Driver
+}
+
+// Connect opens a connection to the database that the connector is configured
+// to connect to. The opening of the connection pays respect to deadlines or
+// timeouts configured in the context.
+func (c *Connector) Connect(ctx context.Context) (driver.Conn, error) {
+	return &Conn{}, nil
+}
+
+// Driver returns the underlying driver, that the connector has been created
+// from.
+func (c *Connector) Driver() driver.Driver {
+	return c.driver
+}
diff --git a/driver/doc.go b/driver/doc.go
new file mode 100644
index 0000000..f47392f
--- /dev/null
+++ b/driver/doc.go
@@ -0,0 +1,2 @@
+// Package driver implements an SQL driver for an lbadd database.
+package driver
diff --git a/driver/driver.go b/driver/driver.go
new file mode 100644
index 0000000..4928679
--- /dev/null
+++ b/driver/driver.go
@@ -0,0 +1,45 @@
+package driver
+
+import (
+	"context"
+	"database/sql"
+	"database/sql/driver"
+	"fmt"
+)
+
+func init() {
+	sql.Register("lbadd", &Driver{})
+}
+
+var _ driver.Driver = (*Driver)(nil)
+var _ driver.DriverContext = (*Driver)(nil)
+
+// Driver is the database driver that can communicate with an lbadd database. It
+// will be registered with the name "lbadd".
+type Driver struct {
+}
+
+// Open creates a new connector and uses that connector to open a new
+// connection. The context that is used to open the new connection is
+// context.Background().
+func (d *Driver) Open(name string) (driver.Conn, error) {
+	connector, err := d.OpenConnector(name)
+	if err != nil {
+		return nil, fmt.Errorf("open connector: %w", err)
+	}
+
+	conn, err := connector.Connect(context.Background())
+	if err != nil {
+		return nil, fmt.Errorf("connect: %w", err)
+	}
+	return conn, nil
+}
+
+// OpenConnector creates a connector that can be used to open a connection to a
+// data source. The data source is specified by the given name. A connector can
+// only open connections to his data source.
+func (d *Driver) OpenConnector(name string) (driver.Connector, error) {
+	return &Connector{
+		driver: d,
+	}, nil
+}
diff --git a/driver/error.go b/driver/error.go
new file mode 100644
index 0000000..f02b236
--- /dev/null
+++ b/driver/error.go
@@ -0,0 +1,11 @@
+package driver
+
+// Error provides constant errors to the driver package.
+type Error string
+
+func (e Error) Error() string { return string(e) }
+
+// Constant errors
+const (
+	ErrConnectionClosed = Error("connection is closed")
+)
diff --git a/driver/stmt.go b/driver/stmt.go
new file mode 100644
index 0000000..c463d11
--- /dev/null
+++ b/driver/stmt.go
@@ -0,0 +1,72 @@
+package driver
+
+import (
+	"context"
+	"database/sql/driver"
+	"fmt"
+	"strconv"
+)
+
+var _ driver.Stmt = (*Stmt)(nil)
+var _ driver.StmtExecContext = (*Stmt)(nil)
+var _ driver.StmtQueryContext = (*Stmt)(nil)
+
+// Stmt is a prepared statement that can be executed. It does not remember
+// values that were passed in.
+type Stmt struct {
+}
+
+// parse attempts to parse the given query string. If the query string is valid
+// and supported sql, a statement and error=nil will be returned.
+func parse(query string) (*Stmt, error) {
+	return nil, fmt.Errorf("unimplemented") // TODO(TimSatke): implement
+}
+
+// Close closes this statement, making it impossible to execute it again.
+func (s *Stmt) Close() error {
+	return nil // TODO(TimSatke): implement
+}
+
+// NumInput returns the amount of argument placeholders that the statement has.
+func (s *Stmt) NumInput() int {
+	return 0 // TODO(TimSatke): implement
+}
+
+// Exec is discouraged. Don't use this, use ExecContext instead.
+func (s *Stmt) Exec(args []driver.Value) (driver.Result, error) {
+	namedValues := make([]driver.NamedValue, 0, len(args))
+	for i, arg := range args {
+		namedValues = append(namedValues, driver.NamedValue{
+			Name:    "param" + strconv.Itoa(i),
+			Ordinal: i,
+			Value:   arg,
+		})
+	}
+	return s.ExecContext(context.Background(), namedValues)
+}
+
+// Query is discouraged. Don't use this, use QueryContext instead.
+func (s *Stmt) Query(args []driver.Value) (driver.Rows, error) {
+	namedValues := make([]driver.NamedValue, 0, len(args))
+	for i, arg := range args {
+		namedValues = append(namedValues, driver.NamedValue{
+			Name:    "param" + strconv.Itoa(i),
+			Ordinal: i,
+			Value:   arg,
+		})
+	}
+	return s.QueryContext(context.Background(), namedValues)
+}
+
+// ExecContext executes this statement with the given arguments as arguments,
+// with respect to the given context. This should be used for update statements only (alter, update, drop, delete etc.).
+func (s *Stmt) ExecContext(ctx context.Context, args []driver.NamedValue) (driver.Result, error) {
+	return nil, fmt.Errorf("unimplemented") // TODO(TimSatke): implement
+}
+
+// QueryContext executes this statement with the given arguments as arguments,
+// with respect to the given context. This should be used for query statements
+// only (select etc.).
+func (s *Stmt) QueryContext(ctx context.Context, args []driver.NamedValue) (driver.Rows, error) {
+	return nil, fmt.Errorf("unimplemented") // TODO(TimSatke): implement
+}
diff --git a/internal/cli/simple_cli.go b/internal/cli/simple_cli.go
index a8d71d7..616a173 100644
--- a/internal/cli/simple_cli.go
+++ b/internal/cli/simple_cli.go
@@ -14,7 +14,8 @@ import (
 var _ Cli = (*simpleCli)(nil)
 
 type simpleCli struct {
-	ctx context.Context
+	ctx    context.Context
+	closed bool
 
 	in  io.Reader
 	out io.Writer
@@ -44,7 +45,7 @@ func (c *simpleCli) Start() {
 		}
 	}()
 
-	for {
+	for !c.closed {
 		_, _ = fmt.Fprint(c.out, "$ ")
 		select {
 		case line := <-lines:
@@ -72,6 +73,7 @@ func (c *simpleCli) printHelp() {
 
 func (c *simpleCli) quit() {
 	_, _ = fmt.Fprintln(c.out, "Bye!")
+	c.closed = true
 }
 
 func (c *simpleCli) handleSQL(sqlInput string) {
diff --git a/internal/server/doc.go b/internal/server/doc.go
new file mode 100644
index 0000000..e1b4e18
--- /dev/null
+++ b/internal/server/doc.go
@@ -0,0 +1,4 @@
+// Package server implements a server that is meant to provide external API
+// access to the database. This is the API that is used by our sql driver. The
+// documented API can be used to develop dedicated clients.
+package server
diff --git a/internal/server/server.go b/internal/server/server.go
new file mode 100644
index 0000000..3abf58b
--- /dev/null
+++ b/internal/server/server.go
@@ -0,0 +1,20 @@
+package server
+
+import (
+	"context"
+
+	"github.com/rs/zerolog"
+	"github.com/tomarrell/lbadd/internal/executor"
+)
+
+// Server describes a component that can be started on a network address with a
+// context, that is used to terminate the server, instead of a Close method.
+type Server interface {
+	ListenAndServe(context.Context, string) error
+}
+
+// New creates a new server that uses the given logger and executes statements
+// with the help of the given executor.
+func New(log zerolog.Logger, executor executor.Executor) Server {
+	return newSimpleServer(log, executor)
+}
diff --git a/internal/server/simple_server.go b/internal/server/simple_server.go
new file mode 100644
index 0000000..42ba002
--- /dev/null
+++ b/internal/server/simple_server.go
@@ -0,0 +1,29 @@
+package server
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/rs/zerolog"
+	"github.com/tomarrell/lbadd/internal/executor"
+)
+
+var _ Server = (*simpleServer)(nil)
+
+type simpleServer struct {
+	log zerolog.Logger
+
+	executor executor.Executor
+}
+
+func newSimpleServer(log zerolog.Logger, executor executor.Executor) *simpleServer {
+	return &simpleServer{
+		log:      log,
+		executor: executor,
+	}
+}
+
+func (s *simpleServer) ListenAndServe(ctx context.Context, addr string) error {
+	s.log.Info().Str("addr", addr).Msg("start listening")
+	return fmt.Errorf("unimplemented")
+}


Test patch:
diff --git a/driver/driver_test.go b/driver/driver_test.go
new file mode 100644
index 0000000..898cf7e
--- /dev/null
+++ b/driver/driver_test.go
@@ -0,0 +1,61 @@
+package driver_test
+
+import (
+	"database/sql"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	_ "github.com/tomarrell/lbadd/driver"
+)
+
+var (
+	LocalDatabaseAddress = ":53672"
+)
+
+func TestMain(m *testing.M) {
+	// TODO(TimSatke): start local database or mock it
+
+	exitCode := m.Run()
+
+	// TODO(TimSatke): shut down local database or destroy mock
+
+	os.Exit(exitCode)
+}
+
+func TestDriverRegister(t *testing.T) {
+	t.Logf("local database address: %v", LocalDatabaseAddress)
+	assert.Contains(t, sql.Drivers(), "lbadd")
+}
+
+func TestStatement(t *testing.T) {
+	t.SkipNow() // skip until database is functional
+
+	assert := assert.New(t)
+
+	pool, err := sql.Open("lbadd", LocalDatabaseAddress)
+	assert.NoError(err)
+	assert.NoError(pool.Ping())
+	defer func() {
+		assert.NoError(pool.Close())
+	}()
+
+	stmt, err := pool.Prepare(`CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(25));`)
+	assert.NoError(err)
+
+	_, err = stmt.Exec()
+	assert.NoError(err)
+	assert.NoError(stmt.Close())
+
+	stmt, err = pool.Prepare(`INSERT INTO users (name) VALUES (?);`)
+	assert.NoError(err)
+
+	result, err := stmt.Exec("jdoe")
+	assert.NoError(err)
+
+	affected, err := result.RowsAffected()
+	assert.NoError(err)
+	assert.Equal(affected, 1)
+
+	assert.NoError(stmt.Close())
+}
diff --git a/driver/test/doc.go b/driver/test/doc.go
new file mode 100644
index 0000000..ba85464
--- /dev/null
+++ b/driver/test/doc.go
@@ -0,0 +1,7 @@
+// Package test contains all integration tests that were created as regression
+// tests for issues that were fixed. Every test has to be named
+// TestIssue<number>, and the file issue<number>_test.go, to clearly identify,
+// which issue the test originated from. If there are multiple tests for this
+// issue, please run them as sub-tests. See issue62.go for an example and
+// additional info.
+package test
diff --git a/driver/test/issue62_test.go b/driver/test/issue62_test.go
new file mode 100644
index 0000000..f52352d
--- /dev/null
+++ b/driver/test/issue62_test.go
@@ -0,0 +1,26 @@
+package test
+
+import (
+	"database/sql"
+	"testing"
+)
+
+func TestIssue62(t *testing.T) {
+	/*
+		This test does not do anything. This is an example to show how
+		issue-based integration tests are written. This package contains a
+		test-main, which sets up and tears down a database server. In the tests,
+		obtain a connection pool and execute statements; in general, try to
+		reproduce the error that is described in the issue.
+
+		This generally works best if the issue was a reproducible sql statement,
+		that produced an error inside the parser/executor/db or wherever.
+
+		Assume that there is a database up and running on the address that is
+		stored inside TestAddress.
+	*/
+	pool, _ := sql.Open("lbadd", TestAddress)
+	if pool != nil {
+		_ = pool.Close()
+	}
+}
diff --git a/driver/test/main_test.go b/driver/test/main_test.go
new file mode 100644
index 0000000..c2f5610
--- /dev/null
+++ b/driver/test/main_test.go
@@ -0,0 +1,17 @@
+package test
+
+import (
+	"os"
+	"testing"
+)
+
+const (
+	TestAddress = "localhost:57263"
+)
+
+func TestMain(m *testing.M) {
+
+	exitCode := m.Run()
+
+	os.Exit(exitCode)
+}