[lxc-devel] [lxd/master] Add db/query sub-package with common query helpers
freeekanayaka on Github
lxc-bot at linuxcontainers.org
Tue Aug 22 12:22:39 UTC 2017
A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 518 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20170822/34431746/attachment.bin>
-------------- next part --------------
From f7efe86d15b2ca58ce4d2b4f48bfc642347fbac1 Mon Sep 17 00:00:00 2001
From: Free Ekanayaka <free.ekanayaka at canonical.com>
Date: Sun, 20 Aug 2017 14:59:07 +0000
Subject: [PATCH] Add db/query sub-package with common query helpers
Introduce helpers around database/sql to execute various kinds of very
common SQL read queries. This package will grow as needed in follow-up
branches.
Signed-off-by: Free Ekanayaka <free.ekanayaka at canonical.com>
---
lxd/db/query/doc.go | 3 ++
lxd/db/query/slices.go | 83 ++++++++++++++++++++++++++++++++++++++++++
lxd/db/query/slices_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 175 insertions(+)
create mode 100644 lxd/db/query/doc.go
create mode 100644 lxd/db/query/slices.go
create mode 100644 lxd/db/query/slices_test.go
diff --git a/lxd/db/query/doc.go b/lxd/db/query/doc.go
new file mode 100644
index 000000000..eef2fc7bf
--- /dev/null
+++ b/lxd/db/query/doc.go
@@ -0,0 +1,3 @@
+// Package query implements helpers around database/sql to execute various
+// kinds of very common SQL read queries.
+package query
diff --git a/lxd/db/query/slices.go b/lxd/db/query/slices.go
new file mode 100644
index 000000000..472620848
--- /dev/null
+++ b/lxd/db/query/slices.go
@@ -0,0 +1,83 @@
+package query
+
+import (
+ "database/sql"
+ "fmt"
+ "strings"
+)
+
+// Strings executes a statement which must yield rows with a single string
+// column. It returns the list of column values.
+func Strings(tx *sql.Tx, query string) ([]string, error) {
+ values := []string{}
+ scan := func(rows *sql.Rows) error {
+ var value string
+ if err := rows.Scan(&value); err != nil {
+ return err
+ }
+ values = append(values, value)
+ return nil
+ }
+
+ if err := scanSingleColumn(tx, query, "TEXT", scan); err != nil {
+ return nil, err
+ }
+
+ return values, nil
+}
+
+// Integers executes a statement which must yield rows with a single integer
+// column. It returns the list of column values.
+func Integers(tx *sql.Tx, query string) ([]int, error) {
+ values := []int{}
+ scan := func(rows *sql.Rows) error {
+ var value int
+ if err := rows.Scan(&value); err != nil {
+ return err
+ }
+ values = append(values, value)
+ return nil
+ }
+
+ if err := scanSingleColumn(tx, query, "INTEGER", scan); err != nil {
+ return nil, err
+ }
+
+ return values, nil
+}
+
+// Execute the given query and ensure that it yields rows with a single column
+// of the given database type. For every row yielded, execute the given
+// scanner.
+func scanSingleColumn(tx *sql.Tx, query string, typeName string, scan scanFunc) error {
+ rows, err := tx.Query(query)
+ if err != nil {
+ return err
+ }
+ defer rows.Close()
+
+ types, err := rows.ColumnTypes()
+ if err != nil {
+ return err
+ }
+ if len(types) != 1 {
+ return fmt.Errorf("query yields %d columns, not 1", len(types))
+ }
+ actualTypeName := strings.ToUpper(types[0].DatabaseTypeName())
+ if actualTypeName != typeName {
+ return fmt.Errorf("query yields %s column, not %s", actualTypeName, typeName)
+ }
+
+ for rows.Next() {
+ if err := scan(rows); err != nil {
+ return err
+ }
+ }
+ if err := rows.Err(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// Function to scan a single row.
+type scanFunc func(*sql.Rows) error
diff --git a/lxd/db/query/slices_test.go b/lxd/db/query/slices_test.go
new file mode 100644
index 000000000..e079f3649
--- /dev/null
+++ b/lxd/db/query/slices_test.go
@@ -0,0 +1,89 @@
+package query_test
+
+import (
+ "database/sql"
+ "fmt"
+ "testing"
+
+ _ "github.com/mattn/go-sqlite3"
+ "github.com/mpvl/subtest"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/lxc/lxd/lxd/db/query"
+)
+
+// Exercise possible failure modes.
+func TestStrings_Error(t *testing.T) {
+ cases := []struct {
+ query string
+ error string
+ }{
+ {"garbage", "near \"garbage\": syntax error"},
+ {"SELECT id, name FROM test", "query yields 2 columns, not 1"},
+ {"SELECT id FROM test", "query yields INTEGER column, not TEXT"},
+ }
+ for _, c := range cases {
+ subtest.Run(t, c.query, func(t *testing.T) {
+ tx := newTxForSlices()
+ values, err := query.Strings(tx, c.query)
+ assert.EqualError(t, err, c.error)
+ assert.Nil(t, values)
+ })
+ }
+}
+
+// All values yield by the query are returned.
+func TestStrings(t *testing.T) {
+ tx := newTxForSlices()
+ values, err := query.Strings(tx, "SELECT name FROM test ORDER BY name")
+ assert.Nil(t, err)
+ assert.Equal(t, []string{"bar", "foo"}, values)
+}
+
+// Exercise possible failure modes.
+func TestIntegers_Error(t *testing.T) {
+ cases := []struct {
+ query string
+ error string
+ }{
+ {"garbage", "near \"garbage\": syntax error"},
+ {"SELECT id, name FROM test", "query yields 2 columns, not 1"},
+ {"SELECT name FROM test", "query yields TEXT column, not INTEGER"},
+ }
+ for _, c := range cases {
+ subtest.Run(t, c.query, func(t *testing.T) {
+ tx := newTxForSlices()
+ values, err := query.Integers(tx, c.query)
+ assert.EqualError(t, err, c.error)
+ assert.Nil(t, values)
+ })
+ }
+}
+
+// All values yield by the query are returned.
+func TestIntegers(t *testing.T) {
+ tx := newTxForSlices()
+ values, err := query.Integers(tx, "SELECT id FROM test ORDER BY id")
+ assert.Nil(t, err)
+ assert.Equal(t, []int{0, 1}, values)
+}
+
+// Return a new transaction against an in-memory SQLite database with a single
+// test table populated with a few rows.
+func newTxForSlices() *sql.Tx {
+ db, err := sql.Open("sqlite3", ":memory:")
+ if err != nil {
+ panic(fmt.Sprintf("failed to create database: %v", err))
+ }
+ if _, err := db.Exec("CREATE TABLE test (id INTEGER, name TEXT)"); err != nil {
+ panic(fmt.Sprintf("failed to create table: %v", err))
+ }
+ if _, err := db.Exec("INSERT INTO test VALUES (0, 'foo'), (1, 'bar')"); err != nil {
+ panic(fmt.Sprintf("failed to insert rows: %v", err))
+ }
+ tx, err := db.Begin()
+ if err != nil {
+ panic(fmt.Sprintf("failed to begin transaction: %v", err))
+ }
+ return tx
+}
More information about the lxc-devel
mailing list