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

     1  // Copyright 2023 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  	"slices"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/dolthub/go-mysql-server/sql"
    24  	"github.com/dolthub/go-mysql-server/sql/types"
    25  
    26  	"github.com/dolthub/dolt/go/libraries/doltcore/ref"
    27  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
    28  	"github.com/dolthub/dolt/go/store/hash"
    29  )
    30  
    31  type ReflogTableFunction struct {
    32  	ctx            *sql.Context
    33  	database       sql.Database
    34  	refAndArgExprs []sql.Expression
    35  }
    36  
    37  var _ sql.TableFunction = (*ReflogTableFunction)(nil)
    38  var _ sql.ExecSourceRel = (*ReflogTableFunction)(nil)
    39  
    40  var reflogTableSchema = sql.Schema{
    41  	&sql.Column{Name: "ref", Type: types.LongText},
    42  	&sql.Column{Name: "ref_timestamp", Type: types.Timestamp, Nullable: true},
    43  	&sql.Column{Name: "commit_hash", Type: types.LongText},
    44  	&sql.Column{Name: "commit_message", Type: types.LongText},
    45  }
    46  
    47  func (rltf *ReflogTableFunction) NewInstance(ctx *sql.Context, database sql.Database, expressions []sql.Expression) (sql.Node, error) {
    48  	newInstance := &ReflogTableFunction{
    49  		ctx:      ctx,
    50  		database: database,
    51  	}
    52  
    53  	node, err := newInstance.WithExpressions(expressions...)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  
    58  	return node, nil
    59  }
    60  
    61  func (rltf *ReflogTableFunction) RowIter(ctx *sql.Context, row sql.Row) (sql.RowIter, error) {
    62  	sqlDb, ok := rltf.database.(dsess.SqlDatabase)
    63  	if !ok {
    64  		return nil, fmt.Errorf("unexpected database type: %T", rltf.database)
    65  	}
    66  
    67  	var refName string
    68  	showAll := false
    69  	for _, expr := range rltf.refAndArgExprs {
    70  		target, err := expr.Eval(ctx, row)
    71  		if err != nil {
    72  			return nil, fmt.Errorf("error evaluating expression (%s): %s",
    73  				expr.String(), err.Error())
    74  		}
    75  		targetStr, ok := target.(string)
    76  		if !ok {
    77  			return nil, fmt.Errorf("argument (%v) is not a string value, but a %T", target, target)
    78  		}
    79  
    80  		if targetStr == "--all" {
    81  			if showAll {
    82  				return nil, fmt.Errorf("error: multiple values provided for `all`")
    83  			}
    84  			showAll = true
    85  		} else {
    86  			if refName != "" {
    87  				return nil, fmt.Errorf("error: %s has too many positional arguments. Expected at most %d, found %d: %s",
    88  					rltf.Name(), 1, 2, rltf.refAndArgExprs)
    89  			}
    90  			refName = targetStr
    91  		}
    92  	}
    93  
    94  	ddb := sqlDb.DbData().Ddb
    95  	journal := ddb.ChunkJournal()
    96  	if journal == nil {
    97  		return sql.RowsToRowIter(), nil
    98  	}
    99  
   100  	previousCommitsByRef := make(map[string]string)
   101  	rows := make([]sql.Row, 0)
   102  	err := journal.IterateRoots(func(root string, timestamp *time.Time) error {
   103  		hashof := hash.Parse(root)
   104  		datasets, err := ddb.DatasetsByRootHash(ctx, hashof)
   105  		if err != nil {
   106  			return fmt.Errorf("unable to look up references for root hash %s: %s",
   107  				hashof.String(), err.Error())
   108  		}
   109  
   110  		return datasets.IterAll(ctx, func(id string, addr hash.Hash) error {
   111  			// Skip working set references (WorkingSetRefs can't always be resolved to commits)
   112  			if ref.IsWorkingSet(id) {
   113  				return nil
   114  			}
   115  
   116  			doltRef, err := ref.Parse(id)
   117  			if err != nil {
   118  				return err
   119  			}
   120  
   121  			// Skip any internal refs
   122  			if doltRef.GetType() == ref.InternalRefType {
   123  				return nil
   124  			}
   125  			// skip workspace refs by default
   126  			if doltRef.GetType() == ref.WorkspaceRefType {
   127  				if !showAll {
   128  					return nil
   129  				}
   130  			}
   131  
   132  			// If a ref expression to filter on was specified, see if we match the current ref
   133  			if refName != "" {
   134  				// If the caller has supplied a branch or tag name, without the fully qualified ref path,
   135  				// take the first match and use that as the canonical ref to filter on
   136  				if strings.HasSuffix(strings.ToLower(id), "/"+strings.ToLower(refName)) {
   137  					refName = id
   138  				}
   139  
   140  				// Skip refs that don't match the target we're looking for
   141  				if strings.ToLower(id) != strings.ToLower(refName) {
   142  					return nil
   143  				}
   144  			}
   145  
   146  			// Skip ref entries where the commit didn't change from the previous ref entry
   147  			if prev, ok := previousCommitsByRef[id]; ok && prev == addr.String() {
   148  				return nil
   149  			}
   150  
   151  			commit, err := ddb.ResolveCommitRefAtRoot(ctx, doltRef, hashof)
   152  			if err != nil {
   153  				return err
   154  			}
   155  			commitMeta, err := commit.GetCommitMeta(ctx)
   156  			if err != nil {
   157  				return err
   158  			}
   159  
   160  			// TODO: We should be able to pass in a nil *time.Time, but it
   161  			// currently triggers a problem in GMS' Time conversion logic.
   162  			// Passing a nil any value works correctly though.
   163  			var ts any = nil
   164  			if timestamp != nil {
   165  				ts = *timestamp
   166  			}
   167  
   168  			rows = append(rows, sql.Row{
   169  				id,                     // ref
   170  				ts,                     // ref_timestamp
   171  				addr.String(),          // commit_hash
   172  				commitMeta.Description, // commit_message
   173  			})
   174  			previousCommitsByRef[id] = addr.String()
   175  			return nil
   176  		})
   177  	})
   178  
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	// Reverse the results so that we return the most recent reflog entries first
   184  	slices.Reverse(rows)
   185  
   186  	return sql.RowsToRowIter(rows...), nil
   187  }
   188  
   189  func (rltf *ReflogTableFunction) Schema() sql.Schema {
   190  	return reflogTableSchema
   191  }
   192  
   193  func (rltf *ReflogTableFunction) Resolved() bool {
   194  	for _, expr := range rltf.refAndArgExprs {
   195  		if !expr.Resolved() {
   196  			return false
   197  		}
   198  	}
   199  	return true
   200  }
   201  
   202  func (rltf *ReflogTableFunction) String() string {
   203  	var args []string
   204  
   205  	for _, expr := range rltf.refAndArgExprs {
   206  		args = append(args, expr.String())
   207  	}
   208  	return fmt.Sprintf("DOLT_REFLOG(%s)", strings.Join(args, ", "))
   209  }
   210  
   211  func (rltf *ReflogTableFunction) Children() []sql.Node {
   212  	return nil
   213  }
   214  
   215  func (rltf *ReflogTableFunction) WithChildren(children ...sql.Node) (sql.Node, error) {
   216  	if len(children) != 0 {
   217  		return nil, fmt.Errorf("unexpected children")
   218  	}
   219  	return rltf, nil
   220  }
   221  
   222  func (rltf *ReflogTableFunction) CheckPrivileges(ctx *sql.Context, opChecker sql.PrivilegedOperationChecker) bool {
   223  	// Currently, we only support viewing the reflog for the HEAD ref of the current session,
   224  	// so no privileges need to be checked.
   225  	return true
   226  }
   227  
   228  func (rltf *ReflogTableFunction) IsReadOnly() bool {
   229  	return true
   230  }
   231  
   232  func (rltf *ReflogTableFunction) Expressions() []sql.Expression {
   233  	return rltf.refAndArgExprs
   234  }
   235  
   236  func (rltf *ReflogTableFunction) WithExpressions(expression ...sql.Expression) (sql.Node, error) {
   237  	if len(expression) > 2 {
   238  		return nil, sql.ErrInvalidArgumentNumber.New(rltf.Name(), "0 to 2", len(expression))
   239  	}
   240  
   241  	new := *rltf
   242  	new.refAndArgExprs = expression
   243  
   244  	return &new, nil
   245  }
   246  
   247  func (rltf *ReflogTableFunction) Name() string {
   248  	return "dolt_reflog"
   249  }
   250  
   251  // Database implements the sql.Databaser interface
   252  func (rltf *ReflogTableFunction) Database() sql.Database {
   253  	return rltf.database
   254  }
   255  
   256  // WithDatabase implements the sql.Databaser interface
   257  func (rltf *ReflogTableFunction) WithDatabase(database sql.Database) (sql.Node, error) {
   258  	new := *rltf
   259  	new.database = database
   260  	return &new, nil
   261  }