Merge pull request #4564 from dolthub/taylor/log-tf

Add basic `dolt_log` table function
This commit is contained in:
Taylor Bantle
2022-10-18 16:48:07 -07:00
committed by GitHub
4 changed files with 432 additions and 1 deletions

View File

@@ -835,6 +835,9 @@ func (p DoltDatabaseProvider) TableFunction(_ *sql.Context, name string) (sql.Ta
case "dolt_diff_summary":
dtf := &DiffSummaryTableFunction{}
return dtf, nil
case "dolt_log":
dtf := &LogTableFunction{}
return dtf, nil
}
return nil, sql.ErrTableFunctionNotFound.New(name)

View File

@@ -0,0 +1,260 @@
// Copyright 2022 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sqle
import (
"fmt"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/env/actions/commitwalk"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
)
var _ sql.TableFunction = (*LogTableFunction)(nil)
type LogTableFunction struct {
ctx *sql.Context
revisionExpr sql.Expression
database sql.Database
}
var logTableSchema = sql.Schema{
&sql.Column{Name: "commit_hash", Type: sql.Text},
&sql.Column{Name: "committer", Type: sql.Text},
&sql.Column{Name: "email", Type: sql.Text},
&sql.Column{Name: "date", Type: sql.Datetime},
&sql.Column{Name: "message", Type: sql.Text},
}
// NewInstance creates a new instance of TableFunction interface
func (ltf *LogTableFunction) NewInstance(ctx *sql.Context, db sql.Database, expressions []sql.Expression) (sql.Node, error) {
newInstance := &LogTableFunction{
ctx: ctx,
database: db,
}
node, err := newInstance.WithExpressions(expressions...)
if err != nil {
return nil, err
}
return node, nil
}
// Database implements the sql.Databaser interface
func (ltf *LogTableFunction) Database() sql.Database {
return ltf.database
}
// WithDatabase implements the sql.Databaser interface
func (ltf *LogTableFunction) WithDatabase(database sql.Database) (sql.Node, error) {
ltf.database = database
return ltf, nil
}
// FunctionName implements the sql.TableFunction interface
func (ltf *LogTableFunction) FunctionName() string {
return "dolt_log"
}
// Resolved implements the sql.Resolvable interface
func (ltf *LogTableFunction) Resolved() bool {
if ltf.revisionExpr != nil {
return ltf.revisionExpr.Resolved()
}
return true
}
// String implements the Stringer interface
func (ltf *LogTableFunction) String() string {
if ltf.revisionExpr != nil {
return fmt.Sprintf("DOLT_LOG(%s)", ltf.revisionExpr.String())
}
return "DOLT_LOG()"
}
// Schema implements the sql.Node interface.
func (ltf *LogTableFunction) Schema() sql.Schema {
return logTableSchema
}
// Children implements the sql.Node interface.
func (ltf *LogTableFunction) Children() []sql.Node {
return nil
}
// WithChildren implements the sql.Node interface.
func (ltf *LogTableFunction) WithChildren(children ...sql.Node) (sql.Node, error) {
if len(children) != 0 {
return nil, fmt.Errorf("unexpected children")
}
return ltf, nil
}
// CheckPrivileges implements the interface sql.Node.
func (ltf *LogTableFunction) CheckPrivileges(ctx *sql.Context, opChecker sql.PrivilegedOperationChecker) bool {
tblNames, err := ltf.database.GetTableNames(ctx)
if err != nil {
return false
}
var operations []sql.PrivilegedOperation
for _, tblName := range tblNames {
operations = append(operations, sql.NewPrivilegedOperation(ltf.database.Name(), tblName, "", sql.PrivilegeType_Select))
}
return opChecker.UserHasPrivileges(ctx, operations...)
}
// Expressions implements the sql.Expressioner interface.
func (ltf *LogTableFunction) Expressions() []sql.Expression {
exprs := []sql.Expression{}
if ltf.revisionExpr != nil {
exprs = append(exprs, ltf.revisionExpr)
}
return exprs
}
// WithExpressions implements the sql.Expressioner interface.
func (ltf *LogTableFunction) WithExpressions(expression ...sql.Expression) (sql.Node, error) {
if len(expression) < 0 || len(expression) > 1 {
return nil, sql.ErrInvalidArgumentNumber.New(ltf.FunctionName(), "0 or 1", len(expression))
}
for _, expr := range expression {
if !expr.Resolved() {
return nil, ErrInvalidNonLiteralArgument.New(ltf.FunctionName(), expr.String())
}
}
exLen := len(expression)
if exLen == 1 {
ltf.revisionExpr = expression[0]
}
// validate the expressions
if ltf.revisionExpr != nil {
if !sql.IsText(ltf.revisionExpr.Type()) {
return nil, sql.ErrInvalidArgumentDetails.New(ltf.FunctionName(), ltf.revisionExpr.String())
}
}
return ltf, nil
}
// RowIter implements the sql.Node interface
func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter, error) {
revisionVal, err := ltf.evaluateArguments()
if err != nil {
return nil, err
}
sqledb, ok := ltf.database.(Database)
if !ok {
return nil, fmt.Errorf("unexpected database type: %T", ltf.database)
}
sess := dsess.DSessFromSess(ctx.Session)
var commit *doltdb.Commit
if ltf.revisionExpr != nil {
cs, err := doltdb.NewCommitSpec(revisionVal)
if err != nil {
return nil, err
}
commit, err = sqledb.GetDoltDB().Resolve(ctx, cs, nil)
if err != nil {
return nil, err
}
} else {
// If revisionExpr not defined, use session head
commit, err = sess.GetHeadCommit(ctx, sqledb.name)
if err != nil {
return nil, err
}
}
return NewLogTableFunctionRowIter(ctx, sqledb.GetDoltDB(), commit)
}
// evaluateArguments returns revisionValStr.
// It evaluates the argument expressions to turn them into values this LogTableFunction
// can use. Note that this method only evals the expressions, and doesn't validate the values.
func (ltf *LogTableFunction) evaluateArguments() (string, error) {
if ltf.revisionExpr != nil {
revisionVal, err := ltf.revisionExpr.Eval(ltf.ctx, nil)
if err != nil {
return "", err
}
revisionValStr, ok := revisionVal.(string)
if !ok {
return "", fmt.Errorf("received '%v' when expecting revision string", revisionVal)
}
return revisionValStr, nil
}
return "", nil
}
//------------------------------------
// logTableFunctionRowIter
//------------------------------------
var _ sql.RowIter = (*logTableFunctionRowIter)(nil)
// logTableFunctionRowIter is a sql.RowIter implementation which iterates over each commit as if it's a row in the table.
type logTableFunctionRowIter struct {
child doltdb.CommitItr
}
func NewLogTableFunctionRowIter(ctx *sql.Context, ddb *doltdb.DoltDB, commit *doltdb.Commit) (*logTableFunctionRowIter, error) {
hash, err := commit.HashOf()
if err != nil {
return nil, err
}
child, err := commitwalk.GetTopologicalOrderIterator(ctx, ddb, hash)
if err != nil {
return nil, err
}
return &logTableFunctionRowIter{child}, nil
}
// Next retrieves the next row. It will return io.EOF if it's the last row.
// After retrieving the last row, Close will be automatically closed.
func (itr *logTableFunctionRowIter) Next(ctx *sql.Context) (sql.Row, error) {
h, cm, err := itr.child.Next(ctx)
if err != nil {
return nil, err
}
meta, err := cm.GetCommitMeta(ctx)
if err != nil {
return nil, err
}
return sql.NewRow(h.String(), meta.Name, meta.Email, meta.Time(), meta.Description), nil
}
func (itr *logTableFunctionRowIter) Close(_ *sql.Context) error {
return nil
}

