github.com/hasnat/dolt/go@v0.0.0-20210628190320-9eb5d843fbb7/libraries/doltcore/diff/table_deltas.go (about) 1 // Copyright 2019 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 diff 16 17 import ( 18 "context" 19 "fmt" 20 "sort" 21 22 "github.com/dolthub/dolt/go/libraries/doltcore/doltdocs" 23 24 "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" 25 "github.com/dolthub/dolt/go/libraries/doltcore/env" 26 "github.com/dolthub/dolt/go/libraries/doltcore/schema" 27 "github.com/dolthub/dolt/go/libraries/utils/set" 28 "github.com/dolthub/dolt/go/store/hash" 29 "github.com/dolthub/dolt/go/store/types" 30 ) 31 32 type TableDiffType int 33 34 const ( 35 AddedTable TableDiffType = iota 36 ModifiedTable 37 RenamedTable 38 RemovedTable 39 ) 40 41 type DocDiffType int 42 43 const ( 44 AddedDoc DocDiffType = iota 45 ModifiedDoc 46 RemovedDoc 47 ) 48 49 type DocDiffs struct { 50 NumAdded int 51 NumModified int 52 NumRemoved int 53 DocToType map[string]DocDiffType 54 Docs []string 55 } 56 57 // NewDocDiffs returns DocDiffs for Dolt Docs between two roots. 58 func NewDocDiffs(ctx context.Context, older *doltdb.RootValue, newer *doltdb.RootValue, docs doltdocs.Docs) (*DocDiffs, error) { 59 var added []string 60 var modified []string 61 var removed []string 62 if older != nil { 63 if newer == nil { 64 a, m, r, err := DocsDiff(ctx, older, nil, docs) 65 if err != nil { 66 return nil, err 67 } 68 added = a 69 modified = m 70 removed = r 71 } else { 72 a, m, r, err := DocsDiff(ctx, older, newer, docs) 73 if err != nil { 74 return nil, err 75 } 76 added = a 77 modified = m 78 removed = r 79 } 80 } 81 var docNames []string 82 docNames = append(docNames, added...) 83 docNames = append(docNames, modified...) 84 docNames = append(docNames, removed...) 85 sort.Strings(docNames) 86 87 docsToType := make(map[string]DocDiffType) 88 for _, nt := range added { 89 docsToType[nt] = AddedDoc 90 } 91 92 for _, nt := range modified { 93 docsToType[nt] = ModifiedDoc 94 } 95 96 for _, nt := range removed { 97 docsToType[nt] = RemovedDoc 98 } 99 100 return &DocDiffs{len(added), len(modified), len(removed), docsToType, docNames}, nil 101 } 102 103 // Len returns the number of docs in a DocDiffs 104 func (nd *DocDiffs) Len() int { 105 return len(nd.Docs) 106 } 107 108 // GetDocDiffs retrieves staged and unstaged DocDiffs. 109 func GetDocDiffs(ctx context.Context, ddb *doltdb.DoltDB, rsr env.RepoStateReader, drw env.DocsReadWriter) (*DocDiffs, *DocDiffs, error) { 110 docsOnDisk, err := drw.GetDocsOnDisk() 111 if err != nil { 112 return nil, nil, err 113 } 114 115 workingRoot, err := env.WorkingRoot(ctx, ddb, rsr) 116 if err != nil { 117 return nil, nil, err 118 } 119 120 notStagedDocDiffs, err := NewDocDiffs(ctx, workingRoot, nil, docsOnDisk) 121 if err != nil { 122 return nil, nil, err 123 } 124 125 headRoot, err := env.HeadRoot(ctx, ddb, rsr) 126 if err != nil { 127 return nil, nil, err 128 } 129 130 stagedRoot, err := env.StagedRoot(ctx, ddb, rsr) 131 if err != nil { 132 return nil, nil, err 133 } 134 135 stagedDocDiffs, err := NewDocDiffs(ctx, headRoot, stagedRoot, docsOnDisk) 136 if err != nil { 137 return nil, nil, err 138 } 139 140 return stagedDocDiffs, notStagedDocDiffs, nil 141 } 142 143 // TableDelta represents the change of a single table between two roots. 144 // FromFKs and ToFKs contain Foreign Keys that constrain columns in this table, 145 // they do not contain Foreign Keys that reference this table. 146 type TableDelta struct { 147 FromName string 148 ToName string 149 FromTable *doltdb.Table 150 ToTable *doltdb.Table 151 FromFks []doltdb.ForeignKey 152 ToFks []doltdb.ForeignKey 153 ToFksParentSch map[string]schema.Schema 154 } 155 156 // GetTableDeltas returns a slice of TableDelta objects for each table that changed between fromRoot and toRoot. 157 // It matches tables across roots using the tag of the first primary key column in the table's schema. 158 func GetTableDeltas(ctx context.Context, fromRoot, toRoot *doltdb.RootValue) (deltas []TableDelta, err error) { 159 deltas, err = getKeylessDeltas(ctx, fromRoot, toRoot) 160 if err != nil { 161 return nil, err 162 } 163 164 fromTables := make(map[uint64]*doltdb.Table) 165 fromTableNames := make(map[uint64]string) 166 fromTableFKs := make(map[uint64][]doltdb.ForeignKey) 167 fromTableHashes := make(map[uint64]hash.Hash) 168 169 fromFKC, err := fromRoot.GetForeignKeyCollection(ctx) 170 if err != nil { 171 return nil, err 172 } 173 174 err = fromRoot.IterTables(ctx, func(name string, table *doltdb.Table, sch schema.Schema) (stop bool, err error) { 175 if schema.IsKeyless(sch) { 176 return 177 } 178 179 th, err := table.HashOf() 180 if err != nil { 181 return true, err 182 } 183 184 pkTag := getUniqueTag(sch) 185 fromTables[pkTag] = table 186 fromTableNames[pkTag] = name 187 fromTableHashes[pkTag] = th 188 fromTableFKs[pkTag], _ = fromFKC.KeysForTable(name) 189 return false, nil 190 }) 191 if err != nil { 192 return nil, err 193 } 194 195 toFKC, err := toRoot.GetForeignKeyCollection(ctx) 196 if err != nil { 197 return nil, err 198 } 199 200 err = toRoot.IterTables(ctx, func(name string, table *doltdb.Table, sch schema.Schema) (stop bool, err error) { 201 if schema.IsKeyless(sch) { 202 return 203 } 204 205 th, err := table.HashOf() 206 if err != nil { 207 return true, err 208 } 209 210 toFKs, _ := toFKC.KeysForTable(name) 211 toFksParentSch, err := getFkParentSchs(ctx, toRoot, toFKs...) 212 if err != nil { 213 return false, err 214 } 215 216 pkTag := getUniqueTag(sch) 217 oldName, ok := fromTableNames[pkTag] 218 219 if !ok { 220 deltas = append(deltas, TableDelta{ 221 ToName: name, 222 ToTable: table, 223 ToFks: toFKs, 224 ToFksParentSch: toFksParentSch, 225 }) 226 } else if oldName != name || 227 fromTableHashes[pkTag] != th || 228 !fkSlicesAreEqual(fromTableFKs[pkTag], toFKs) { 229 230 deltas = append(deltas, TableDelta{ 231 FromName: fromTableNames[pkTag], 232 ToName: name, 233 FromTable: fromTables[pkTag], 234 ToTable: table, 235 FromFks: fromTableFKs[pkTag], 236 ToFks: toFKs, 237 ToFksParentSch: toFksParentSch, 238 }) 239 } 240 241 if ok { 242 delete(fromTableNames, pkTag) // consume table name 243 } 244 245 return false, nil 246 }) 247 if err != nil { 248 return nil, err 249 } 250 251 // all unmatched tables in fromRoot must have been dropped 252 for pkTag, oldName := range fromTableNames { 253 deltas = append(deltas, TableDelta{ 254 FromName: oldName, 255 FromTable: fromTables[pkTag], 256 FromFks: fromTableFKs[pkTag], 257 }) 258 } 259 260 return deltas, nil 261 } 262 263 func GetStagedUnstagedTableDeltas(ctx context.Context, ddb *doltdb.DoltDB, rsr env.RepoStateReader) (staged, unstaged []TableDelta, err error) { 264 headRoot, err := env.HeadRoot(ctx, ddb, rsr) 265 if err != nil { 266 return nil, nil, doltdb.RootValueUnreadable{RootType: doltdb.HeadRoot, Cause: err} 267 } 268 269 stagedRoot, err := env.StagedRoot(ctx, ddb, rsr) 270 if err != nil { 271 return nil, nil, doltdb.RootValueUnreadable{RootType: doltdb.StagedRoot, Cause: err} 272 } 273 274 workingRoot, err := env.WorkingRoot(ctx, ddb, rsr) 275 if err != nil { 276 return nil, nil, doltdb.RootValueUnreadable{RootType: doltdb.WorkingRoot, Cause: err} 277 } 278 279 staged, err = GetTableDeltas(ctx, headRoot, stagedRoot) 280 if err != nil { 281 return nil, nil, err 282 } 283 284 unstaged, err = GetTableDeltas(ctx, stagedRoot, workingRoot) 285 if err != nil { 286 return nil, nil, err 287 } 288 289 return staged, unstaged, nil 290 } 291 292 // we don't have any stable identifier to a keyless table, have to do an n^2 match 293 // todo: this is a good reason to implement table tags 294 func getKeylessDeltas(ctx context.Context, fromRoot, toRoot *doltdb.RootValue) (deltas []TableDelta, err error) { 295 type fromTable struct { 296 tags *set.Uint64Set 297 tbl *doltdb.Table 298 hsh hash.Hash 299 } 300 301 fromTables := make(map[string]fromTable) 302 err = fromRoot.IterTables(ctx, func(name string, tbl *doltdb.Table, sch schema.Schema) (stop bool, err error) { 303 if !schema.IsKeyless(sch) { 304 return 305 } 306 307 h, err := tbl.HashOf() 308 if err != nil { 309 return false, err 310 } 311 312 fromTables[name] = fromTable{ 313 tags: set.NewUint64Set(sch.GetAllCols().Tags), 314 tbl: tbl, 315 hsh: h, 316 } 317 return 318 }) 319 if err != nil { 320 return nil, err 321 } 322 323 err = toRoot.IterTables(ctx, func(name string, tbl *doltdb.Table, sch schema.Schema) (stop bool, err error) { 324 if !schema.IsKeyless(sch) { 325 return 326 } 327 328 toTblHash, err := tbl.HashOf() 329 if err != nil { 330 return false, err 331 } 332 333 delta := TableDelta{ 334 ToName: name, 335 ToTable: tbl, 336 } 337 338 toTableTags := set.NewUint64Set(sch.GetAllCols().Tags) 339 for fromName, fromTbl := range fromTables { 340 341 // |tbl| and |fromTbl| have the same identity 342 // if they have column tags in common 343 if toTableTags.Intersection(fromTbl.tags).Size() > 0 { 344 345 // consume matched fromTable 346 delete(fromTables, fromName) 347 348 if toTblHash.Equal(fromTbl.hsh) { 349 // no diff, skip table 350 return 351 } 352 353 delta.FromName = fromName 354 delta.FromTable = fromTbl.tbl 355 break 356 } 357 } 358 359 // append if matched or unmatched 360 deltas = append(deltas, delta) 361 return 362 }) 363 if err != nil { 364 return nil, err 365 } 366 367 // all unmatched pairs are table drops 368 for name, fromPair := range fromTables { 369 deltas = append(deltas, TableDelta{ 370 FromName: name, 371 FromTable: fromPair.tbl, 372 }) 373 } 374 375 return deltas, nil 376 } 377 378 func getUniqueTag(sch schema.Schema) uint64 { 379 if schema.IsKeyless(sch) { 380 panic("keyless tables have no stable column tags") 381 } 382 return sch.GetPKCols().Tags[0] 383 } 384 385 func getFkParentSchs(ctx context.Context, root *doltdb.RootValue, fks ...doltdb.ForeignKey) (map[string]schema.Schema, error) { 386 schs := make(map[string]schema.Schema) 387 for _, toFk := range fks { 388 toRefTable, _, ok, err := root.GetTableInsensitive(ctx, toFk.ReferencedTableName) 389 if err != nil { 390 return nil, err 391 } 392 if !ok { 393 continue // as the schemas are for display-only, we can skip on any missing parents (they were deleted, etc.) 394 } 395 toRefSch, err := toRefTable.GetSchema(ctx) 396 if err != nil { 397 return nil, err 398 } 399 schs[toFk.ReferencedTableName] = toRefSch 400 } 401 return schs, nil 402 } 403 404 // IsAdd returns true if the table was added between the fromRoot and toRoot. 405 func (td TableDelta) IsAdd() bool { 406 return td.FromTable == nil && td.ToTable != nil 407 } 408 409 // IsDrop returns true if the table was dropped between the fromRoot and toRoot. 410 func (td TableDelta) IsDrop() bool { 411 return td.FromTable != nil && td.ToTable == nil 412 } 413 414 // IsRename return true if the table was renamed between the fromRoot and toRoot. 415 func (td TableDelta) IsRename() bool { 416 if td.IsAdd() || td.IsDrop() { 417 return false 418 } 419 return td.FromName != td.ToName 420 } 421 422 // CurName returns the most recent name of the table. 423 func (td TableDelta) CurName() string { 424 if td.ToName != "" { 425 return td.ToName 426 } 427 return td.FromName 428 } 429 430 func (td TableDelta) HasFKChanges() bool { 431 return !fkSlicesAreEqual(td.FromFks, td.ToFks) 432 } 433 434 // GetSchemas returns the table's schema at the fromRoot and toRoot, or schema.Empty if the table did not exist. 435 func (td TableDelta) GetSchemas(ctx context.Context) (from, to schema.Schema, err error) { 436 if td.FromTable != nil { 437 from, err = td.FromTable.GetSchema(ctx) 438 439 if err != nil { 440 return nil, nil, err 441 } 442 } else { 443 from = schema.EmptySchema 444 } 445 446 if td.ToTable != nil { 447 to, err = td.ToTable.GetSchema(ctx) 448 449 if err != nil { 450 return nil, nil, err 451 } 452 } else { 453 to = schema.EmptySchema 454 } 455 456 return from, to, nil 457 } 458 459 func (td TableDelta) IsKeyless(ctx context.Context) (bool, error) { 460 f, t, err := td.GetSchemas(ctx) 461 if err != nil { 462 return false, err 463 } 464 465 from, to := schema.IsKeyless(f), schema.IsKeyless(t) 466 467 if from && to { 468 return true, nil 469 } else if !from && !to { 470 return false, nil 471 } else { 472 return false, fmt.Errorf("mismatched keyless and keyed schemas for table %s", td.CurName()) 473 } 474 } 475 476 // GetMaps returns the table's row map at the fromRoot and toRoot, or and empty map if the table did not exist. 477 func (td TableDelta) GetMaps(ctx context.Context) (from, to types.Map, err error) { 478 if td.FromTable != nil { 479 from, err = td.FromTable.GetRowData(ctx) 480 if err != nil { 481 return from, to, err 482 } 483 } else { 484 from, _ = types.NewMap(ctx, td.ToTable.ValueReadWriter()) 485 } 486 487 if td.ToTable != nil { 488 to, err = td.ToTable.GetRowData(ctx) 489 if err != nil { 490 return from, to, err 491 } 492 } else { 493 to, _ = types.NewMap(ctx, td.FromTable.ValueReadWriter()) 494 } 495 496 return from, to, nil 497 } 498 499 func fkSlicesAreEqual(from, to []doltdb.ForeignKey) bool { 500 if len(from) != len(to) { 501 return false 502 } 503 504 sort.Slice(from, func(i, j int) bool { 505 return from[i].Name < from[j].Name 506 }) 507 sort.Slice(to, func(i, j int) bool { 508 return to[i].Name < to[j].Name 509 }) 510 511 for i := range from { 512 if !from[i].DeepEquals(to[i]) { 513 return false 514 } 515 } 516 return true 517 }