github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/dolt_log_table_function.go (about)

     1  // Copyright 2022 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package sqle
    16  
    17  import (
    18  	"fmt"
    19  	"strings"
    20  
    21  	"github.com/dolthub/go-mysql-server/sql"
    22  	"github.com/dolthub/go-mysql-server/sql/types"
    23  	"gopkg.in/src-d/go-errors.v1"
    24  
    25  	"github.com/dolthub/dolt/go/cmd/dolt/cli"
    26  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    27  	"github.com/dolthub/dolt/go/libraries/doltcore/env/actions/commitwalk"
    28  	"github.com/dolthub/dolt/go/libraries/doltcore/merge"
    29  	"github.com/dolthub/dolt/go/libraries/doltcore/schema"
    30  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
    31  	"github.com/dolthub/dolt/go/store/hash"
    32  )
    33  
    34  const logTableDefaultRowCount = 10
    35  
    36  var _ sql.TableFunction = (*LogTableFunction)(nil)
    37  var _ sql.ExecSourceRel = (*LogTableFunction)(nil)
    38  
    39  type LogTableFunction struct {
    40  	ctx *sql.Context
    41  
    42  	revisionExprs    []sql.Expression
    43  	notRevisionExprs []sql.Expression
    44  	notRevisionStrs  []string
    45  	tableNames       []string
    46  
    47  	minParents  int
    48  	showParents bool
    49  	decoration  string
    50  
    51  	database sql.Database
    52  }
    53  
    54  var logTableSchema = sql.Schema{
    55  	&sql.Column{Name: "commit_hash", Type: types.Text},
    56  	&sql.Column{Name: "committer", Type: types.Text},
    57  	&sql.Column{Name: "email", Type: types.Text},
    58  	&sql.Column{Name: "date", Type: types.Datetime},
    59  	&sql.Column{Name: "message", Type: types.Text},
    60  }
    61  
    62  // NewInstance creates a new instance of TableFunction interface
    63  func (ltf *LogTableFunction) NewInstance(ctx *sql.Context, db sql.Database, expressions []sql.Expression) (sql.Node, error) {
    64  	newInstance := &LogTableFunction{
    65  		ctx:      ctx,
    66  		database: db,
    67  	}
    68  
    69  	node, err := newInstance.evalArguments(expressions...)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	return node, nil
    75  }
    76  
    77  // Database implements the sql.Databaser interface
    78  func (ltf *LogTableFunction) Database() sql.Database {
    79  	return ltf.database
    80  }
    81  
    82  func (ltf *LogTableFunction) DataLength(ctx *sql.Context) (uint64, error) {
    83  	numBytesPerRow := schema.SchemaAvgLength(ltf.Schema())
    84  	numRows, _, err := ltf.RowCount(ctx)
    85  	if err != nil {
    86  		return 0, err
    87  	}
    88  	return numBytesPerRow * numRows, nil
    89  }
    90  
    91  func (ltf *LogTableFunction) RowCount(_ *sql.Context) (uint64, bool, error) {
    92  	return logTableDefaultRowCount, false, nil
    93  }
    94  
    95  // WithDatabase implements the sql.Databaser interface
    96  func (ltf *LogTableFunction) WithDatabase(database sql.Database) (sql.Node, error) {
    97  	nltf := *ltf
    98  	nltf.database = database
    99  	return &nltf, nil
   100  }
   101  
   102  // Name implements the sql.TableFunction interface
   103  func (ltf *LogTableFunction) Name() string {
   104  	return "dolt_log"
   105  }
   106  
   107  // Resolved implements the sql.Resolvable interface
   108  func (ltf *LogTableFunction) Resolved() bool {
   109  	for _, expr := range ltf.revisionExprs {
   110  		return expr.Resolved()
   111  	}
   112  	return true
   113  }
   114  
   115  func (ltf *LogTableFunction) IsReadOnly() bool {
   116  	return true
   117  }
   118  
   119  // String implements the Stringer interface
   120  func (ltf *LogTableFunction) String() string {
   121  	return fmt.Sprintf("DOLT_LOG(%s)", ltf.getOptionsString())
   122  }
   123  
   124  func (ltf *LogTableFunction) getOptionsString() string {
   125  	var options []string
   126  
   127  	for _, expr := range ltf.revisionExprs {
   128  		options = append(options, expr.String())
   129  	}
   130  
   131  	for _, expr := range ltf.notRevisionStrs {
   132  		options = append(options, fmt.Sprintf("^%s", expr))
   133  	}
   134  
   135  	if ltf.minParents > 0 {
   136  		options = append(options, fmt.Sprintf("--%s %d", cli.MinParentsFlag, ltf.minParents))
   137  	}
   138  
   139  	if ltf.showParents {
   140  		options = append(options, fmt.Sprintf("--%s", cli.ParentsFlag))
   141  	}
   142  
   143  	if len(ltf.decoration) > 0 && ltf.decoration != "auto" {
   144  		options = append(options, fmt.Sprintf("--%s %s", cli.DecorateFlag, ltf.decoration))
   145  	}
   146  
   147  	if len(ltf.tableNames) > 0 {
   148  		options = append(options, "--tables", strings.Join(ltf.tableNames, ","))
   149  	}
   150  
   151  	return strings.Join(options, ", ")
   152  }
   153  
   154  // Schema implements the sql.Node interface.
   155  func (ltf *LogTableFunction) Schema() sql.Schema {
   156  	logSchema := logTableSchema
   157  
   158  	if ltf.showParents {
   159  		logSchema = append(logSchema, &sql.Column{Name: "parents", Type: types.Text})
   160  	}
   161  	if shouldDecorateWithRefs(ltf.decoration) {
   162  		logSchema = append(logSchema, &sql.Column{Name: "refs", Type: types.Text})
   163  	}
   164  
   165  	return logSchema
   166  }
   167  
   168  // Children implements the sql.Node interface.
   169  func (ltf *LogTableFunction) Children() []sql.Node {
   170  	return nil
   171  }
   172  
   173  // WithChildren implements the sql.Node interface.
   174  func (ltf *LogTableFunction) WithChildren(children ...sql.Node) (sql.Node, error) {
   175  	if len(children) != 0 {
   176  		return nil, fmt.Errorf("unexpected children")
   177  	}
   178  	return ltf, nil
   179  }
   180  
   181  // CheckPrivileges implements the interface sql.Node.
   182  func (ltf *LogTableFunction) CheckPrivileges(ctx *sql.Context, opChecker sql.PrivilegedOperationChecker) bool {
   183  	tblNames, err := ltf.database.GetTableNames(ctx)
   184  	if err != nil {
   185  		return false
   186  	}
   187  
   188  	var operations []sql.PrivilegedOperation
   189  	for _, tblName := range tblNames {
   190  		subject := sql.PrivilegeCheckSubject{Database: ltf.database.Name(), Table: tblName}
   191  		operations = append(operations, sql.NewPrivilegedOperation(subject, sql.PrivilegeType_Select))
   192  	}
   193  
   194  	return opChecker.UserHasPrivileges(ctx, operations...)
   195  }
   196  
   197  // Expressions implements the sql.Expressioner interface.
   198  func (ltf *LogTableFunction) Expressions() []sql.Expression {
   199  	return []sql.Expression{}
   200  }
   201  
   202  // getDoltArgs builds an argument string from sql expressions so that we can
   203  // later parse the arguments with the same util as the CLI
   204  func getDoltArgs(ctx *sql.Context, expressions []sql.Expression, name string) ([]string, error) {
   205  	var args []string
   206  
   207  	for _, expr := range expressions {
   208  		childVal, err := expr.Eval(ctx, nil)
   209  		if err != nil {
   210  			return nil, err
   211  		}
   212  
   213  		if !types.IsText(expr.Type()) {
   214  			return args, sql.ErrInvalidArgumentDetails.New(name, expr.String())
   215  		}
   216  
   217  		text, _, err := types.Text.Convert(childVal)
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  
   222  		if text != nil {
   223  			args = append(args, text.(string))
   224  		}
   225  	}
   226  
   227  	return args, nil
   228  }
   229  
   230  func (ltf *LogTableFunction) addOptions(expression []sql.Expression) error {
   231  	args, err := getDoltArgs(ltf.ctx, expression, ltf.Name())
   232  	if err != nil {
   233  		return err
   234  	}
   235  
   236  	apr, err := cli.CreateLogArgParser(true).Parse(args)
   237  	if err != nil {
   238  		return sql.ErrInvalidArgumentDetails.New(ltf.Name(), err.Error())
   239  	}
   240  
   241  	if notRevisionStrs, ok := apr.GetValueList(cli.NotFlag); ok {
   242  		ltf.notRevisionStrs = append(ltf.notRevisionStrs, notRevisionStrs...)
   243  	}
   244  
   245  	if tableNames, ok := apr.GetValueList(cli.TablesFlag); ok {
   246  		ltf.tableNames = append(ltf.tableNames, tableNames...)
   247  	}
   248  
   249  	minParents := apr.GetIntOrDefault(cli.MinParentsFlag, 0)
   250  	if apr.Contains(cli.MergesFlag) {
   251  		minParents = 2
   252  	}
   253  
   254  	ltf.minParents = minParents
   255  	ltf.showParents = apr.Contains(cli.ParentsFlag)
   256  
   257  	decorateOption := apr.GetValueOrDefault(cli.DecorateFlag, "auto")
   258  	switch decorateOption {
   259  	case "short", "full", "auto", "no":
   260  	default:
   261  		return ltf.invalidArgDetailsErr(fmt.Sprintf("invalid --decorate option: %s", decorateOption))
   262  	}
   263  	ltf.decoration = decorateOption
   264  
   265  	return nil
   266  }
   267  
   268  func (ltf *LogTableFunction) WithExpressions(exprs ...sql.Expression) (sql.Node, error) {
   269  	if len(exprs) != 0 {
   270  		return nil, sql.ErrInvalidChildrenNumber.New(0, len(exprs))
   271  	}
   272  	return ltf, nil
   273  }
   274  
   275  // evalArguments converts the input expressions into string literals and
   276  // formats them as function arguments.
   277  func (ltf *LogTableFunction) evalArguments(expression ...sql.Expression) (sql.Node, error) {
   278  	for _, expr := range expression {
   279  		if !expr.Resolved() {
   280  			return nil, ErrInvalidNonLiteralArgument.New(ltf.Name(), expr.String())
   281  		}
   282  		// prepared statements resolve functions beforehand, so above check fails
   283  		if _, ok := expr.(sql.FunctionExpression); ok {
   284  			return nil, ErrInvalidNonLiteralArgument.New(ltf.Name(), expr.String())
   285  		}
   286  	}
   287  
   288  	newLtf := *ltf
   289  	if err := newLtf.addOptions(expression); err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	// Gets revisions, excluding any flag-related expression
   294  	for i, ex := range expression {
   295  		if !strings.Contains(ex.String(), "--") && !(i > 0 && strings.Contains(expression[i-1].String(), "--")) {
   296  			exStr := strings.ReplaceAll(ex.String(), "'", "")
   297  			if strings.HasPrefix(exStr, "^") {
   298  				newLtf.notRevisionExprs = append(newLtf.notRevisionExprs, ex)
   299  			} else {
   300  				newLtf.revisionExprs = append(newLtf.revisionExprs, ex)
   301  			}
   302  		}
   303  	}
   304  
   305  	if err := newLtf.validateRevisionExpressions(); err != nil {
   306  		return nil, err
   307  	}
   308  
   309  	return &newLtf, nil
   310  }
   311  
   312  func (ltf *LogTableFunction) validateRevisionExpressions() error {
   313  	// We must convert the expressions to strings before making string comparisons
   314  	// For dolt_log('^main'), ltf.revisionExpr.String() = "'^main'"" and revisionStr = "^main"
   315  
   316  	revisionStrs, err := mustExpressionsToString(ltf.ctx, ltf.revisionExprs)
   317  	if err != nil {
   318  		return err
   319  	}
   320  	notRevisionStrs, err := mustExpressionsToString(ltf.ctx, ltf.notRevisionExprs)
   321  	if err != nil {
   322  		return err
   323  	}
   324  
   325  	for i, revisionStr := range revisionStrs {
   326  		if !types.IsText(ltf.revisionExprs[i].Type()) {
   327  			return ltf.invalidArgDetailsErr(ltf.revisionExprs[i].String())
   328  		}
   329  		if strings.Contains(revisionStr, "..") && (len(revisionStrs) > 1 || ltf.notRevisionExprs != nil || ltf.notRevisionStrs != nil) {
   330  			return ltf.invalidArgDetailsErr("revision cannot contain '..' or '...' if multiple revisions exist")
   331  		}
   332  	}
   333  
   334  	for i, notRevisionStr := range notRevisionStrs {
   335  		if !types.IsText(ltf.notRevisionExprs[i].Type()) {
   336  			return ltf.invalidArgDetailsErr(ltf.notRevisionExprs[i].String())
   337  		}
   338  		if strings.Contains(notRevisionStr, "..") {
   339  			return ltf.invalidArgDetailsErr("revision cannot contain both '..' or '...' and '^'")
   340  		}
   341  	}
   342  	for _, notRevStr := range ltf.notRevisionStrs {
   343  		if strings.Contains(notRevStr, "..") {
   344  			return ltf.invalidArgDetailsErr("--not revision cannot contain '..'")
   345  		}
   346  		if strings.HasPrefix(notRevStr, "^") {
   347  			return ltf.invalidArgDetailsErr("--not revision cannot contain '^'")
   348  		}
   349  	}
   350  
   351  	return nil
   352  }
   353  
   354  // mustExpressionsToString converts a slice of expressions to a slice of resolved strings.
   355  func mustExpressionsToString(ctx *sql.Context, expr []sql.Expression) ([]string, error) {
   356  	var valStrs []string
   357  
   358  	for _, ex := range expr {
   359  		valStr, err := expressionToString(ctx, ex)
   360  		if err != nil {
   361  			return nil, err
   362  		}
   363  
   364  		valStrs = append(valStrs, valStr)
   365  	}
   366  
   367  	return valStrs, nil
   368  }
   369  
   370  func expressionToString(ctx *sql.Context, expr sql.Expression) (string, error) {
   371  	val, err := expr.Eval(ctx, nil)
   372  	if err != nil {
   373  		return "", err
   374  	}
   375  
   376  	valStr, ok := val.(string)
   377  	if !ok {
   378  		return "", fmt.Errorf("received '%v' when expecting string", val)
   379  	}
   380  
   381  	return valStr, nil
   382  }
   383  
   384  func (ltf *LogTableFunction) invalidArgDetailsErr(reason string) *errors.Error {
   385  	return sql.ErrInvalidArgumentDetails.New(ltf.Name(), reason)
   386  }
   387  
   388  // RowIter implements the sql.Node interface
   389  func (ltf *LogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter, error) {
   390  	revisionValStrs, notRevisionValStrs, threeDot, err := ltf.evaluateArguments()
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  	notRevisionValStrs = append(notRevisionValStrs, ltf.notRevisionStrs...)
   395  
   396  	sqledb, ok := ltf.database.(dsess.SqlDatabase)
   397  	if !ok {
   398  		return nil, fmt.Errorf("unexpected database type: %T", ltf.database)
   399  	}
   400  
   401  	sess := dsess.DSessFromSess(ctx.Session)
   402  	var commit *doltdb.Commit
   403  
   404  	matchFunc := func(optCmt *doltdb.OptionalCommit) (bool, error) {
   405  		commit, ok := optCmt.ToCommit()
   406  		if !ok {
   407  			return false, nil
   408  		}
   409  
   410  		return commit.NumParents() >= ltf.minParents, nil
   411  	}
   412  
   413  	cHashToRefs, err := getCommitHashToRefs(ctx, sqledb.DbData().Ddb, ltf.decoration)
   414  	if err != nil {
   415  		return nil, err
   416  	}
   417  
   418  	var commits []*doltdb.Commit
   419  	if len(revisionValStrs) == 0 {
   420  		// If no revisions given, use session head
   421  		commit, err = sess.GetHeadCommit(ctx, sqledb.RevisionQualifiedName())
   422  		if err != nil {
   423  			return nil, err
   424  		}
   425  		commits = append(commits, commit)
   426  	}
   427  
   428  	dbName := sess.Session.GetCurrentDatabase()
   429  	headRef, err := sess.CWBHeadRef(ctx, dbName)
   430  	if err != nil {
   431  		return nil, err
   432  	}
   433  
   434  	for _, revisionStr := range revisionValStrs {
   435  		cs, err := doltdb.NewCommitSpec(revisionStr)
   436  		if err != nil {
   437  			return nil, err
   438  		}
   439  
   440  		optCmt, err := sqledb.DbData().Ddb.Resolve(ctx, cs, headRef)
   441  		if err != nil {
   442  			return nil, err
   443  		}
   444  		commit, ok = optCmt.ToCommit()
   445  		if err != nil {
   446  			return nil, doltdb.ErrGhostCommitEncountered
   447  		}
   448  
   449  		commits = append(commits, commit)
   450  	}
   451  
   452  	var notCommits []*doltdb.Commit
   453  	for _, notRevisionStr := range notRevisionValStrs {
   454  		cs, err := doltdb.NewCommitSpec(notRevisionStr)
   455  		if err != nil {
   456  			return nil, err
   457  		}
   458  
   459  		optCmt, err := sqledb.DbData().Ddb.Resolve(ctx, cs, headRef)
   460  		if err != nil {
   461  			return nil, err
   462  		}
   463  		notCommit, ok := optCmt.ToCommit()
   464  		if !ok {
   465  			return nil, doltdb.ErrGhostCommitEncountered
   466  		}
   467  
   468  		notCommits = append(notCommits, notCommit)
   469  	}
   470  
   471  	if threeDot {
   472  		mergeBase, err := merge.MergeBase(ctx, commits[0], commits[1])
   473  		if err != nil {
   474  			return nil, err
   475  		}
   476  
   477  		mergeCs, err := doltdb.NewCommitSpec(mergeBase.String())
   478  		if err != nil {
   479  			return nil, err
   480  		}
   481  
   482  		// Use merge base as excluding commit
   483  		optCmt, err := sqledb.DbData().Ddb.Resolve(ctx, mergeCs, nil)
   484  		if err != nil {
   485  			return nil, err
   486  		}
   487  		mergeCommit, ok := optCmt.ToCommit()
   488  		if !ok {
   489  			return nil, doltdb.ErrGhostCommitEncountered
   490  		}
   491  
   492  		notCommits = append(notCommits, mergeCommit)
   493  
   494  		return ltf.NewDotDotLogTableFunctionRowIter(ctx, sqledb.DbData().Ddb, commits, notCommits, matchFunc, cHashToRefs, ltf.tableNames)
   495  	}
   496  
   497  	if len(revisionValStrs) <= 1 && len(notRevisionValStrs) == 0 {
   498  		return ltf.NewLogTableFunctionRowIter(ctx, sqledb.DbData().Ddb, commits[0], matchFunc, cHashToRefs, ltf.tableNames)
   499  	}
   500  
   501  	return ltf.NewDotDotLogTableFunctionRowIter(ctx, sqledb.DbData().Ddb, commits, notCommits, matchFunc, cHashToRefs, ltf.tableNames)
   502  }
   503  
   504  // evaluateArguments returns revisionValStrs, notRevisionValStrs, and three dot boolean.
   505  // It evaluates the argument expressions to turn them into values this LogTableFunction
   506  // can use. Note that this method only evals the expressions, and doesn't validate the values.
   507  func (ltf *LogTableFunction) evaluateArguments() (revisionValStrs []string, notRevisionValStrs []string, threeDot bool, err error) {
   508  	for _, expr := range ltf.revisionExprs {
   509  		valStr, err := expressionToString(ltf.ctx, expr)
   510  		if err != nil {
   511  			return nil, nil, false, err
   512  		}
   513  
   514  		if strings.Contains(valStr, "..") {
   515  			if strings.Contains(valStr, "...") {
   516  				refs := strings.Split(valStr, "...")
   517  				return refs, nil, true, nil
   518  			}
   519  			refs := strings.Split(valStr, "..")
   520  			return []string{refs[1]}, []string{refs[0]}, false, nil
   521  		}
   522  
   523  		revisionValStrs = append(revisionValStrs, valStr)
   524  	}
   525  
   526  	for _, notExpr := range ltf.notRevisionExprs {
   527  		notValStr, err := expressionToString(ltf.ctx, notExpr)
   528  		if err != nil {
   529  			return nil, nil, false, err
   530  		}
   531  
   532  		if strings.HasPrefix(notValStr, "^") {
   533  			notValStr = strings.TrimPrefix(notValStr, "^")
   534  		}
   535  
   536  		notRevisionValStrs = append(notRevisionValStrs, notValStr)
   537  	}
   538  
   539  	return revisionValStrs, notRevisionValStrs, false, nil
   540  }
   541  
   542  func getCommitHashToRefs(ctx *sql.Context, ddb *doltdb.DoltDB, decoration string) (map[hash.Hash][]string, error) {
   543  	cHashToRefs := map[hash.Hash][]string{}
   544  
   545  	// Get all branches
   546  	branches, err := ddb.GetBranchesWithHashes(ctx)
   547  	if err != nil {
   548  		return nil, err
   549  	}
   550  	for _, b := range branches {
   551  		refName := b.Ref.String()
   552  		if decoration != "full" {
   553  			refName = b.Ref.GetPath() // trim out "refs/heads/"
   554  		}
   555  		cHashToRefs[b.Hash] = append(cHashToRefs[b.Hash], refName)
   556  	}
   557  
   558  	// Get all remote branches
   559  	remotes, err := ddb.GetRemotesWithHashes(ctx)
   560  	if err != nil {
   561  		return nil, err
   562  	}
   563  	for _, r := range remotes {
   564  		refName := r.Ref.String()
   565  		if decoration != "full" {
   566  			refName = r.Ref.GetPath() // trim out "refs/remotes/"
   567  		}
   568  		cHashToRefs[r.Hash] = append(cHashToRefs[r.Hash], refName)
   569  	}
   570  
   571  	// Get all tags
   572  	tags, err := ddb.GetTagsWithHashes(ctx)
   573  	if err != nil {
   574  		return nil, err
   575  	}
   576  	for _, t := range tags {
   577  		tagName := t.Tag.GetDoltRef().String()
   578  		if decoration != "full" {
   579  			tagName = t.Tag.Name // trim out "refs/tags/"
   580  		}
   581  		tagName = fmt.Sprintf("tag: %s", tagName)
   582  		cHashToRefs[t.Hash] = append(cHashToRefs[t.Hash], tagName)
   583  	}
   584  
   585  	return cHashToRefs, nil
   586  }
   587  
   588  //------------------------------------
   589  // logTableFunctionRowIter
   590  //------------------------------------
   591  
   592  var _ sql.RowIter = (*logTableFunctionRowIter)(nil)
   593  
   594  // logTableFunctionRowIter is a sql.RowIter implementation which iterates over each commit as if it's a row in the table.
   595  type logTableFunctionRowIter struct {
   596  	child       doltdb.CommitItr
   597  	showParents bool
   598  	decoration  string
   599  	cHashToRefs map[hash.Hash][]string
   600  	headHash    hash.Hash
   601  
   602  	tableNames []string
   603  }
   604  
   605  func (ltf *LogTableFunction) NewLogTableFunctionRowIter(ctx *sql.Context, ddb *doltdb.DoltDB, commit *doltdb.Commit, matchFn func(*doltdb.OptionalCommit) (bool, error), cHashToRefs map[hash.Hash][]string, tableNames []string) (*logTableFunctionRowIter, error) {
   606  	h, err := commit.HashOf()
   607  	if err != nil {
   608  		return nil, err
   609  	}
   610  
   611  	child, err := commitwalk.GetTopologicalOrderIterator(ctx, ddb, []hash.Hash{h}, matchFn)
   612  	if err != nil {
   613  		return nil, err
   614  	}
   615  
   616  	return &logTableFunctionRowIter{
   617  		child:       child,
   618  		showParents: ltf.showParents,
   619  		decoration:  ltf.decoration,
   620  		cHashToRefs: cHashToRefs,
   621  		headHash:    h,
   622  		tableNames:  tableNames,
   623  	}, nil
   624  }
   625  
   626  func (ltf *LogTableFunction) NewDotDotLogTableFunctionRowIter(ctx *sql.Context, ddb *doltdb.DoltDB, commits []*doltdb.Commit, excludingCommits []*doltdb.Commit, matchFn func(*doltdb.OptionalCommit) (bool, error), cHashToRefs map[hash.Hash][]string, tableNames []string) (*logTableFunctionRowIter, error) {
   627  	hashes := make([]hash.Hash, len(commits))
   628  	for i, commit := range commits {
   629  		h, err := commit.HashOf()
   630  		if err != nil {
   631  			return nil, err
   632  		}
   633  		hashes[i] = h
   634  	}
   635  
   636  	exHashes := make([]hash.Hash, len(excludingCommits))
   637  	for i, exCommit := range excludingCommits {
   638  		h, err := exCommit.HashOf()
   639  		if err != nil {
   640  			return nil, err
   641  		}
   642  		exHashes[i] = h
   643  	}
   644  
   645  	child, err := commitwalk.GetDotDotRevisionsIterator(ctx, ddb, hashes, ddb, exHashes, matchFn)
   646  	if err != nil {
   647  		return nil, err
   648  	}
   649  
   650  	var headHash hash.Hash
   651  
   652  	if len(hashes) == 1 {
   653  		headHash = hashes[0]
   654  	}
   655  
   656  	return &logTableFunctionRowIter{
   657  		child:       child,
   658  		showParents: ltf.showParents,
   659  		decoration:  ltf.decoration,
   660  		cHashToRefs: cHashToRefs,
   661  		headHash:    headHash,
   662  		tableNames:  tableNames,
   663  	}, nil
   664  }
   665  
   666  // Next retrieves the next row. It will return io.EOF if it's the last row.
   667  // After retrieving the last row, Close will be automatically closed.
   668  func (itr *logTableFunctionRowIter) Next(ctx *sql.Context) (sql.Row, error) {
   669  	var commitHash hash.Hash
   670  	var commit *doltdb.Commit
   671  	var optCmt *doltdb.OptionalCommit
   672  	var err error
   673  	for {
   674  		commitHash, optCmt, err = itr.child.Next(ctx)
   675  		if err != nil {
   676  			return nil, err
   677  		}
   678  		ok := false
   679  		commit, ok = optCmt.ToCommit()
   680  		if !ok {
   681  			return nil, doltdb.ErrGhostCommitEncountered
   682  		}
   683  
   684  		if itr.tableNames != nil {
   685  			if commit.NumParents() == 0 {
   686  				// if we're at the root commit, we continue without checking if any tables changed
   687  				// we expect EOF to be returned on the next call to Next(), but continue in case there are more commits
   688  				continue
   689  			}
   690  			optCmt, err := commit.GetParent(ctx, 0)
   691  			if err != nil {
   692  				return nil, err
   693  			}
   694  			parent0Cm, ok := optCmt.ToCommit()
   695  			if !ok {
   696  				return nil, doltdb.ErrGhostCommitEncountered
   697  			}
   698  
   699  			var parent1Cm *doltdb.Commit
   700  			if commit.NumParents() > 1 {
   701  				optCmt, err = commit.GetParent(ctx, 1)
   702  				if err != nil {
   703  					return nil, err
   704  				}
   705  				parent1Cm, ok = optCmt.ToCommit()
   706  				if !ok {
   707  					return nil, doltdb.ErrGhostCommitEncountered
   708  				}
   709  			}
   710  
   711  			parent0RV, err := parent0Cm.GetRootValue(ctx)
   712  			if err != nil {
   713  				return nil, err
   714  			}
   715  			var parent1RV doltdb.RootValue
   716  			if parent1Cm != nil {
   717  				parent1RV, err = parent1Cm.GetRootValue(ctx)
   718  				if err != nil {
   719  					return nil, err
   720  				}
   721  			}
   722  			childRV, err := commit.GetRootValue(ctx)
   723  			if err != nil {
   724  				return nil, err
   725  			}
   726  
   727  			didChange := false
   728  			for _, tableName := range itr.tableNames {
   729  				didChange, err = didTableChangeBetweenRootValues(ctx, childRV, parent0RV, parent1RV, tableName)
   730  				if err != nil {
   731  					return nil, err
   732  				}
   733  				if didChange {
   734  					break
   735  				}
   736  			}
   737  
   738  			if didChange {
   739  				break
   740  			}
   741  		} else {
   742  			break
   743  		}
   744  	}
   745  
   746  	meta, err := commit.GetCommitMeta(ctx)
   747  	if err != nil {
   748  		return nil, err
   749  	}
   750  
   751  	row := sql.NewRow(commitHash.String(), meta.Name, meta.Email, meta.Time(), meta.Description)
   752  
   753  	if itr.showParents {
   754  		prStr, err := getParentsString(ctx, commit)
   755  		if err != nil {
   756  			return nil, err
   757  		}
   758  		row = row.Append(sql.NewRow(prStr))
   759  	}
   760  
   761  	if shouldDecorateWithRefs(itr.decoration) {
   762  		branchNames := itr.cHashToRefs[commitHash]
   763  		isHead := itr.headHash == commitHash
   764  		row = row.Append(sql.NewRow(getRefsString(branchNames, isHead)))
   765  	}
   766  
   767  	return row, nil
   768  }
   769  
   770  func (itr *logTableFunctionRowIter) Close(_ *sql.Context) error {
   771  	return nil
   772  }
   773  
   774  func getRefsString(branchNames []string, isHead bool) string {
   775  	if len(branchNames) == 0 {
   776  		return ""
   777  	}
   778  	var refStr string
   779  	if isHead {
   780  		refStr += "HEAD -> "
   781  	}
   782  	refStr += strings.Join(branchNames, ", ")
   783  
   784  	return refStr
   785  }
   786  
   787  func getParentsString(ctx *sql.Context, cm *doltdb.Commit) (string, error) {
   788  	parents, err := cm.ParentHashes(ctx)
   789  	if err != nil {
   790  		return "", err
   791  	}
   792  
   793  	var prStr string
   794  	for i, h := range parents {
   795  		prStr += h.String()
   796  		if i < len(parents)-1 {
   797  			prStr += ", "
   798  		}
   799  	}
   800  
   801  	return prStr, nil
   802  }
   803  
   804  // Default ("auto") for the dolt_log table function is "no"
   805  func shouldDecorateWithRefs(decoration string) bool {
   806  	return decoration == "full" || decoration == "short"
   807  }
   808  
   809  // didTableChangeBetweenRootValues checks if the given table changed between the two given root values.
   810  func didTableChangeBetweenRootValues(ctx *sql.Context, child, parent0, parent1 doltdb.RootValue, tableName string) (bool, error) {
   811  	childHash, childOk, err := child.GetTableHash(ctx, tableName)
   812  	if err != nil {
   813  		return false, err
   814  	}
   815  	parent0Hash, parent0Ok, err := parent0.GetTableHash(ctx, tableName)
   816  	if err != nil {
   817  		return false, err
   818  	}
   819  	var parent1Hash hash.Hash
   820  	var parent1Ok bool
   821  	if parent1 != nil {
   822  		parent1Hash, parent1Ok, err = parent1.GetTableHash(ctx, tableName)
   823  		if err != nil {
   824  			return false, err
   825  		}
   826  	}
   827  
   828  	if parent1 == nil {
   829  		if !childOk && !parent0Ok {
   830  			return false, nil
   831  		} else if !childOk && parent0Ok {
   832  			return true, nil
   833  		} else if childOk && !parent0Ok {
   834  			return true, nil
   835  		} else {
   836  			return childHash != parent0Hash, nil
   837  		}
   838  	} else {
   839  		if !childOk && !parent0Ok && !parent1Ok {
   840  			return false, nil
   841  		} else if !childOk && parent0Ok && !parent1Ok {
   842  			return true, nil
   843  		} else if !childOk && !parent0Ok && parent1Ok {
   844  			return true, nil
   845  		} else if !childOk && parent0Ok && parent1Ok {
   846  			return true, nil
   847  		} else if childOk && !parent0Ok && !parent1Ok {
   848  			return true, nil
   849  		} else if childOk && !parent0Ok && parent1Ok {
   850  			return childHash != parent1Hash, nil
   851  		} else if childOk && parent0Ok && !parent1Ok {
   852  			return childHash != parent0Hash, nil
   853  		} else {
   854  			return childHash != parent0Hash || childHash != parent1Hash, nil
   855  		}
   856  	}
   857  }