Repository: genjidb/genji
Pull request #149 (2020-08-13 20:29:21)

Add support for ReIndex statement
Fixes #25 


Patch:
diff --git a/database/table.go b/database/table.go
index 5ab9e9bc..02d1a3b2 100644
--- a/database/table.go
+++ b/database/table.go
@@ -589,3 +589,20 @@ func getParentValue(d document.Document, p document.ValuePath) (document.Value,
 
 	return p[:len(p)-1].GetValue(d)
 }
+
+// ReIndex all the indexes of the table.
+func (t *Table) ReIndex() error {
+	indexes, err := t.Indexes()
+	if err != nil {
+		return err
+	}
+
+	for _, idx := range indexes {
+		err = t.tx.ReIndex(idx.Opts.IndexName)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/sql/parser/parser.go b/sql/parser/parser.go
index 5a1748b3..e2b8232b 100644
--- a/sql/parser/parser.go
+++ b/sql/parser/parser.go
@@ -73,6 +73,8 @@ func (p *Parser) ParseStatement() (query.Statement, error) {
 		return p.parseDropStatement()
 	case scanner.EXPLAIN:
 		return p.parseExplainStatement()
+	case scanner.REINDEX:
+		return p.parseReIndexStatement()
 	}
 
 	return nil, newParseError(scanner.Tokstr(tok, lit), []string{
diff --git a/sql/parser/reindex.go b/sql/parser/reindex.go
new file mode 100644
index 00000000..05eff38b
--- /dev/null
+++ b/sql/parser/reindex.go
@@ -0,0 +1,21 @@
+package parser
+
+import (
+	"github.com/genjidb/genji/sql/query"
+	"github.com/genjidb/genji/sql/scanner"
+)
+
+// parseReindexStatement parses a reindex statement.
+// This function assumes the REINDEX token has already been consumed.
+func (p *Parser) parseReIndexStatement() (query.Statement, error) {
+	var stmt query.ReIndexStmt
+	var err error
+
+	tok, _, lit := p.ScanIgnoreWhitespace()
+	if tok == scanner.IDENT {
+		stmt.TableOrIndexName = lit
+	} else {
+		p.Unscan()
+	}
+	return stmt, err
+}
diff --git a/sql/query/reindex.go b/sql/query/reindex.go
new file mode 100644
index 00000000..3626b7e5
--- /dev/null
+++ b/sql/query/reindex.go
@@ -0,0 +1,37 @@
+package query
+
+import (
+	"github.com/genjidb/genji/database"
+	"github.com/genjidb/genji/sql/query/expr"
+)
+
+// ReIndexStmt is a DSL that allows creating a full REINDEX statement.
+type ReIndexStmt struct {
+	TableOrIndexName string
+}
+
+// IsReadOnly always returns false. It implements the Statement interface.
+func (stmt ReIndexStmt) IsReadOnly() bool {
+	return false
+}
+
+// Run runs the Reindex statement in the given transaction.
+// It implements the Statement interface.
+func (stmt ReIndexStmt) Run(tx *database.Transaction, args []expr.Param) (Result, error) {
+	var res Result
+
+	if stmt.TableOrIndexName == "" {
+		return res, tx.ReIndexAll()
+	}
+
+	t, err := tx.GetTable(stmt.TableOrIndexName)
+	if err == nil {
+		return res, t.ReIndex()
+	}
+	if err != database.ErrTableNotFound {
+		return res, err
+	}
+
+	err = tx.ReIndex(stmt.TableOrIndexName)
+	return res, err
+}
diff --git a/sql/scanner/token.go b/sql/scanner/token.go
index 73473442..0cb6093f 100644
--- a/sql/scanner/token.go
+++ b/sql/scanner/token.go
@@ -96,6 +96,7 @@ const (
 	ON
 	ORDER
 	PRIMARY
+	REINDEX
 	RENAME
 	SELECT
 	SET
@@ -195,6 +196,7 @@ var tokens = [...]string{
 	ON:      "ON",
 	ORDER:   "ORDER",
 	PRIMARY: "PRIMARY",
+	REINDEX: "REINDEX",
 	RENAME:  "RENAME",
 	SELECT:  "SELECT",
 	SET:     "SET",


Test patch:
diff --git a/database/table_test.go b/database/table_test.go
index 4f51a955..e8ceee6a 100644
--- a/database/table_test.go
+++ b/database/table_test.go
@@ -604,6 +604,94 @@ func TestTableTruncate(t *testing.T) {
 	})
 }
 
+func TestTableReIndex(t *testing.T) {
+	t.Run("Should succeed if table has no index", func(t *testing.T) {
+		tb, cleanup := newTestTable(t)
+		defer cleanup()
+
+		err := tb.ReIndex()
+		require.NoError(t, err)
+	})
+
+	t.Run("Should reindex the right indexes", func(t *testing.T) {
+		tx, cleanup := newTestDB(t)
+		defer cleanup()
+
+		err := tx.CreateTable("test1", nil)
+		require.NoError(t, err)
+		err = tx.CreateTable("test2", nil)
+		require.NoError(t, err)
+		tb1, err := tx.GetTable("test1")
+		require.NoError(t, err)
+		tb2, err := tx.GetTable("test2")
+		require.NoError(t, err)
+
+		for i := int64(0); i < 10; i++ {
+			doc := document.NewFieldBuffer().
+				Add("a", document.NewIntegerValue(i)).
+				Add("b", document.NewIntegerValue(i*10))
+			_, err = tb1.Insert(doc)
+			require.NoError(t, err)
+			_, err = tb2.Insert(doc)
+			require.NoError(t, err)
+		}
+
+		err = tx.CreateIndex(database.IndexConfig{
+			IndexName: "test1a",
+			TableName: "test1",
+			Path:      document.NewValuePath("a"),
+		})
+		require.NoError(t, err)
+		err = tx.CreateIndex(database.IndexConfig{
+			IndexName: "test1b",
+			TableName: "test1",
+			Path:      document.NewValuePath("b"),
+		})
+		require.NoError(t, err)
+		err = tx.CreateIndex(database.IndexConfig{
+			IndexName: "test2a",
+			TableName: "test2",
+			Path:      document.NewValuePath("a"),
+		})
+		require.NoError(t, err)
+		err = tx.CreateIndex(database.IndexConfig{
+			IndexName: "test2b",
+			TableName: "test2",
+			Path:      document.NewValuePath("b"),
+		})
+		require.NoError(t, err)
+
+		err = tb1.ReIndex()
+		require.NoError(t, err)
+
+		countIndexElems := func(idx *database.Index) int {
+			var i int
+			err = idx.AscendGreaterOrEqual(document.Value{Type: document.IntegerValue}, func(v, k []byte, isEqual bool) error {
+				i++
+				return nil
+			})
+			require.NoError(t, err)
+			return i
+		}
+
+		idx, err := tx.GetIndex("test1a")
+		require.NoError(t, err)
+		require.Equal(t, 10, countIndexElems(idx))
+
+		idx, err = tx.GetIndex("test1b")
+		require.NoError(t, err)
+		require.Equal(t, 10, countIndexElems(idx))
+
+		idx, err = tx.GetIndex("test2a")
+		require.NoError(t, err)
+		require.Equal(t, 0, countIndexElems(idx))
+
+		idx, err = tx.GetIndex("test2b")
+		require.NoError(t, err)
+		require.Equal(t, 0, countIndexElems(idx))
+	})
+}
+
 func TestTableIndexes(t *testing.T) {
 	t.Run("Should succeed if table has no indexes", func(t *testing.T) {
 		tb, cleanup := newTestTable(t)
diff --git a/sql/parser/reindex_test.go b/sql/parser/reindex_test.go
new file mode 100644
index 00000000..4bbd29ed
--- /dev/null
+++ b/sql/parser/reindex_test.go
@@ -0,0 +1,34 @@
+package parser
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji/sql/query"
+	"github.com/stretchr/testify/require"
+)
+
+func TestParserReIndex(t *testing.T) {
+	tests := []struct {
+		name     string
+		s        string
+		expected query.Statement
+		errored  bool
+	}{
+		{"All", "REINDEX", query.ReIndexStmt{}, false},
+		{"With ident", "REINDEX tableOrIndex", query.ReIndexStmt{TableOrIndexName: "tableOrIndex"}, false},
+		{"With extra", "REINDEX tableOrIndex tableOrIndex", nil, true},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			q, err := ParseQuery(test.s)
+			if test.errored {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			require.Len(t, q.Statements, 1)
+			require.EqualValues(t, test.expected, q.Statements[0])
+		})
+	}
+}
diff --git a/sql/query/reindex_test.go b/sql/query/reindex_test.go
new file mode 100644
index 00000000..79f5e3f3
--- /dev/null
+++ b/sql/query/reindex_test.go
@@ -0,0 +1,86 @@
+package query_test
+
+import (
+	"testing"
+
+	"github.com/genjidb/genji"
+	"github.com/genjidb/genji/document"
+	"github.com/stretchr/testify/require"
+)
+
+func TestReIndex(t *testing.T) {
+	tests := []struct {
+		name            string
+		query           string
+		expectReIndexed []string
+		fails           bool
+	}{
+		{"ReIndex all", `REINDEX`, []string{"idx_test1_a", "idx_test1_b", "idx_test2_a", "idx_test2_b"}, false},
+		{"ReIndex table", `REINDEX test2`, []string{"idx_test2_a", "idx_test2_b"}, false},
+		{"ReIndex index", `REINDEX idx_test1_a`, []string{"idx_test1_a"}, false},
+		{"ReIndex unknown", `REINDEX doesntexist`, []string{}, true},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			db, err := genji.Open(":memory:")
+			require.NoError(t, err)
+			defer db.Close()
+
+			err = db.Exec(`
+				CREATE TABLE test1;
+				CREATE TABLE test2;
+
+				INSERT INTO test1(a, b) VALUES (1, 'a'), (2, 'b');
+				INSERT INTO test2(a, b) VALUES (3, 'c'), (4, 'd');
+
+				CREATE INDEX idx_test1_a ON test1(a);
+				CREATE INDEX idx_test1_b ON test1(b);
+				CREATE INDEX idx_test2_a ON test2(a);
+				CREATE INDEX idx_test2_b ON test2(b);
+			`)
+			require.NoError(t, err)
+
+			err = db.Exec(test.query)
+			if test.fails {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+
+			err = db.View(func(tx *genji.Tx) error {
+				idxList, err := tx.ListIndexes()
+				require.NoError(t, err)
+
+				for _, cfg := range idxList {
+					idx, err := tx.GetIndex(cfg.IndexName)
+					require.NoError(t, err)
+
+					shouldBeIndexed := false
+					for _, name := range test.expectReIndexed {
+						if name == idx.Opts.IndexName {
+							shouldBeIndexed = true
+							break
+						}
+					}
+
+					i := 0
+					err = idx.AscendGreaterOrEqual(document.Value{}, func(val []byte, key []byte, isEqual bool) error {
+						i++
+						return nil
+					})
+					require.NoError(t, err)
+					if shouldBeIndexed {
+						require.Equal(t, 2, i)
+					} else {
+						require.Equal(t, 0, i)
+					}
+				}
+
+				return nil
+			})
+			require.NoError(t, err)
+		})
+	}
+
+}
diff --git a/sql/scanner/scanner_test.go b/sql/scanner/scanner_test.go
index 8ef7110a..1de3d983 100644
--- a/sql/scanner/scanner_test.go
+++ b/sql/scanner/scanner_test.go
@@ -144,6 +144,7 @@ func TestScanner_Scan(t *testing.T) {
 		{s: `OFFSET`, tok: scanner.OFFSET, raw: `OFFSET`},
 		{s: `ORDER`, tok: scanner.ORDER, raw: `ORDER`},
 		{s: `PRIMARY`, tok: scanner.PRIMARY, raw: `PRIMARY`},
+		{s: `REINDEX`, tok: scanner.REINDEX, raw: `REINDEX`},
 		{s: `RENAME`, tok: scanner.RENAME, raw: `RENAME`},
 		{s: `SELECT`, tok: scanner.SELECT, raw: `SELECT`},
 		{s: `SET`, tok: scanner.SET, raw: `SET`},