Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5ba5e38
test: dolt_reset --hard preserves untracked tables with tag collisions
elianddb May 11, 2026
ba3dbeb
fix: retag colliding columns when moving untracked tables on reset --…
elianddb May 11, 2026
ef677bd
test: add regression tests for untracked table preservation
elianddb May 12, 2026
e6ad5a1
refactor(schema): centralize column tag updates in WithUpdatedColumnTags
elianddb May 12, 2026
f65cc1e
fix: preserve and retag untracked tables across root transitions
elianddb May 12, 2026
4d9e88b
fix: carry untracked tables on sql checkout and rebase
elianddb May 12, 2026
5445795
test: cover uncommitted-table carry across checkout and reset
elianddb May 13, 2026
3091946
schema: add WithRemappedColumnTags, WithUpdatedColumnTag, and RemapTa…
elianddb May 13, 2026
ffaba86
schcmds, dprocedures: dedup column-tag update via schema.WithUpdatedC…
elianddb May 13, 2026
7b857b1
actions: carry uncommitted tables across checkout and reset
elianddb May 13, 2026
e01bac6
actions: exclude system and ignored tables from uncommitted carry
elianddb May 13, 2026
44503bf
schema: trim helper docs and tighten consumer error wrapping
elianddb May 18, 2026
2e569b9
actions: add CarryPolicy and split overwrite error by table state
elianddb May 18, 2026
c0ac0d7
dprocedures: keep non-move sql checkout session-local
elianddb May 18, 2026
635212b
bats: add checkout ignored tables stay on source test
elianddb May 18, 2026
eae044e
actions: carry writable system tables and allow identical-state checkout
elianddb May 20, 2026
5be427f
amend tests that expect mysql dialect
elianddb May 20, 2026
de010ba
schema: add index properties accessor and tidy tag remap docs
elianddb May 22, 2026
68d8564
doltdb: note dolt_ignore conflict tie-break todo
elianddb May 22, 2026
f82ee8d
actions: thread ignored table set through carry and checkout
elianddb May 22, 2026
4662fe9
tests: cover uncommitted table carry on checkout and reset
elianddb May 22, 2026
e7013ac
doltdb: add RootsStatus and lift DiffTableHashes from merge
elianddb May 27, 2026
5a151d9
[ga-format-pr] Run go/utils/repofmt/format_repo.sh and go/Godeps/upda…
elianddb May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go/cmd/dolt/commands/schcmds/copy-tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ func updateRootWithNewColumnTag(ctx context.Context, root doltdb.RootValue, tabl
}

// Update the column tag in the schema
updatedSchema, err := updateColumnTag(sch, columnName, tag)
updatedSchema, err := schema.WithUpdatedColumnTag(sch, columnName, tag)
if err != nil {
return nil, err
}
Expand Down
38 changes: 3 additions & 35 deletions go/cmd/dolt/commands/schcmds/update-tag.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package schcmds