View File

@@ -1168,6 +1168,28 @@ func TestDiffSummaryTableFunctionPrepared(t *testing.T) {
}
}
func TestLogTableFunction(t *testing.T) {
harness := newDoltHarness(t)
harness.Setup(setup.MydbData)
for _, test := range LogTableFunctionScriptTests {
harness.engine = nil
t.Run(test.Name, func(t *testing.T) {
enginetest.TestScript(t, harness, test)
})
}
}
func TestLogTableFunctionPrepared(t *testing.T) {
harness := newDoltHarness(t)
harness.Setup(setup.MydbData)
for _, test := range LogTableFunctionScriptTests {
harness.engine = nil
t.Run(test.Name, func(t *testing.T) {
enginetest.TestScriptPrepared(t, harness, test)
})
}
}
func TestCommitDiffSystemTable(t *testing.T) {
harness := newDoltHarness(t)
harness.Setup(setup.MydbData)

View File

@@ -749,7 +749,7 @@ func makeLargeInsert(sz int) string {
// DoltUserPrivTests are tests for Dolt-specific functionality that includes privilege checking logic.
var DoltUserPrivTests = []queries.UserPrivilegeTest{
{
Name: "dolt_diff table function privilege checking",
Name: "table function privilege checking",
SetUpScript: []string{
"CREATE TABLE mydb.test (pk BIGINT PRIMARY KEY);",
"CREATE TABLE mydb.test2 (pk BIGINT PRIMARY KEY);",
@@ -774,6 +774,13 @@ var DoltUserPrivTests = []queries.UserPrivilegeTest{
Query: "SELECT * FROM dolt_diff_summary('main~', 'main', 'test');",
ExpectedErr: sql.ErrDatabaseAccessDeniedForUser,
},
{
// Without access to the database, dolt_log should fail with a database access error
User: "tester",
Host: "localhost",
Query: "SELECT * FROM dolt_log('main');",
ExpectedErr: sql.ErrDatabaseAccessDeniedForUser,
},
{
// Grant single-table access to the underlying user table
User: "root",
@@ -844,6 +851,13 @@ var DoltUserPrivTests = []queries.UserPrivilegeTest{
Query: "SELECT COUNT(*) FROM dolt_diff_summary('main~', 'main');",
Expected: []sql.Row{{1}},
},
{
// After granting access to the entire db, dolt_log should work
User: "tester",
Host: "localhost",
Query: "SELECT COUNT(*) FROM dolt_log('main');",
Expected: []sql.Row{{4}},
},
{
// Revoke multi-table access
User: "root",
@@ -865,6 +879,13 @@ var DoltUserPrivTests = []queries.UserPrivilegeTest{
Query: "SELECT * FROM dolt_diff_summary('main~', 'main', 'test');",
ExpectedErr: sql.ErrDatabaseAccessDeniedForUser,
},
{
// After revoking access, dolt_log should fail
User: "tester",
Host: "localhost",
Query: "SELECT * FROM dolt_log('main');",
ExpectedErr: sql.ErrDatabaseAccessDeniedForUser,
},
{
// Grant global access to *.*
User: "root",
@@ -4919,6 +4940,131 @@ var DiffTableFunctionScriptTests = []queries.ScriptTest{
},
}
var LogTableFunctionScriptTests = []queries.ScriptTest{
{
Name: "invalid arguments",
SetUpScript: []string{
"create table t (pk int primary key, c1 varchar(20), c2 varchar(20));",
"call dolt_add('.')",
"set @Commit1 = dolt_commit('-am', 'creating table t');",
"insert into t values(1, 'one', 'two'), (2, 'two', 'three');",
"set @Commit2 = dolt_commit('-am', 'inserting into t');",
},
Assertions: []queries.ScriptTestAssertion{
{
Query: "SELECT * from dolt_log(@Commit1, 't');",
ExpectedErr: sql.ErrInvalidArgumentNumber,
},
{
Query: "SELECT * from dolt_log(null);",
ExpectedErr: sql.ErrInvalidArgumentDetails,
},
{
Query: "SELECT * from dolt_log(123);",
ExpectedErr: sql.ErrInvalidArgumentDetails,
},
{
Query: "SELECT * from dolt_log('fake-branch');",
ExpectedErrStr: "branch not found: fake-branch",
},
{
Query: "SELECT * from dolt_log(concat('fake', '-', 'branch'));",
ExpectedErr: sqle.ErrInvalidNonLiteralArgument,
},
{
Query: "SELECT * from dolt_log(hashof('main'));",
ExpectedErr: sqle.ErrInvalidNonLiteralArgument,
},
{
Query: "SELECT * from dolt_log(LOWER(@Commit2));",
ExpectedErr: sqle.ErrInvalidNonLiteralArgument,
},
},
},
{
Name: "basic case with one revision",
SetUpScript: []string{
"create table t (pk int primary key, c1 varchar(20), c2 varchar(20));",
"call dolt_add('.')",
"set @Commit1 = dolt_commit('-am', 'creating table t');",
"insert into t values(1, 'one', 'two'), (2, 'two', 'three');",
"set @Commit2 = dolt_commit('-am', 'inserting into t');",
"call dolt_checkout('-b', 'new-branch')",
"insert into t values (3, 'three', 'four');",
"set @Commit3 = dolt_commit('-am', 'inserting into t again');",
"call dolt_checkout('main')",
},
Assertions: []queries.ScriptTestAssertion{
{
Query: "SELECT count(*) from dolt_log();",
Expected: []sql.Row{{4}},
},
{
Query: "SELECT count(*) from dolt_log('main');",
Expected: []sql.Row{{4}},
},
{
Query: "SELECT count(*) from dolt_log(@Commit1);",
Expected: []sql.Row{{3}},
},
{
Query: "SELECT count(*) from dolt_log(@Commit2);",
Expected: []sql.Row{{4}},
},
{
Query: "SELECT count(*) from dolt_log(@Commit3);",
Expected: []sql.Row{{5}},
},
{
Query: "SELECT count(*) from dolt_log('new-branch');",
Expected: []sql.Row{{5}},
},
},
},
{
Name: "basic case with one revision, row content",
SetUpScript: []string{
"create table t (pk int primary key, c1 varchar(20), c2 varchar(20));",
"call dolt_add('.')",
"set @Commit1 = dolt_commit('-am', 'creating table t');",
"insert into t values(1, 'one', 'two'), (2, 'two', 'three');",
"set @Commit2 = dolt_commit('-am', 'inserting into t');",
"call dolt_checkout('-b', 'new-branch')",
"insert into t values (3, 'three', 'four');",
"set @Commit3 = dolt_commit('-am', 'inserting into t again', '--author', 'John Doe <johndoe@example.com>');",
"call dolt_checkout('main')",
},
Assertions: []queries.ScriptTestAssertion{
{
Query: "SELECT commit_hash = @Commit2, commit_hash = @Commit1, committer, email, message from dolt_log();",
Expected: []sql.Row{
{true, false, "billy bob", "bigbillieb@fake.horse", "inserting into t"},
{false, true, "billy bob", "bigbillieb@fake.horse", "creating table t"},
{false, false, "billy bob", "bigbillieb@fake.horse", "checkpoint enginetest database mydb"},
{false, false, "billy bob", "bigbillieb@fake.horse", "Initialize data repository"},
},
},
{
Query: "SELECT commit_hash = @Commit2, committer, email, message from dolt_log('main') limit 1;",
Expected: []sql.Row{{true, "billy bob", "bigbillieb@fake.horse", "inserting into t"}},
},
{
Query: "SELECT commit_hash = @Commit3, committer, email, message from dolt_log('new-branch') limit 1;",
Expected: []sql.Row{{true, "John Doe", "johndoe@example.com", "inserting into t again"}},
},
{
Query: "SELECT commit_hash = @Commit1, committer, email, message from dolt_log(@Commit1) limit 1;",
Expected: []sql.Row{{true, "billy bob", "bigbillieb@fake.horse", "creating table t"}},
},
},
},
}
var DiffSummaryTableFunctionScriptTests = []queries.ScriptTest{
{
Name: "invalid arguments",