mirror of
https://github.com/dolthub/dolt.git
synced 2026-02-28 18:30:02 -06:00
Merge pull request #4564 from dolthub/taylor/log-tf
Add basic `dolt_log` table function
This commit is contained in:
@@ -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)
|
||||
|
||||
260
go/libraries/doltcore/sqle/dolt_log_table_function.go
Normal file
260
go/libraries/doltcore/sqle/dolt_log_table_function.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user