From 5b51b1ac0ca13a175c70fa11003b738772397744 Mon Sep 17 00:00:00 2001 From: Asdine El Hrychy Date: Wed, 4 Dec 2019 20:47:19 +0100 Subject: [PATCH] Move tests to query package --- db_test.go | 389 --------------------------- parser/expr_test.go | 86 ++++++ parser/parser_test.go | 78 ------ query/{create_index.go => create.go} | 39 +++ query/create_table.go | 47 ---- query/create_test.go | 76 ++++++ query/delete_test.go | 58 ++++ query/{drop_index.go => drop.go} | 28 ++ query/drop_table.go | 36 --- query/drop_test.go | 40 +++ query/insert.go | 11 + query/insert_test.go | 107 ++++++++ query/select_test.go | 109 ++++++++ query/update_test.go | 64 +++++ 14 files changed, 618 insertions(+), 550 deletions(-) create mode 100644 parser/expr_test.go rename query/{create_index.go => create.go} (54%) delete mode 100644 query/create_table.go create mode 100644 query/create_test.go create mode 100644 query/delete_test.go rename query/{drop_index.go => drop.go} (53%) delete mode 100644 query/drop_table.go create mode 100644 query/drop_test.go create mode 100644 query/insert_test.go create mode 100644 query/select_test.go create mode 100644 query/update_test.go diff --git a/db_test.go b/db_test.go index cf3f1b86..513734f2 100644 --- a/db_test.go +++ b/db_test.go @@ -1,8 +1,6 @@ package genji_test import ( - "bytes" - "database/sql" "fmt" "log" "testing" @@ -305,390 +303,3 @@ func ExampleResult_Iterate() { // {10 foo10 100} // 10 foo10 100 } - -func TestCreateTable(t *testing.T) { - tests := []struct { - name string - query string - fails bool - }{ - {"Basic", `CREATE TABLE test`, false}, - {"Exists", "CREATE TABLE test;CREATE TABLE test", true}, - {"If not exists", "CREATE TABLE IF NOT EXISTS test", false}, - {"If not exists, twice", "CREATE TABLE IF NOT EXISTS test;CREATE TABLE IF NOT EXISTS test", false}, - {"With primary key", "CREATE TABLE test(foo STRING PRIMARY KEY)", false}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec(test.query) - if test.fails { - require.Error(t, err) - return - } - require.NoError(t, err) - - err = db.ViewTable("test", func(_ *genji.Tx, _ *database.Table) error { - return nil - }) - require.NoError(t, err) - }) - } -} - -func TestCreateIndex(t *testing.T) { - tests := []struct { - name string - query string - fails bool - }{ - {"Basic", "CREATE INDEX idx ON test (foo)", false}, - {"If not exists", "CREATE INDEX IF NOT EXISTS idx ON test (foo)", false}, - {"Unique", "CREATE UNIQUE INDEX IF NOT EXISTS idx ON test (foo)", false}, - {"No fields", "CREATE INDEX idx ON test", true}, - {"More than 1 field", "CREATE INDEX idx ON test (foo, bar)", true}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec("CREATE TABLE test") - require.NoError(t, err) - - err = db.Exec(test.query) - if test.fails { - require.Error(t, err) - return - } - require.NoError(t, err) - }) - } -} - -func TestDrop(t *testing.T) { - tests := []struct { - name string - query string - fails bool - }{ - {"Drop table", "DROP TABLE test", false}, - {"Drop table If not exists", "DROP TABLE IF EXISTS test", false}, - {"Drop index", "DROP INDEX idx", false}, - {"Drop index if exists", "DROP INDEX IF EXISTS idx", false}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec("CREATE TABLE test; CREATE INDEX idx ON test (foo)") - require.NoError(t, err) - - err = db.Exec(test.query) - if test.fails { - require.Error(t, err) - return - } - require.NoError(t, err) - }) - } -} - -func TestDeleteStmt(t *testing.T) { - tests := []struct { - name string - query string - fails bool - expected string - params []interface{} - }{ - {"No cond", `DELETE FROM test`, false, "", nil}, - {"With cond", "DELETE FROM test WHERE b = 'bar1'", false, "bar2,foo3,bar3\n", nil}, - {"Table not found", "DELETE FROM foo WHERE b = 'bar1'", true, "", nil}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec("CREATE TABLE test") - require.NoError(t, err) - err = db.Exec("INSERT INTO test (a, b, c) VALUES ('foo1', 'bar1', 'baz1')") - require.NoError(t, err) - err = db.Exec("INSERT INTO test (a, b) VALUES ('foo2', 'bar1')") - require.NoError(t, err) - err = db.Exec("INSERT INTO test (d, b, e) VALUES ('foo3', 'bar2', 'bar3')") - require.NoError(t, err) - - err = db.Exec(test.query, test.params...) - if test.fails { - require.Error(t, err) - return - } - require.NoError(t, err) - - st, err := db.Query("SELECT * FROM test") - require.NoError(t, err) - defer st.Close() - - var buf bytes.Buffer - err = document.IteratorToCSV(&buf, st) - require.NoError(t, err) - require.Equal(t, test.expected, buf.String()) - }) - } -} - -func TestInsertStmt(t *testing.T) { - tests := []struct { - name string - query string - fails bool - expected string - params []interface{} - }{ - {"Values / No columns", `INSERT INTO test VALUES ("a", 'b', 'c')`, true, ``, nil}, - {"Values / With columns", `INSERT INTO test (a, b, c) VALUES ('a', 'b', 'c')`, false, "1,a,b,c\n", nil}, - {"Values / Ident", `INSERT INTO test (a) VALUES (a)`, true, ``, nil}, - {"Values / Ident string", `INSERT INTO test (a) VALUES ("a")`, true, ``, nil}, - {"Values / With fields ident string", `INSERT INTO test (a, "foo bar") VALUES ('c', 'd')`, false, "1,c,d\n", nil}, - {"Values / Positional Params", "INSERT INTO test (a, b, c) VALUES (?, 'e', ?)", false, "1,d,e,f\n", []interface{}{"d", "f"}}, - {"Values / Named Params", "INSERT INTO test (a, b, c) VALUES ($d, 'e', $f)", false, "1,d,e,f\n", []interface{}{sql.Named("f", "f"), sql.Named("d", "d")}}, - {"Values / Invalid params", "INSERT INTO test (a, b, c) VALUES ('d', ?)", true, "", []interface{}{'e'}}, - {"Values / List", `INSERT INTO test (a, b, c) VALUES ("a", 'b', (1, 2, 3))`, true, "", nil}, - {"Records", "INSERT INTO test DOCUMENTS (a: 'a', b: 2.3, c: 1 = 1)", false, "1,a,2.3,true\n", nil}, - {"Records / Positional Params", "INSERT INTO test DOCUMENTS (a: ?, b: 2.3, c: ?)", false, "1,a,2.3,true\n", []interface{}{"a", true}}, - {"Records / Named Params", "INSERT INTO test DOCUMENTS (a: $a, b: 2.3, c: $c)", false, "1,1,2.3,true\n", []interface{}{sql.Named("c", true), sql.Named("a", 1)}}, - {"Records / List ", "INSERT INTO test DOCUMENTS (a: (1, 2, 3))", true, "", nil}, - {"Records / strings", `INSERT INTO test DOCUMENTS ('a': 'a', b: 2.3)`, true, "", nil}, - {"Records / ident value", `INSERT INTO test DOCUMENTS ("a": "a")`, true, "", nil}, - } - - for _, test := range tests { - testFn := func(withIndexes bool) func(t *testing.T) { - return func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec("CREATE TABLE test") - require.NoError(t, err) - if withIndexes { - err = db.Exec(` - CREATE INDEX idx_a ON test (a); - CREATE INDEX idx_b ON test (b); - CREATE INDEX idx_c ON test (c); - `) - require.NoError(t, err) - } - err = db.Exec(test.query, test.params...) - if test.fails { - require.Error(t, err) - return - } - require.NoError(t, err) - - st, err := db.Query("SELECT key(), * FROM test") - require.NoError(t, err) - defer st.Close() - - var buf bytes.Buffer - err = document.IteratorToCSV(&buf, st) - require.NoError(t, err) - require.Equal(t, test.expected, buf.String()) - } - } - - t.Run("No Index/"+test.name, testFn(false)) - t.Run("With Index/"+test.name, testFn(true)) - } - - t.Run("with primary key", func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec("CREATE TABLE test (foo INTEGER PRIMARY KEY)") - require.NoError(t, err) - - err = db.Exec(`INSERT INTO test (bar) VALUES (1)`) - require.Error(t, err) - err = db.Exec(`INSERT INTO test (bar, foo) VALUES (1, 2)`) - require.NoError(t, err) - - err = db.Exec(`INSERT INTO test (bar, foo) VALUES (1, 2)`) - require.Equal(t, err, database.ErrDuplicateRecord) - }) - - t.Run("with shadowing", func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec("CREATE TABLE test") - require.NoError(t, err) - - err = db.Exec(`INSERT INTO test ("key()", "key") VALUES (1, 2)`) - require.NoError(t, err) - }) -} - -func TestSelectStmt(t *testing.T) { - tests := []struct { - name string - query string - fails bool - expected string - params []interface{} - }{ - {"No cond", "SELECT * FROM test", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\nfoo3,bar2,3\n", nil}, - {"Multiple wildcards cond", "SELECT *, *, a FROM test", false, "foo1,bar1,baz1,1,foo1,bar1,baz1,1,foo1\nfoo2,bar1,1,2,foo2,bar1,1,2,foo2\nfoo3,bar2,3,foo3,bar2,3\n", nil}, - {"With fields", "SELECT a, c FROM test", false, "foo1,baz1\nfoo2\n\n", nil}, - {"With eq cond", "SELECT * FROM test WHERE b = 'bar1'", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\n", nil}, - {"With neq cond", "SELECT * FROM test WHERE a != 'foo1'", false, "foo2,bar1,1,2\nfoo3,bar2,3\n", nil}, - {"With gt cond", "SELECT * FROM test WHERE b > 'bar1'", false, "", nil}, - {"With lt cond", "SELECT * FROM test WHERE a < 'zzzzz'", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\n", nil}, - {"With lte cond", "SELECT * FROM test WHERE a <= 'foo3'", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\n", nil}, - {"With field comparison", "SELECT * FROM test WHERE b < a", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\n", nil}, - {"With limit", "SELECT * FROM test WHERE b = 'bar1' LIMIT 1", false, "foo1,bar1,baz1,1\n", nil}, - {"With offset", "SELECT *, key() FROM test WHERE b = 'bar1' OFFSET 1", false, "foo2,bar1,1,2,2\n", nil}, - {"With limit then offset", "SELECT * FROM test WHERE b = 'bar1' LIMIT 1 OFFSET 1", false, "foo2,bar1,1,2\n", nil}, - {"With offset then limit", "SELECT * FROM test WHERE b = 'bar1' OFFSET 1 LIMIT 1", true, "", nil}, - {"With positional params", "SELECT * FROM test WHERE a = ? OR d = ?", false, "foo1,bar1,baz1,1\nfoo3,bar2,3\n", []interface{}{"foo1", "foo3"}}, - {"With named params", "SELECT * FROM test WHERE a = $a OR d = $d", false, "foo1,bar1,baz1,1\nfoo3,bar2,3\n", []interface{}{sql.Named("a", "foo1"), sql.Named("d", "foo3")}}, - {"With key()", "SELECT key(), a FROM test", false, "1,foo1\n2,foo2\n3\n", []interface{}{sql.Named("a", "foo1"), sql.Named("d", "foo3")}}, - {"With pk in cond, gt", "SELECT * FROM test WHERE k > 0 AND e = 1", false, "foo2,bar1,1,2\n", nil}, - {"With pk in cond, =", "SELECT * FROM test WHERE k = 2.0 AND e = 1", false, "foo2,bar1,1,2\n", nil}, - } - - for _, test := range tests { - testFn := func(withIndexes bool) func(t *testing.T) { - return func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec("CREATE TABLE test (k INTEGER PRIMARY KEY)") - require.NoError(t, err) - if withIndexes { - err = db.Exec(` - CREATE INDEX idx_a ON test (a); - CREATE INDEX idx_b ON test (b); - CREATE INDEX idx_c ON test (c); - CREATE INDEX idx_d ON test (d); - `) - require.NoError(t, err) - } - - err = db.Exec("INSERT INTO test (k, a, b, c) VALUES (1, 'foo1', 'bar1', 'baz1')") - require.NoError(t, err) - err = db.Exec("INSERT INTO test (k, a, b, e) VALUES (2, 'foo2', 'bar1', 1)") - require.NoError(t, err) - err = db.Exec("INSERT INTO test (k, d, e) VALUES (3, 'foo3', 'bar2')") - require.NoError(t, err) - - st, err := db.Query(test.query, test.params...) - if test.fails { - require.Error(t, err) - return - } - require.NoError(t, err) - defer st.Close() - - var buf bytes.Buffer - err = document.IteratorToCSV(&buf, st) - require.NoError(t, err) - require.Equal(t, test.expected, buf.String()) - } - } - t.Run("No Index/"+test.name, testFn(false)) - t.Run("With Index/"+test.name, testFn(true)) - } - - t.Run("with primary key only", func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec("CREATE TABLE test (foo UINT8 PRIMARY KEY)") - require.NoError(t, err) - - err = db.Exec(`INSERT INTO test (foo, bar) VALUES (1, 'a')`) - err = db.Exec(`INSERT INTO test (foo, bar) VALUES (2, 'b')`) - err = db.Exec(`INSERT INTO test (foo, bar) VALUES (3, 'c')`) - err = db.Exec(`INSERT INTO test (foo, bar) VALUES (4, 'd')`) - require.NoError(t, err) - - st, err := db.Query("SELECT * FROM test WHERE foo < 400 AND foo >= 2") - require.NoError(t, err) - defer st.Close() - - var buf bytes.Buffer - err = document.IteratorToCSV(&buf, st) - require.NoError(t, err) - require.Equal(t, "b,2\nc,3\nd,4\n", buf.String()) - }) -} - -func TestUpdateStmt(t *testing.T) { - tests := []struct { - name string - query string - fails bool - expected string - params []interface{} - }{ - {"No cond", `UPDATE test SET a = 'boo'`, false, "boo,bar1,baz1\nboo,bar2\nfoo3,bar3\n", nil}, - {"No cond / with ident string", `UPDATE test SET "a" = 'boo'`, false, "boo,bar1,baz1\nboo,bar2\nfoo3,bar3\n", nil}, - {"No cond / with multiple idents", `UPDATE test SET a = c`, false, "baz1,bar1,baz1\nNULL,bar2\nfoo3,bar3\n", nil}, - {"No cond / with string", `UPDATE test SET 'a' = 'boo'`, true, "", nil}, - {"With cond", "UPDATE test SET a = 1, b = 2 WHERE a = 'foo2'", false, "foo1,bar1,baz1\n1,2\nfoo3,bar3\n", nil}, - {"Field not found", "UPDATE test SET a = 1, b = 2 WHERE a = f", false, "foo1,bar1,baz1\nfoo2,bar2\nfoo3,bar3\n", nil}, - {"Positional params", "UPDATE test SET a = ?, b = ? WHERE a = ?", false, "a,b,baz1\nfoo2,bar2\nfoo3,bar3\n", []interface{}{"a", "b", "foo1"}}, - {"Named params", "UPDATE test SET a = $a, b = $b WHERE a = $c", false, "a,b,baz1\nfoo2,bar2\nfoo3,bar3\n", []interface{}{sql.Named("b", "b"), sql.Named("a", "a"), sql.Named("c", "foo1")}}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - db, err := genji.New(memoryengine.NewEngine()) - require.NoError(t, err) - defer db.Close() - - err = db.Exec("CREATE TABLE test") - require.NoError(t, err) - err = db.Exec("INSERT INTO test (a, b, c) VALUES ('foo1', 'bar1', 'baz1')") - require.NoError(t, err) - err = db.Exec("INSERT INTO test (a, b) VALUES ('foo2', 'bar2')") - require.NoError(t, err) - err = db.Exec("INSERT INTO test (d, e) VALUES ('foo3', 'bar3')") - require.NoError(t, err) - - err = db.Exec(test.query, test.params...) - if test.fails { - require.Error(t, err) - return - } - require.NoError(t, err) - - st, err := db.Query("SELECT * FROM test") - require.NoError(t, err) - defer st.Close() - - var buf bytes.Buffer - err = document.IteratorToCSV(&buf, st) - require.NoError(t, err) - require.Equal(t, test.expected, buf.String()) - }) - } -} diff --git a/parser/expr_test.go b/parser/expr_test.go new file mode 100644 index 00000000..2f7eb6f7 --- /dev/null +++ b/parser/expr_test.go @@ -0,0 +1,86 @@ +package parser + +import ( + "strings" + "testing" + + "github.com/asdine/genji/query" + "github.com/stretchr/testify/require" +) + +func TestParserExpr(t *testing.T) { + tests := []struct { + name string + s string + expected query.Expr + }{ + {"=", "age = 10", query.Eq(query.FieldSelector("age"), query.Int8Value(10))}, + {"!=", "age != 10", query.Neq(query.FieldSelector("age"), query.Int8Value(10))}, + {">", "age > 10", query.Gt(query.FieldSelector("age"), query.Int8Value(10))}, + {">=", "age >= 10", query.Gte(query.FieldSelector("age"), query.Int8Value(10))}, + {"<", "age < 10", query.Lt(query.FieldSelector("age"), query.Int8Value(10))}, + {"<=", "age <= 10", query.Lte(query.FieldSelector("age"), query.Int8Value(10))}, + {"AND", "age = 10 AND age <= 11", + query.And( + query.Eq(query.FieldSelector("age"), query.Int8Value(10)), + query.Lte(query.FieldSelector("age"), query.Int8Value(11)), + )}, + {"OR", "age = 10 OR age = 11", + query.Or( + query.Eq(query.FieldSelector("age"), query.Int8Value(10)), + query.Eq(query.FieldSelector("age"), query.Int8Value(11)), + )}, + {"AND then OR", "age >= 10 AND age > $age OR age < 10.4", + query.Or( + query.And( + query.Gte(query.FieldSelector("age"), query.Int8Value(10)), + query.Gt(query.FieldSelector("age"), query.NamedParam("age")), + ), + query.Lt(query.FieldSelector("age"), query.Float64Value(10.4)), + )}, + {"with NULL", "age > NULL", query.Gt(query.FieldSelector("age"), query.NullValue())}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ex, err := NewParser(strings.NewReader(test.s)).ParseExpr() + require.NoError(t, err) + require.EqualValues(t, test.expected, ex) + }) + } +} + +func TestParserParams(t *testing.T) { + tests := []struct { + name string + s string + expected query.Expr + errored bool + }{ + {"one positional", "age = ?", query.Eq(query.FieldSelector("age"), query.PositionalParam(1)), false}, + {"multiple positional", "age = ? AND age <= ?", + query.And( + query.Eq(query.FieldSelector("age"), query.PositionalParam(1)), + query.Lte(query.FieldSelector("age"), query.PositionalParam(2)), + ), false}, + {"one named", "age = $age", query.Eq(query.FieldSelector("age"), query.NamedParam("age")), false}, + {"multiple named", "age = $foo OR age = $bar", + query.Or( + query.Eq(query.FieldSelector("age"), query.NamedParam("foo")), + query.Eq(query.FieldSelector("age"), query.NamedParam("bar")), + ), false}, + {"mixed", "age >= ? AND age > $foo OR age < ?", nil, true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ex, err := NewParser(strings.NewReader(test.s)).ParseExpr() + if test.errored { + require.Error(t, err) + } else { + require.NoError(t, err) + require.EqualValues(t, test.expected, ex) + } + }) + } +} diff --git a/parser/parser_test.go b/parser/parser_test.go index 253a069e..8ecae2de 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -1,90 +1,12 @@ package parser import ( - "strings" "testing" "github.com/asdine/genji/query" "github.com/stretchr/testify/require" ) -func TestParserExpr(t *testing.T) { - tests := []struct { - name string - s string - expected query.Expr - }{ - {"=", "age = 10", query.Eq(query.FieldSelector("age"), query.Int8Value(10))}, - {"!=", "age != 10", query.Neq(query.FieldSelector("age"), query.Int8Value(10))}, - {">", "age > 10", query.Gt(query.FieldSelector("age"), query.Int8Value(10))}, - {">=", "age >= 10", query.Gte(query.FieldSelector("age"), query.Int8Value(10))}, - {"<", "age < 10", query.Lt(query.FieldSelector("age"), query.Int8Value(10))}, - {"<=", "age <= 10", query.Lte(query.FieldSelector("age"), query.Int8Value(10))}, - {"AND", "age = 10 AND age <= 11", - query.And( - query.Eq(query.FieldSelector("age"), query.Int8Value(10)), - query.Lte(query.FieldSelector("age"), query.Int8Value(11)), - )}, - {"OR", "age = 10 OR age = 11", - query.Or( - query.Eq(query.FieldSelector("age"), query.Int8Value(10)), - query.Eq(query.FieldSelector("age"), query.Int8Value(11)), - )}, - {"AND then OR", "age >= 10 AND age > $age OR age < 10.4", - query.Or( - query.And( - query.Gte(query.FieldSelector("age"), query.Int8Value(10)), - query.Gt(query.FieldSelector("age"), query.NamedParam("age")), - ), - query.Lt(query.FieldSelector("age"), query.Float64Value(10.4)), - )}, - {"with NULL", "age > NULL", query.Gt(query.FieldSelector("age"), query.NullValue())}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ex, err := NewParser(strings.NewReader(test.s)).ParseExpr() - require.NoError(t, err) - require.EqualValues(t, test.expected, ex) - }) - } -} - -func TestParserParams(t *testing.T) { - tests := []struct { - name string - s string - expected query.Expr - errored bool - }{ - {"one positional", "age = ?", query.Eq(query.FieldSelector("age"), query.PositionalParam(1)), false}, - {"multiple positional", "age = ? AND age <= ?", - query.And( - query.Eq(query.FieldSelector("age"), query.PositionalParam(1)), - query.Lte(query.FieldSelector("age"), query.PositionalParam(2)), - ), false}, - {"one named", "age = $age", query.Eq(query.FieldSelector("age"), query.NamedParam("age")), false}, - {"multiple named", "age = $foo OR age = $bar", - query.Or( - query.Eq(query.FieldSelector("age"), query.NamedParam("foo")), - query.Eq(query.FieldSelector("age"), query.NamedParam("bar")), - ), false}, - {"mixed", "age >= ? AND age > $foo OR age < ?", nil, true}, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - ex, err := NewParser(strings.NewReader(test.s)).ParseExpr() - if test.errored { - require.Error(t, err) - } else { - require.NoError(t, err) - require.EqualValues(t, test.expected, ex) - } - }) - } -} - func TestParserMultiStatement(t *testing.T) { tests := []struct { name string diff --git a/query/create_index.go b/query/create.go similarity index 54% rename from query/create_index.go rename to query/create.go index 43839a7e..5434066b 100644 --- a/query/create_index.go +++ b/query/create.go @@ -5,9 +5,48 @@ import ( "errors" "github.com/asdine/genji/database" + "github.com/asdine/genji/document" "github.com/asdine/genji/index" ) +// CreateTableStmt is a DSL that allows creating a full CREATE TABLE statement. +type CreateTableStmt struct { + TableName string + IfNotExists bool + PrimaryKeyName string + PrimaryKeyType document.ValueType +} + +// IsReadOnly always returns false. It implements the Statement interface. +func (stmt CreateTableStmt) IsReadOnly() bool { + return false +} + +// Run runs the Create table statement in the given transaction. +// It implements the Statement interface. +func (stmt CreateTableStmt) Run(tx *database.Transaction, args []driver.NamedValue) (Result, error) { + var res Result + + if stmt.TableName == "" { + return res, errors.New("missing table name") + } + + var cfg *database.TableConfig + + if stmt.PrimaryKeyName != "" { + cfg = new(database.TableConfig) + cfg.PrimaryKeyName = stmt.PrimaryKeyName + cfg.PrimaryKeyType = stmt.PrimaryKeyType + } + + err := tx.CreateTable(stmt.TableName, cfg) + if stmt.IfNotExists && err == database.ErrTableAlreadyExists { + err = nil + } + + return res, err +} + // CreateIndexStmt is a DSL that allows creating a full CREATE INDEX statement. // It is typically created using the CreateIndex function. type CreateIndexStmt struct { diff --git a/query/create_table.go b/query/create_table.go deleted file mode 100644 index 7c1ca9f8..00000000 --- a/query/create_table.go +++ /dev/null @@ -1,47 +0,0 @@ -package query - -import ( - "database/sql/driver" - "errors" - - "github.com/asdine/genji/database" - "github.com/asdine/genji/document" -) - -// CreateTableStmt is a DSL that allows creating a full CREATE TABLE statement. -type CreateTableStmt struct { - TableName string - IfNotExists bool - PrimaryKeyName string - PrimaryKeyType document.ValueType -} - -// IsReadOnly always returns false. It implements the Statement interface. -func (stmt CreateTableStmt) IsReadOnly() bool { - return false -} - -// Run runs the Create table statement in the given transaction. -// It implements the Statement interface. -func (stmt CreateTableStmt) Run(tx *database.Transaction, args []driver.NamedValue) (Result, error) { - var res Result - - if stmt.TableName == "" { - return res, errors.New("missing table name") - } - - var cfg *database.TableConfig - - if stmt.PrimaryKeyName != "" { - cfg = new(database.TableConfig) - cfg.PrimaryKeyName = stmt.PrimaryKeyName - cfg.PrimaryKeyType = stmt.PrimaryKeyType - } - - err := tx.CreateTable(stmt.TableName, cfg) - if stmt.IfNotExists && err == database.ErrTableAlreadyExists { - err = nil - } - - return res, err -} diff --git a/query/create_test.go b/query/create_test.go new file mode 100644 index 00000000..94233b91 --- /dev/null +++ b/query/create_test.go @@ -0,0 +1,76 @@ +package query_test + +import ( + "testing" + + "github.com/asdine/genji" + "github.com/asdine/genji/database" + "github.com/asdine/genji/engine/memoryengine" + "github.com/stretchr/testify/require" +) + +func TestCreateTable(t *testing.T) { + tests := []struct { + name string + query string + fails bool + }{ + {"Basic", `CREATE TABLE test`, false}, + {"Exists", "CREATE TABLE test;CREATE TABLE test", true}, + {"If not exists", "CREATE TABLE IF NOT EXISTS test", false}, + {"If not exists, twice", "CREATE TABLE IF NOT EXISTS test;CREATE TABLE IF NOT EXISTS test", false}, + {"With primary key", "CREATE TABLE test(foo STRING PRIMARY KEY)", false}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec(test.query) + if test.fails { + require.Error(t, err) + return + } + require.NoError(t, err) + + err = db.ViewTable("test", func(_ *genji.Tx, _ *database.Table) error { + return nil + }) + require.NoError(t, err) + }) + } +} + +func TestCreateIndex(t *testing.T) { + tests := []struct { + name string + query string + fails bool + }{ + {"Basic", "CREATE INDEX idx ON test (foo)", false}, + {"If not exists", "CREATE INDEX IF NOT EXISTS idx ON test (foo)", false}, + {"Unique", "CREATE UNIQUE INDEX IF NOT EXISTS idx ON test (foo)", false}, + {"No fields", "CREATE INDEX idx ON test", true}, + {"More than 1 field", "CREATE INDEX idx ON test (foo, bar)", true}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec("CREATE TABLE test") + require.NoError(t, err) + + err = db.Exec(test.query) + if test.fails { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/query/delete_test.go b/query/delete_test.go new file mode 100644 index 00000000..5592fe3c --- /dev/null +++ b/query/delete_test.go @@ -0,0 +1,58 @@ +package query_test + +import ( + "bytes" + "testing" + + "github.com/asdine/genji" + "github.com/asdine/genji/document" + "github.com/asdine/genji/engine/memoryengine" + "github.com/stretchr/testify/require" +) + +func TestDeleteStmt(t *testing.T) { + tests := []struct { + name string + query string + fails bool + expected string + params []interface{} + }{ + {"No cond", `DELETE FROM test`, false, "", nil}, + {"With cond", "DELETE FROM test WHERE b = 'bar1'", false, "bar2,foo3,bar3\n", nil}, + {"Table not found", "DELETE FROM foo WHERE b = 'bar1'", true, "", nil}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec("CREATE TABLE test") + require.NoError(t, err) + err = db.Exec("INSERT INTO test (a, b, c) VALUES ('foo1', 'bar1', 'baz1')") + require.NoError(t, err) + err = db.Exec("INSERT INTO test (a, b) VALUES ('foo2', 'bar1')") + require.NoError(t, err) + err = db.Exec("INSERT INTO test (d, b, e) VALUES ('foo3', 'bar2', 'bar3')") + require.NoError(t, err) + + err = db.Exec(test.query, test.params...) + if test.fails { + require.Error(t, err) + return + } + require.NoError(t, err) + + st, err := db.Query("SELECT * FROM test") + require.NoError(t, err) + defer st.Close() + + var buf bytes.Buffer + err = document.IteratorToCSV(&buf, st) + require.NoError(t, err) + require.Equal(t, test.expected, buf.String()) + }) + } +} diff --git a/query/drop_index.go b/query/drop.go similarity index 53% rename from query/drop_index.go rename to query/drop.go index 8eb92261..c12d1765 100644 --- a/query/drop_index.go +++ b/query/drop.go @@ -7,6 +7,34 @@ import ( "github.com/asdine/genji/database" ) +// DropTableStmt is a DSL that allows creating a DROP TABLE query. +type DropTableStmt struct { + TableName string + IfExists bool +} + +// IsReadOnly always returns false. It implements the Statement interface. +func (stmt DropTableStmt) IsReadOnly() bool { + return false +} + +// Run runs the DropTable statement in the given transaction. +// It implements the Statement interface. +func (stmt DropTableStmt) Run(tx *database.Transaction, args []driver.NamedValue) (Result, error) { + var res Result + + if stmt.TableName == "" { + return res, errors.New("missing table name") + } + + err := tx.DropTable(stmt.TableName) + if err == database.ErrTableNotFound && stmt.IfExists { + err = nil + } + + return res, err +} + // DropIndexStmt is a DSL that allows creating a DROP INDEX query. type DropIndexStmt struct { IndexName string diff --git a/query/drop_table.go b/query/drop_table.go deleted file mode 100644 index 41c0c03a..00000000 --- a/query/drop_table.go +++ /dev/null @@ -1,36 +0,0 @@ -package query - -import ( - "database/sql/driver" - "errors" - - "github.com/asdine/genji/database" -) - -// DropTableStmt is a DSL that allows creating a DROP TABLE query. -type DropTableStmt struct { - TableName string - IfExists bool -} - -// IsReadOnly always returns false. It implements the Statement interface. -func (stmt DropTableStmt) IsReadOnly() bool { - return false -} - -// Run runs the DropTable statement in the given transaction. -// It implements the Statement interface. -func (stmt DropTableStmt) Run(tx *database.Transaction, args []driver.NamedValue) (Result, error) { - var res Result - - if stmt.TableName == "" { - return res, errors.New("missing table name") - } - - err := tx.DropTable(stmt.TableName) - if err == database.ErrTableNotFound && stmt.IfExists { - err = nil - } - - return res, err -} diff --git a/query/drop_test.go b/query/drop_test.go new file mode 100644 index 00000000..84908689 --- /dev/null +++ b/query/drop_test.go @@ -0,0 +1,40 @@ +package query_test + +import ( + "testing" + + "github.com/asdine/genji" + "github.com/asdine/genji/engine/memoryengine" + "github.com/stretchr/testify/require" +) + +func TestDrop(t *testing.T) { + tests := []struct { + name string + query string + fails bool + }{ + {"Drop table", "DROP TABLE test", false}, + {"Drop table If not exists", "DROP TABLE IF EXISTS test", false}, + {"Drop index", "DROP INDEX idx", false}, + {"Drop index if exists", "DROP INDEX IF EXISTS idx", false}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec("CREATE TABLE test; CREATE INDEX idx ON test (foo)") + require.NoError(t, err) + + err = db.Exec(test.query) + if test.fails { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/query/insert.go b/query/insert.go index fdbb05a2..ff520878 100644 --- a/query/insert.go +++ b/query/insert.go @@ -83,6 +83,17 @@ func (stmt InsertStmt) insertDocuments(t *database.Table, stack EvalStack) (Resu if err != nil { return res, err } + case KVPairs: + v, err := tp.Eval(stack) + if err != nil { + return res, err + } + d, err = v.Value.Value.DecodeToDocument() + if err != nil { + return res, err + } + default: + return res, fmt.Errorf("values must be a list of documents if field list is empty") } res.lastInsertKey, err = t.Insert(d) diff --git a/query/insert_test.go b/query/insert_test.go new file mode 100644 index 00000000..ea6a47cb --- /dev/null +++ b/query/insert_test.go @@ -0,0 +1,107 @@ +package query_test + +import ( + "bytes" + "database/sql" + "testing" + + "github.com/asdine/genji" + "github.com/asdine/genji/database" + "github.com/asdine/genji/document" + "github.com/asdine/genji/engine/memoryengine" + "github.com/stretchr/testify/require" +) + +func TestInsertStmt(t *testing.T) { + tests := []struct { + name string + query string + fails bool + expected string + params []interface{} + }{ + {"Values / No columns", `INSERT INTO test VALUES ("a", 'b', 'c')`, true, ``, nil}, + {"Values / With columns", `INSERT INTO test (a, b, c) VALUES ('a', 'b', 'c')`, false, "1,a,b,c\n", nil}, + {"Values / Ident", `INSERT INTO test (a) VALUES (a)`, true, ``, nil}, + {"Values / Ident string", `INSERT INTO test (a) VALUES ("a")`, true, ``, nil}, + {"Values / With fields ident string", `INSERT INTO test (a, "foo bar") VALUES ('c', 'd')`, false, "1,c,d\n", nil}, + {"Values / Positional Params", "INSERT INTO test (a, b, c) VALUES (?, 'e', ?)", false, "1,d,e,f\n", []interface{}{"d", "f"}}, + {"Values / Named Params", "INSERT INTO test (a, b, c) VALUES ($d, 'e', $f)", false, "1,d,e,f\n", []interface{}{sql.Named("f", "f"), sql.Named("d", "d")}}, + {"Values / Invalid params", "INSERT INTO test (a, b, c) VALUES ('d', ?)", true, "", []interface{}{'e'}}, + {"Values / List", `INSERT INTO test (a, b, c) VALUES ("a", 'b', (1, 2, 3))`, true, "", nil}, + {"Documents", "INSERT INTO test VALUES {a: 'a', b: 2.3, c: 1 = 1}", false, "1,a,2.3,true\n", nil}, + {"Documents / Positional Params", "INSERT INTO test VALUES {a: ?, b: 2.3, c: ?}", false, "1,a,2.3,true\n", []interface{}{"a", true}}, + {"Documents / Named Params", "INSERT INTO test VALUES {a: $a, b: 2.3, c: $c}", false, "1,1,2.3,true\n", []interface{}{sql.Named("c", true), sql.Named("a", 1)}}, + {"Documents / List ", "INSERT INTO test VALUES {a: (1, 2, 3)}", true, "", nil}, + {"Documents / strings", `INSERT INTO test VALUES {'a': 'a', b: 2.3}`, true, "", nil}, + {"Documents / double quotes", `INSERT INTO test VALUES {"a": "b"}`, false, "1,b\n", nil}, + } + + for _, test := range tests { + testFn := func(withIndexes bool) func(t *testing.T) { + return func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec("CREATE TABLE test") + require.NoError(t, err) + if withIndexes { + err = db.Exec(` + CREATE INDEX idx_a ON test (a); + CREATE INDEX idx_b ON test (b); + CREATE INDEX idx_c ON test (c); + `) + require.NoError(t, err) + } + err = db.Exec(test.query, test.params...) + if test.fails { + require.Error(t, err) + return + } + require.NoError(t, err) + + st, err := db.Query("SELECT key(), * FROM test") + require.NoError(t, err) + defer st.Close() + + var buf bytes.Buffer + err = document.IteratorToCSV(&buf, st) + require.NoError(t, err) + require.Equal(t, test.expected, buf.String()) + } + } + + t.Run("No Index/"+test.name, testFn(false)) + t.Run("With Index/"+test.name, testFn(true)) + } + + t.Run("with primary key", func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec("CREATE TABLE test (foo INTEGER PRIMARY KEY)") + require.NoError(t, err) + + err = db.Exec(`INSERT INTO test (bar) VALUES (1)`) + require.Error(t, err) + err = db.Exec(`INSERT INTO test (bar, foo) VALUES (1, 2)`) + require.NoError(t, err) + + err = db.Exec(`INSERT INTO test (bar, foo) VALUES (1, 2)`) + require.Equal(t, err, database.ErrDuplicateRecord) + }) + + t.Run("with shadowing", func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec("CREATE TABLE test") + require.NoError(t, err) + + err = db.Exec(`INSERT INTO test ("key()", "key") VALUES (1, 2)`) + require.NoError(t, err) + }) +} diff --git a/query/select_test.go b/query/select_test.go new file mode 100644 index 00000000..4e82f08d --- /dev/null +++ b/query/select_test.go @@ -0,0 +1,109 @@ +package query_test + +import ( + "bytes" + "database/sql" + "testing" + + "github.com/asdine/genji" + "github.com/asdine/genji/document" + "github.com/asdine/genji/engine/memoryengine" + "github.com/stretchr/testify/require" +) + +func TestSelectStmt(t *testing.T) { + tests := []struct { + name string + query string + fails bool + expected string + params []interface{} + }{ + {"No cond", "SELECT * FROM test", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\nfoo3,bar2,3\n", nil}, + {"Multiple wildcards cond", "SELECT *, *, a FROM test", false, "foo1,bar1,baz1,1,foo1,bar1,baz1,1,foo1\nfoo2,bar1,1,2,foo2,bar1,1,2,foo2\nfoo3,bar2,3,foo3,bar2,3\n", nil}, + {"With fields", "SELECT a, c FROM test", false, "foo1,baz1\nfoo2\n\n", nil}, + {"With eq cond", "SELECT * FROM test WHERE b = 'bar1'", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\n", nil}, + {"With neq cond", "SELECT * FROM test WHERE a != 'foo1'", false, "foo2,bar1,1,2\nfoo3,bar2,3\n", nil}, + {"With gt cond", "SELECT * FROM test WHERE b > 'bar1'", false, "", nil}, + {"With lt cond", "SELECT * FROM test WHERE a < 'zzzzz'", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\n", nil}, + {"With lte cond", "SELECT * FROM test WHERE a <= 'foo3'", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\n", nil}, + {"With field comparison", "SELECT * FROM test WHERE b < a", false, "foo1,bar1,baz1,1\nfoo2,bar1,1,2\n", nil}, + {"With limit", "SELECT * FROM test WHERE b = 'bar1' LIMIT 1", false, "foo1,bar1,baz1,1\n", nil}, + {"With offset", "SELECT *, key() FROM test WHERE b = 'bar1' OFFSET 1", false, "foo2,bar1,1,2,2\n", nil}, + {"With limit then offset", "SELECT * FROM test WHERE b = 'bar1' LIMIT 1 OFFSET 1", false, "foo2,bar1,1,2\n", nil}, + {"With offset then limit", "SELECT * FROM test WHERE b = 'bar1' OFFSET 1 LIMIT 1", true, "", nil}, + {"With positional params", "SELECT * FROM test WHERE a = ? OR d = ?", false, "foo1,bar1,baz1,1\nfoo3,bar2,3\n", []interface{}{"foo1", "foo3"}}, + {"With named params", "SELECT * FROM test WHERE a = $a OR d = $d", false, "foo1,bar1,baz1,1\nfoo3,bar2,3\n", []interface{}{sql.Named("a", "foo1"), sql.Named("d", "foo3")}}, + {"With key()", "SELECT key(), a FROM test", false, "1,foo1\n2,foo2\n3\n", []interface{}{sql.Named("a", "foo1"), sql.Named("d", "foo3")}}, + {"With pk in cond, gt", "SELECT * FROM test WHERE k > 0 AND e = 1", false, "foo2,bar1,1,2\n", nil}, + {"With pk in cond, =", "SELECT * FROM test WHERE k = 2.0 AND e = 1", false, "foo2,bar1,1,2\n", nil}, + } + + for _, test := range tests { + testFn := func(withIndexes bool) func(t *testing.T) { + return func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec("CREATE TABLE test (k INTEGER PRIMARY KEY)") + require.NoError(t, err) + if withIndexes { + err = db.Exec(` + CREATE INDEX idx_a ON test (a); + CREATE INDEX idx_b ON test (b); + CREATE INDEX idx_c ON test (c); + CREATE INDEX idx_d ON test (d); + `) + require.NoError(t, err) + } + + err = db.Exec("INSERT INTO test (k, a, b, c) VALUES (1, 'foo1', 'bar1', 'baz1')") + require.NoError(t, err) + err = db.Exec("INSERT INTO test (k, a, b, e) VALUES (2, 'foo2', 'bar1', 1)") + require.NoError(t, err) + err = db.Exec("INSERT INTO test (k, d, e) VALUES (3, 'foo3', 'bar2')") + require.NoError(t, err) + + st, err := db.Query(test.query, test.params...) + if test.fails { + require.Error(t, err) + return + } + require.NoError(t, err) + defer st.Close() + + var buf bytes.Buffer + err = document.IteratorToCSV(&buf, st) + require.NoError(t, err) + require.Equal(t, test.expected, buf.String()) + } + } + t.Run("No Index/"+test.name, testFn(false)) + t.Run("With Index/"+test.name, testFn(true)) + } + + t.Run("with primary key only", func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec("CREATE TABLE test (foo UINT8 PRIMARY KEY)") + require.NoError(t, err) + + err = db.Exec(`INSERT INTO test (foo, bar) VALUES (1, 'a')`) + err = db.Exec(`INSERT INTO test (foo, bar) VALUES (2, 'b')`) + err = db.Exec(`INSERT INTO test (foo, bar) VALUES (3, 'c')`) + err = db.Exec(`INSERT INTO test (foo, bar) VALUES (4, 'd')`) + require.NoError(t, err) + + st, err := db.Query("SELECT * FROM test WHERE foo < 400 AND foo >= 2") + require.NoError(t, err) + defer st.Close() + + var buf bytes.Buffer + err = document.IteratorToCSV(&buf, st) + require.NoError(t, err) + require.Equal(t, "b,2\nc,3\nd,4\n", buf.String()) + }) +} diff --git a/query/update_test.go b/query/update_test.go new file mode 100644 index 00000000..fd4d81d6 --- /dev/null +++ b/query/update_test.go @@ -0,0 +1,64 @@ +package query_test + +import ( + "bytes" + "database/sql" + "testing" + + "github.com/asdine/genji" + "github.com/asdine/genji/document" + "github.com/asdine/genji/engine/memoryengine" + "github.com/stretchr/testify/require" +) + +func TestUpdateStmt(t *testing.T) { + tests := []struct { + name string + query string + fails bool + expected string + params []interface{} + }{ + {"No cond", `UPDATE test SET a = 'boo'`, false, "boo,bar1,baz1\nboo,bar2\nfoo3,bar3\n", nil}, + {"No cond / with ident string", `UPDATE test SET "a" = 'boo'`, false, "boo,bar1,baz1\nboo,bar2\nfoo3,bar3\n", nil}, + {"No cond / with multiple idents", `UPDATE test SET a = c`, false, "baz1,bar1,baz1\nNULL,bar2\nfoo3,bar3\n", nil}, + {"No cond / with string", `UPDATE test SET 'a' = 'boo'`, true, "", nil}, + {"With cond", "UPDATE test SET a = 1, b = 2 WHERE a = 'foo2'", false, "foo1,bar1,baz1\n1,2\nfoo3,bar3\n", nil}, + {"Field not found", "UPDATE test SET a = 1, b = 2 WHERE a = f", false, "foo1,bar1,baz1\nfoo2,bar2\nfoo3,bar3\n", nil}, + {"Positional params", "UPDATE test SET a = ?, b = ? WHERE a = ?", false, "a,b,baz1\nfoo2,bar2\nfoo3,bar3\n", []interface{}{"a", "b", "foo1"}}, + {"Named params", "UPDATE test SET a = $a, b = $b WHERE a = $c", false, "a,b,baz1\nfoo2,bar2\nfoo3,bar3\n", []interface{}{sql.Named("b", "b"), sql.Named("a", "a"), sql.Named("c", "foo1")}}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db, err := genji.New(memoryengine.NewEngine()) + require.NoError(t, err) + defer db.Close() + + err = db.Exec("CREATE TABLE test") + require.NoError(t, err) + err = db.Exec("INSERT INTO test (a, b, c) VALUES ('foo1', 'bar1', 'baz1')") + require.NoError(t, err) + err = db.Exec("INSERT INTO test (a, b) VALUES ('foo2', 'bar2')") + require.NoError(t, err) + err = db.Exec("INSERT INTO test (d, e) VALUES ('foo3', 'bar3')") + require.NoError(t, err) + + err = db.Exec(test.query, test.params...) + if test.fails { + require.Error(t, err) + return + } + require.NoError(t, err) + + st, err := db.Query("SELECT * FROM test") + require.NoError(t, err) + defer st.Close() + + var buf bytes.Buffer + err = document.IteratorToCSV(&buf, st) + require.NoError(t, err) + require.Equal(t, test.expected, buf.String()) + }) + } +}