import (
"context"
"fmt"
"strconv"

"github.com/dolthub/dolt/go/cmd/dolt/cli"
Expand Down Expand Up @@ -88,18 +87,18 @@ func (cmd UpdateTagCmd) Exec(ctx context.Context, commandStr string, args []stri

tbl, tName, ok, err := doltdb.GetTableInsensitive(ctx, root, doltdb.TableName{Name: tableName})
if err != nil {
return commands.HandleVErrAndExitCode(errhand.BuildDError("failed to get table").Build(), usage)
return commands.HandleVErrAndExitCode(errhand.BuildDError("failed to get table").AddCause(err).Build(), usage)
}
if !ok {
return commands.HandleVErrAndExitCode(errhand.BuildDError("table %s does not exist", tableName).Build(), usage)
}

sch, err := tbl.GetSchema(ctx)
if err != nil {
return commands.HandleVErrAndExitCode(errhand.BuildDError("failed to get schema").Build(), usage)
return commands.HandleVErrAndExitCode(errhand.BuildDError("failed to get schema").AddCause(err).Build(), usage)
}

newSch, err := updateColumnTag(sch, columnName, tag)
newSch, err := schema.WithUpdatedColumnTag(sch, columnName, tag)
if err != nil {
return commands.HandleVErrAndExitCode(errhand.BuildDError("failed to update column tag").AddCause(err).Build(), usage)
}
Expand All @@ -121,34 +120,3 @@ func (cmd UpdateTagCmd) Exec(ctx context.Context, commandStr string, args []stri

return commands.HandleVErrAndExitCode(nil, usage)
}

func updateColumnTag(sch schema.Schema, name string, tag uint64) (schema.Schema, error) {
var found bool
columns := sch.GetAllCols().GetColumns()
// Find column and update its tag
for i, col := range columns {
if col.Name == name {
col.Tag = tag
columns[i] = col
found = true
break
}
}

if !found {
return nil, fmt.Errorf("column %s does not exist", name)
}

newSch, err := schema.SchemaFromCols(schema.NewColCollection(columns...))
if err != nil {
return nil, err
}

err = newSch.SetPkOrdinals(sch.GetPkOrdinals())
if err != nil {
return nil, err
}
newSch.SetCollation(sch.GetCollation())

return newSch, nil
}
23 changes: 23 additions & 0 deletions go/libraries/doltcore/doltdb/root_val.go
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,29 @@ func (root *rootValue) DebugString(ctx context.Context, transitive bool) string
return buf.String()
}

// DiffTableHashes returns the table-level changes between |from| and |to|, both produced by
// [MapTableHashes] on two roots. Modified and added tables map to their |to| hash. Deleted
// tables map to the zero hash rather than being omitted, so two diffs computed against the
// same baseline compare equal under [maps.Equal] when the same tables were dropped.
func DiffTableHashes(from, to map[TableName]hash.Hash) map[TableName]hash.Hash {
diffs := make(map[TableName]hash.Hash)
for name, fromHash := range from {
if toHash, ok := to[name]; ok {
if toHash != fromHash {
diffs[name] = toHash
}
} else {
diffs[name] = hash.Hash{}
}
}
for name, toHash := range to {
if _, ok := from[name]; !ok {
diffs[name] = toHash
}
}
return diffs
}

// MapTableHashes returns a map of each table name and hash.
func MapTableHashes(ctx context.Context, root RootValue) (map[TableName]hash.Hash, error) {
names, err := UnionTableNames(ctx, root)
Expand Down
95 changes: 95 additions & 0 deletions go/libraries/doltcore/doltdb/roots_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2026 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 doltdb

import (
"context"
"maps"

"github.com/dolthub/dolt/go/store/hash"
)

// RootsStatus holds a [Roots] bundle's staged and unstaged table diffs. Construct with
// [NewRootsStatus]; a nil value means the branch has no uncommitted changes.
type RootsStatus struct {
// Head holds the committed table hashes, used to classify entries in Staged and Unstaged.
Head map[TableName]hash.Hash
// Staged is the head-to-staged diff: tables where staged differs from head.
Staged map[TableName]hash.Hash
// Unstaged is the staged-to-working diff: tables where working differs from staged.
Unstaged map[TableName]hash.Hash
}

// NewRootsStatus returns |roots|' staged and unstaged table diffs, or nil when both are
// empty. Read-only system tables and tables in |ignored| are excluded.
func NewRootsStatus(ctx context.Context, roots Roots, ignored *TableNameSet) (*RootsStatus, error) {
head, err := userTableHashes(ctx, roots.Head, ignored)
if err != nil {
return nil, err
}
stagedHashes, err := userTableHashes(ctx, roots.Staged, ignored)
if err != nil {
return nil, err
}
workingHashes, err := userTableHashes(ctx, roots.Working, ignored)
if err != nil {
return nil, err
}
staged := DiffTableHashes(head, stagedHashes)
unstaged := DiffTableHashes(stagedHashes, workingHashes)
if len(staged) == 0 && len(unstaged) == 0 {
return nil, nil
}
return &RootsStatus{Head: head, Staged: staged, Unstaged: unstaged}, nil
}

// Added returns staged tables that are absent from head.
func (s *RootsStatus) Added() []TableName {
var out []TableName
for name := range s.Staged {
if _, inHead := s.Head[name]; !inHead {
out = append(out, name)
}
}
return out
}

// Untracked returns unstaged tables that are absent from both staged and head.
func (s *RootsStatus) Untracked() []TableName {
var out []TableName
for name := range s.Unstaged {
if _, inHead := s.Head[name]; inHead {
continue
}
if _, inStaged := s.Staged[name]; inStaged {
continue
}
out = append(out, name)
}
return out
}

// userTableHashes returns |root|'s table hashes excluding read-only system tables and any
// table named in |ignored|.
func userTableHashes(ctx context.Context, root RootValue, ignored *TableNameSet) (map[TableName]hash.Hash, error) {
hashes, err := MapTableHashes(ctx, root)
if err != nil {
return nil, err
}
maps.DeleteFunc(hashes, func(name TableName, _ hash.Hash) bool {
return IsReadOnlySystemTable(name) || ignored.Contains(name)
})
return hashes, nil
}
180 changes: 180 additions & 0 deletions go/libraries/doltcore/env/actions/carry_tables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
// Copyright 2026 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 actions

import (
"context"
"fmt"
"slices"
"strings"

"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/schema"
"github.com/dolthub/dolt/go/store/types"
)

// CarryTablesAbsentFromBaseline copies tables that exist in |src| but not in |baseline| into
// |dest|, resolving column tag collisions and carrying matching foreign keys. Tables already
// on |dest| are left alone. Names in |exclude| are skipped; pass nil to carry all.
func CarryTablesAbsentFromBaseline(ctx context.Context, src, baseline, dest doltdb.RootValue, exclude *doltdb.TableNameSet) (doltdb.RootValue, error) {
srcSchemas, err := doltdb.GetAllSchemas(ctx, src)
if err != nil {
return nil, err
}
destSchemas, err := doltdb.GetAllSchemas(ctx, dest)
if err != nil {
return nil, err
}

absent, err := tablesAbsentFromBaseline(ctx, srcSchemas, baseline, exclude)
if err != nil {
return nil, err
}

dest, carried, tagRemaps, err := carryTables(ctx, src, dest, absent, destSchemas)
if err != nil {
return nil, err
}

if len(carried) == 0 {
return dest, nil
}

return carryForeignKeys(ctx, src, dest, carried, tagRemaps, srcSchemas, destSchemas)
}

// tablesAbsentFromBaseline returns the entries of |srcSchemas| not present in |baseline|,
// excluding read-only dolt system tables and any names in |exclude|.
func tablesAbsentFromBaseline(ctx context.Context, srcSchemas map[doltdb.TableName]schema.Schema, baseline doltdb.RootValue, exclude *doltdb.TableNameSet) (map[doltdb.TableName]schema.Schema, error) {
baselineNames, err := baseline.GetAllTableNames(ctx, false)
if err != nil {
return nil, err
}
baselineSet := doltdb.NewTableNameSet(baselineNames)

absent := make(map[doltdb.TableName]schema.Schema, len(srcSchemas))
for name, sch := range srcSchemas {
if baselineSet.Contains(name) || doltdb.IsReadOnlySystemTable(name) {
continue
}
if exclude != nil && exclude.Contains(name) {
continue
}
absent[name] = sch
}
return absent, nil
}

// carryTables copies each entry of |toCarry| from |src| into |dest|, retagging columns whose
// tag collides with one held by |dest| or by an earlier carried table. Names already on |dest|
// are skipped so the dest version wins. Returns the updated |dest|, the names that were
// carried, and the per-table tag remaps so the caller can fix up foreign keys.
func carryTables(ctx context.Context, src, dest doltdb.RootValue, toCarry, destSchemas map[doltdb.TableName]schema.Schema) (doltdb.RootValue, []doltdb.TableName, map[doltdb.TableName]map[uint64]uint64, error) {
heldTags := make(schema.TagMapping)
for tblName, tblSch := range destSchemas {
for _, t := range tblSch.GetAllCols().Tags {
heldTags.Add(t, tblName.Name)
}
}

allTagRemaps := make(map[doltdb.TableName]map[uint64]uint64)
var carried []doltdb.TableName
// Sort so retags reproduce across runs. Each table's new tags depend on tables
// carried before it.
names := make([]doltdb.TableName, 0, len(toCarry))
for name := range toCarry {
names = append(names, name)
}
slices.SortFunc(names, func(a, b doltdb.TableName) int { return strings.Compare(a.String(), b.String()) })
for _, name := range names {
if _, exists := destSchemas[name]; exists {
continue
}

tbl, exists, err := src.GetTable(ctx, name)
if err != nil {
return nil, nil, nil, err
}
if !exists {
return nil, nil, nil, fmt.Errorf("table %q does not exist in src root", name.String())
}

sch := toCarry[name]
columns := sch.GetAllCols().GetColumns()
tagRemap := make(map[uint64]uint64)
priorKinds := make([]types.NomsKind, 0, len(columns))
for _, column := range columns {
if heldTags.Contains(column.Tag) {
newTag := schema.AutoGenerateTag(heldTags, name.Name, priorKinds, column.Name, column.Kind)
tagRemap[column.Tag] = newTag
column.Tag = newTag
}
priorKinds = append(priorKinds, column.Kind)
heldTags.Add(column.Tag, name.Name)
}

if len(tagRemap) > 0 {
allTagRemaps[name] = tagRemap
newSch, err := schema.WithRemappedColumnTags(sch, tagRemap)
if err != nil {
return nil, nil, nil, err
}
tbl, err = tbl.UpdateSchema(ctx, newSch)
if err != nil {
return nil, nil, nil, err
}
}
dest, err = dest.PutTable(ctx, name, tbl)
if err != nil {
return nil, nil, nil, err
}
carried = append(carried, name)
}
return dest, carried, allTagRemaps, nil
}

// carryForeignKeys copies foreign keys from |src| whose child table is in |carried| into
// |dest|, applying |tagRemaps| to child columns. Parent column tags are re-resolved by
// column name via |destSchemas| when the parent already lives on |dest|. An existing key
// of the same name on |dest| is replaced.
func carryForeignKeys(ctx context.Context, src, dest doltdb.RootValue, carried []doltdb.TableName, tagRemaps map[doltdb.TableName]map[uint64]uint64, srcSchemas, destSchemas map[doltdb.TableName]schema.Schema) (doltdb.RootValue, error) {
destFks, err := dest.GetForeignKeyCollection(ctx)
if err != nil {
return nil, err
}
srcFks, err := src.GetForeignKeyCollection(ctx)
if err != nil {
return nil, err
}
carriedSet := doltdb.NewTableNameSet(carried)
err = srcFks.Iter(func(fk doltdb.ForeignKey) (stop bool, err error) {
if !carriedSet.Contains(fk.TableName) {
return false, nil
}
fk.TableColumns = schema.RemapTags(fk.TableColumns, tagRemaps[fk.TableName])
if carriedSet.Contains(fk.ReferencedTableName) {
fk.ReferencedTableColumns = schema.RemapTags(fk.ReferencedTableColumns, tagRemaps[fk.ReferencedTableName])
} else {
fk.ReferencedTableColumns = schema.RemapTagsByColumnName(fk.ReferencedTableColumns, srcSchemas[fk.ReferencedTableName], destSchemas[fk.ReferencedTableName])
}
// Remove the merge's stale copy first. AddKeys would otherwise refuse the duplicate name.
destFks.RemoveKeyByName(fk.Name, fk.TableName)
return false, destFks.AddKeys(fk)
})
if err != nil {
return nil, err
}
return dest.PutForeignKeyCollection(ctx, destFks)
}
Loading
Loading