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 }