diff --git a/go/libraries/doltcore/doltdb/system_table.go b/go/libraries/doltcore/doltdb/system_table.go index 65c1c9d6aec..6932e36eee9 100644 --- a/go/libraries/doltcore/doltdb/system_table.go +++ b/go/libraries/doltcore/doltdb/system_table.go @@ -291,6 +291,42 @@ const ( // SchemasTablesSqlModeCol is the name of the column that stores the SQL_MODE string used when this fragment // was originally defined. Mode settings, such as ANSI_QUOTES, are needed to correctly parse the fragment. SchemasTablesSqlModeCol = "sql_mode" + + // DiffTypeCol is the column name for the type of change (added, modified, removed) in diff tables + DiffTypeCol = "diff_type" + + // ToCommitCol is the column name for the "to" commit in diff tables + ToCommitCol = "to_commit" + + // FromCommitCol is the column name for the "from" commit in diff tables + FromCommitCol = "from_commit" + + // ToCommitDateCol is the column name for the "to" commit date in diff tables + ToCommitDateCol = "to_commit_date" + + // FromCommitDateCol is the column name for the "from" commit date in diff tables + FromCommitDateCol = "from_commit_date" + + // WorkingCommitRef is the special commit reference for working changes + WorkingCommitRef = "WORKING" + + // EmptyCommitRef is the special commit reference for empty/initial state + EmptyCommitRef = "EMPTY" + + // DiffTypeAdded represents a row that was added in a diff + DiffTypeAdded = "added" + + // DiffTypeModified represents a row that was modified in a diff + DiffTypeModified = "modified" + + // DiffTypeRemoved represents a row that was removed in a diff + DiffTypeRemoved = "removed" + + // DiffToPrefix is the prefix for "to" columns in diff tables + DiffToPrefix = "to_" + + // DiffFromPrefix is the prefix for "from" columns in diff tables + DiffFromPrefix = "from_" ) const ( diff --git a/go/libraries/doltcore/sqle/database.go b/go/libraries/doltcore/sqle/database.go index 37a18079deb..8426968156b 100644 --- a/go/libraries/doltcore/sqle/database.go +++ b/go/libraries/doltcore/sqle/database.go @@ -332,6 +332,20 @@ func (db Database) getTableInsensitive(ctx *sql.Context, head *doltdb.Commit, ds // TODO: these tables that cache a root value at construction time should not, they need to get it from the session // at runtime switch { + case lwrName == doltdb.DoltDiffTablePrefix+doltdb.SchemasTableName: + // Special handling for dolt_diff_dolt_schemas + // Get the HEAD commit + if head == nil { + var err error + head, err = ds.GetHeadCommit(ctx, db.RevisionQualifiedName()) + if err != nil { + return nil, false, err + } + } + + // Use the same pattern as regular diff tables - this will show complete history + return DoltSchemasDiffTable(ctx, db.ddb, head, root, db), true, nil + case strings.HasPrefix(lwrName, doltdb.DoltDiffTablePrefix): if head == nil { var err error @@ -384,6 +398,17 @@ func (db Database) getTableInsensitive(ctx *sql.Context, head *doltdb.Commit, ds } return dt, true, nil + case lwrName == doltdb.DoltHistoryTablePrefix+doltdb.SchemasTableName: + // Special handling for dolt_history_dolt_schemas + if head == nil { + var err error + head, err = ds.GetHeadCommit(ctx, db.RevisionQualifiedName()) + if err != nil { + return nil, false, err + } + } + return DoltSchemasHistoryTable(db.ddb, head, db), true, nil + case strings.HasPrefix(lwrName, doltdb.DoltHistoryTablePrefix): baseTableName := tblName[len(doltdb.DoltHistoryTablePrefix):] baseTable, ok, err := db.getTable(ctx, root, baseTableName) diff --git a/go/libraries/doltcore/sqle/dolt_schemas_diff_table.go b/go/libraries/doltcore/sqle/dolt_schemas_diff_table.go new file mode 100644 index 00000000000..d6929f4285c --- /dev/null +++ b/go/libraries/doltcore/sqle/dolt_schemas_diff_table.go @@ -0,0 +1,619 @@ +// Copyright 2025 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 sqle + +import ( + "fmt" + "io" + "strings" + "time" + + "github.com/dolthub/go-mysql-server/sql" + gmstypes "github.com/dolthub/go-mysql-server/sql/types" + "github.com/dolthub/vitess/go/sqltypes" + + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/table/editor" + "github.com/dolthub/dolt/go/store/hash" + "github.com/dolthub/dolt/go/store/types" +) + +// DoltSchemasDiffTable creates a dolt_schemas diff table that shows complete history +// like regular dolt_diff_ tables +func DoltSchemasDiffTable(ctx *sql.Context, ddb *doltdb.DoltDB, head *doltdb.Commit, workingRoot doltdb.RootValue, db Database) sql.Table { + return &doltSchemasDiffTable{ + name: doltdb.DoltDiffTablePrefix + doltdb.SchemasTableName, + ddb: ddb, + head: head, + workingRoot: workingRoot, + db: db, + } +} + +// doltSchemasDiffTable implements the dolt_diff_dolt_schemas system table with complete history +// It follows the same pattern as regular dolt_diff_ tables +type doltSchemasDiffTable struct { + name string + ddb *doltdb.DoltDB + head *doltdb.Commit + workingRoot doltdb.RootValue + db Database +} + +var _ sql.Table = (*doltSchemasDiffTable)(nil) +var _ sql.PrimaryKeyTable = (*doltSchemasDiffTable)(nil) + +// Name implements sql.Table +func (dsdt *doltSchemasDiffTable) Name() string { + return dsdt.name +} + +// String implements sql.Table +func (dsdt *doltSchemasDiffTable) String() string { + return dsdt.name +} + +// Schema implements sql.Table +func (dsdt *doltSchemasDiffTable) Schema() sql.Schema { + // Same schema as the regular diff table + return sql.Schema{ + // TO columns + &sql.Column{Name: doltdb.DiffToPrefix + doltdb.SchemasTablesTypeCol, Type: gmstypes.MustCreateString(sqltypes.VarChar, 64, sql.Collation_utf8mb4_0900_ai_ci), Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.DiffToPrefix + doltdb.SchemasTablesNameCol, Type: gmstypes.MustCreateString(sqltypes.VarChar, 64, sql.Collation_utf8mb4_0900_ai_ci), Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.DiffToPrefix + doltdb.SchemasTablesFragmentCol, Type: gmstypes.LongText, Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.DiffToPrefix + doltdb.SchemasTablesExtraCol, Type: gmstypes.JSON, Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.DiffToPrefix + doltdb.SchemasTablesSqlModeCol, Type: gmstypes.MustCreateString(sqltypes.VarChar, 256, sql.Collation_utf8mb4_0900_ai_ci), Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.ToCommitCol, Type: gmstypes.MustCreateString(sqltypes.VarChar, 1023, sql.Collation_utf8mb4_0900_ai_ci), Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.ToCommitDateCol, Type: gmstypes.DatetimeMaxPrecision, Nullable: true, Source: dsdt.name}, + + // FROM columns + &sql.Column{Name: doltdb.DiffFromPrefix + doltdb.SchemasTablesTypeCol, Type: gmstypes.MustCreateString(sqltypes.VarChar, 64, sql.Collation_utf8mb4_0900_ai_ci), Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.DiffFromPrefix + doltdb.SchemasTablesNameCol, Type: gmstypes.MustCreateString(sqltypes.VarChar, 64, sql.Collation_utf8mb4_0900_ai_ci), Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.DiffFromPrefix + doltdb.SchemasTablesFragmentCol, Type: gmstypes.LongText, Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.DiffFromPrefix + doltdb.SchemasTablesExtraCol, Type: gmstypes.JSON, Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.DiffFromPrefix + doltdb.SchemasTablesSqlModeCol, Type: gmstypes.MustCreateString(sqltypes.VarChar, 256, sql.Collation_utf8mb4_0900_ai_ci), Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.FromCommitCol, Type: gmstypes.MustCreateString(sqltypes.VarChar, 1023, sql.Collation_utf8mb4_0900_ai_ci), Nullable: true, Source: dsdt.name}, + &sql.Column{Name: doltdb.FromCommitDateCol, Type: gmstypes.DatetimeMaxPrecision, Nullable: true, Source: dsdt.name}, + + // Diff type column + &sql.Column{Name: doltdb.DiffTypeCol, Type: gmstypes.MustCreateString(sqltypes.VarChar, 1023, sql.Collation_utf8mb4_0900_ai_ci), Nullable: false, Source: dsdt.name}, + } +} + +// Collation implements sql.Table +func (dsdt *doltSchemasDiffTable) Collation() sql.CollationID { + return sql.Collation_Default +} + +// Partitions implements sql.Table - follows the pattern of regular diff tables +func (dsdt *doltSchemasDiffTable) Partitions(ctx *sql.Context) (sql.PartitionIter, error) { + // Create commit iterator for the entire history + cmItr := doltdb.CommitItrForRoots[*sql.Context](dsdt.ddb, dsdt.head) + + // Set up commit iterator like regular diff tables + err := cmItr.Reset(ctx) + if err != nil { + return nil, err + } + + return &DoltSchemasDiffPartitionItr{ + cmItr: cmItr, + db: dsdt.db, + head: dsdt.head, + workingRoot: dsdt.workingRoot, + workingPartitionDone: false, + }, nil +} + +// PartitionRows implements sql.Table +func (dsdt *doltSchemasDiffTable) PartitionRows(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) { + p := partition.(*DoltSchemasDiffPartition) + return p.GetRowIter(ctx) +} + +// PrimaryKeySchema implements sql.PrimaryKeyTable +func (dsdt *doltSchemasDiffTable) PrimaryKeySchema() sql.PrimaryKeySchema { + return sql.PrimaryKeySchema{ + Schema: dsdt.Schema(), + PkOrdinals: []int{0, 1}, // to_type, to_name + } +} + +// DoltSchemasDiffPartitionItr iterates through commit history for schema diffs +type DoltSchemasDiffPartitionItr struct { + cmItr doltdb.CommitItr[*sql.Context] + db Database + head *doltdb.Commit + workingRoot doltdb.RootValue + workingPartitionDone bool +} + +var _ sql.PartitionIter = (*DoltSchemasDiffPartitionItr)(nil) + +// Next implements sql.PartitionIter +func (dsdp *DoltSchemasDiffPartitionItr) Next(ctx *sql.Context) (sql.Partition, error) { + // First iterate through commit history, then add working partition as the final step + for { + cmHash, optCmt, err := dsdp.cmItr.Next(ctx) + if err == io.EOF { + // Finished with commit history, now add working partition if not done + if !dsdp.workingPartitionDone { + dsdp.workingPartitionDone = true + partition, err := dsdp.createWorkingPartition(ctx) + if err != nil { + return nil, fmt.Errorf("failed to create working partition: %w", err) + } + return partition, nil + } + return nil, io.EOF + } + if err != nil { + return nil, err + } + + cm, ok := optCmt.ToCommit() + if !ok { + return nil, doltdb.ErrGhostCommitRuntimeFailure + } + + // Get commit info for this commit + cmRoot, err := cm.GetRootValue(ctx) + if err != nil { + return nil, err + } + + // Check if this commit has a schemas table + cmSchemasTable, cmExists, err := cmRoot.GetTable(ctx, doltdb.TableName{Name: doltdb.SchemasTableName}) + if err != nil { + return nil, err + } + + // Get parent commit for comparison + parentHashes, err := cm.ParentHashes(ctx) + if err != nil { + return nil, err + } + + // For simplicity, only use the first parent (TODO: handle merge commits) + if len(parentHashes) == 0 { + // This is the initial commit, compare with empty state + if cmExists { + // Create partition comparing empty state to this commit + cmMeta, err := cm.GetCommitMeta(ctx) + if err != nil { + return nil, err + } + cmCommitDate := types.Timestamp(cmMeta.Time()) + + return &DoltSchemasDiffPartition{ + toTable: cmSchemasTable, + fromTable: nil, // Empty state + toName: cmHash.String(), + fromName: doltdb.EmptyCommitRef, + toDate: &cmCommitDate, + fromDate: nil, + toRoot: cmRoot, + fromRoot: nil, // No root for empty state + db: dsdp.db, + }, nil + } + continue // Skip if no schemas table in initial commit + } + + // Get parent commit + parentOptCmt, err := cm.GetParent(ctx, 0) + if err != nil { + return nil, err + } + parentCm, ok := parentOptCmt.ToCommit() + if !ok { + return nil, doltdb.ErrGhostCommitEncountered + } + + parentRoot, err := parentCm.GetRootValue(ctx) + if err != nil { + return nil, err + } + + parentSchemasTable, parentExists, err := parentRoot.GetTable(ctx, doltdb.TableName{Name: doltdb.SchemasTableName}) + if err != nil { + return nil, err + } + + // Check if schemas table changed between parent and this commit + var cmTblHash, parentTblHash hash.Hash + if cmExists { + cmTblHash, _, err = cmRoot.GetTableHash(ctx, doltdb.TableName{Name: doltdb.SchemasTableName}) + if err != nil { + return nil, err + } + } + if parentExists { + parentTblHash, _, err = parentRoot.GetTableHash(ctx, doltdb.TableName{Name: doltdb.SchemasTableName}) + if err != nil { + return nil, err + } + } + + // If table hashes are different or existence changed, create a diff partition + if cmTblHash != parentTblHash || cmExists != parentExists { + cmMeta, err := cm.GetCommitMeta(ctx) + if err != nil { + return nil, err + } + cmCommitDate := types.Timestamp(cmMeta.Time()) + + parentMeta, err := parentCm.GetCommitMeta(ctx) + if err != nil { + return nil, err + } + parentCommitDate := types.Timestamp(parentMeta.Time()) + + parentHash, err := parentCm.HashOf() + if err != nil { + return nil, err + } + + return &DoltSchemasDiffPartition{ + toTable: cmSchemasTable, + fromTable: parentSchemasTable, + toName: cmHash.String(), + fromName: parentHash.String(), + toDate: &cmCommitDate, + fromDate: &parentCommitDate, + toRoot: cmRoot, + fromRoot: parentRoot, + db: dsdp.db, + }, nil + } + } +} + +func (dsdp *DoltSchemasDiffPartitionItr) createWorkingPartition(ctx *sql.Context) (sql.Partition, error) { + // Get HEAD commit details + headRoot, err := dsdp.head.GetRootValue(ctx) + if err != nil { + return nil, err + } + + headCommitHash, err := dsdp.head.HashOf() + if err != nil { + return nil, err + } + + headMeta, err := dsdp.head.GetCommitMeta(ctx) + if err != nil { + return nil, err + } + headCommitDate := types.Timestamp(headMeta.Time()) + + headSchemasTable, _, err := headRoot.GetTable(ctx, doltdb.TableName{Name: doltdb.SchemasTableName}) + if err != nil { + return nil, err + } + + workingSchemasTable, _, err := dsdp.workingRoot.GetTable(ctx, doltdb.TableName{Name: doltdb.SchemasTableName}) + if err != nil { + return nil, err + } + + return &DoltSchemasDiffPartition{ + toTable: workingSchemasTable, + fromTable: headSchemasTable, + toName: doltdb.WorkingCommitRef, + fromName: headCommitHash.String(), + toDate: nil, + fromDate: &headCommitDate, + toRoot: dsdp.workingRoot, + fromRoot: headRoot, + db: dsdp.db, + }, nil +} + +// Close implements sql.PartitionIter +func (dsdp *DoltSchemasDiffPartitionItr) Close(ctx *sql.Context) error { + return nil +} + +// DoltSchemasDiffPartition represents a single diff between two commit states +type DoltSchemasDiffPartition struct { + toTable *doltdb.Table + fromTable *doltdb.Table + toName string + fromName string + toDate *types.Timestamp + fromDate *types.Timestamp + toRoot doltdb.RootValue + fromRoot doltdb.RootValue + db Database +} + +var _ sql.Partition = (*DoltSchemasDiffPartition)(nil) + +// Key implements sql.Partition +func (dsdp *DoltSchemasDiffPartition) Key() []byte { + return []byte(dsdp.toName + dsdp.fromName) +} + +// GetRowIter implements sql.Partition +func (dsdp *DoltSchemasDiffPartition) GetRowIter(ctx *sql.Context) (sql.RowIter, error) { + // Create a special diff iterator just for this partition + return &doltSchemasDiffPartitionRowIter{ + ctx: ctx, + toTable: dsdp.toTable, + fromTable: dsdp.fromTable, + toName: dsdp.toName, + fromName: dsdp.fromName, + toDate: dsdp.toDate, + fromDate: dsdp.fromDate, + toRoot: dsdp.toRoot, + fromRoot: dsdp.fromRoot, + db: dsdp.db, + done: false, + }, nil +} + +// doltSchemasDiffPartitionRowIter implements a row iterator for a single diff partition +type doltSchemasDiffPartitionRowIter struct { + ctx *sql.Context + toTable *doltdb.Table + fromTable *doltdb.Table + toName string + fromName string + toDate *types.Timestamp + fromDate *types.Timestamp + toRoot doltdb.RootValue + fromRoot doltdb.RootValue + db Database + rows []sql.Row + idx int + done bool +} + +// Next implements sql.RowIter +func (dspri *doltSchemasDiffPartitionRowIter) Next(ctx *sql.Context) (sql.Row, error) { + if dspri.rows == nil && !dspri.done { + // Initialize diff rows for this specific commit pair + err := dspri.loadDiffRowsForCommitPair() + if err != nil { + return nil, err + } + } + + if dspri.idx >= len(dspri.rows) { + return nil, io.EOF + } + + row := dspri.rows[dspri.idx] + dspri.idx++ + return row, nil +} + +func (dspri *doltSchemasDiffPartitionRowIter) loadDiffRowsForCommitPair() error { + // Build maps of schema rows for comparison + fromRows := make(map[string]sql.Row) + toRows := make(map[string]sql.Row) + + // Read from table if it exists + if dspri.fromTable != nil && dspri.fromRoot != nil { + if err := dspri.readDoltSchemasRowsFromRoot(dspri.fromTable, dspri.fromRoot, fromRows); err != nil { + return err + } + } + + // Read to table if it exists + if dspri.toTable != nil { + if err := dspri.readDoltSchemasRowsFromRoot(dspri.toTable, dspri.toRoot, toRows); err != nil { + return err + } + } + + // Generate diff rows + rows := make([]sql.Row, 0) + + // Find added and modified rows + for key, toRow := range toRows { + if fromRow, exists := fromRows[key]; exists { + // Compare rows to see if modified + if !rowsEqual(fromRow, toRow) { + // Modified row: to_* columns from toRow, from_* columns from fromRow + diffRow := dspri.createDiffRow(toRow, fromRow, doltdb.DiffTypeModified) + rows = append(rows, diffRow) + } + } else { + // Added row: to_* columns from toRow, from_* columns are null + diffRow := dspri.createDiffRow(toRow, nil, doltdb.DiffTypeAdded) + rows = append(rows, diffRow) + } + } + + // Find removed rows + for key, fromRow := range fromRows { + if _, exists := toRows[key]; !exists { + // Removed row: to_* columns are null, from_* columns from fromRow + diffRow := dspri.createDiffRow(nil, fromRow, doltdb.DiffTypeRemoved) + rows = append(rows, diffRow) + } + } + + dspri.rows = rows + dspri.done = true + return nil +} + +func (dspri *doltSchemasDiffPartitionRowIter) readDoltSchemasRowsFromRoot(tbl *doltdb.Table, root doltdb.RootValue, rowMap map[string]sql.Row) error { + if tbl == nil { + return nil // Empty table, no rows to read + } + + // Get the schema from the table + sch, err := tbl.GetSchema(dspri.ctx) + if err != nil { + return err + } + + // Create a DoltTable using the database reference we have + doltTable, err := NewDoltTable(doltdb.SchemasTableName, sch, tbl, dspri.db, editor.Options{}) + if err != nil { + return err + } + + // Lock the table to the specific root + lockedTable, err := doltTable.LockedToRoot(dspri.ctx, root) + if err != nil { + return err + } + + // Get partitions and read rows + partitions, err := lockedTable.Partitions(dspri.ctx) + if err != nil { + return err + } + + var baseRows []sql.Row + for { + partition, err := partitions.Next(dspri.ctx) + if err == io.EOF { + break + } + if err != nil { + return err + } + + rowIter, err := lockedTable.PartitionRows(dspri.ctx, partition) + if err != nil { + return err + } + + for { + row, err := rowIter.Next(dspri.ctx) + if err == io.EOF { + break + } + if err != nil { + return err + } + baseRows = append(baseRows, row) + } + + err = rowIter.Close(dspri.ctx) + if err != nil { + return err + } + } + + err = partitions.Close(dspri.ctx) + if err != nil { + return err + } + + // Process each row and add to map + for _, row := range baseRows { + // Create key from type and name columns + if len(row) >= 2 && row[0] != nil && row[1] != nil { + key := strings.ToLower(row[0].(string)) + ":" + strings.ToLower(row[1].(string)) + rowMap[key] = row + } + } + + return nil +} + +// createDiffRow creates a diff row with proper to_* and from_* column layout +func (dspri *doltSchemasDiffPartitionRowIter) createDiffRow(toRow, fromRow sql.Row, diffType string) sql.Row { + // Expected schema: 7 to_* columns + 7 from_* columns + 1 diff_type = 15 columns + row := make(sql.Row, 15) + + // TO columns (indices 0-6) + if toRow != nil && len(toRow) >= 5 { + copy(row[0:5], toRow[0:5]) // to_type, to_name, to_fragment, to_extra, to_sql_mode + row[5] = dspri.toName // to_commit + if dspri.toDate != nil { + row[6] = time.Time(*dspri.toDate) // to_commit_date converted to time.Time + } else { + row[6] = nil // to_commit_date + } + } else { + // to_* schema columns are null for removed rows, but commit info should be populated + for i := 0; i < 5; i++ { + row[i] = nil + } + row[5] = dspri.toName // to_commit should always be populated + if dspri.toDate != nil { + row[6] = time.Time(*dspri.toDate) // to_commit_date converted to time.Time + } else { + row[6] = nil // to_commit_date + } + } + + // FROM columns (indices 7-13) + if fromRow != nil && len(fromRow) >= 5 { + copy(row[7:12], fromRow[0:5]) // from_type, from_name, from_fragment, from_extra, from_sql_mode + row[12] = dspri.fromName // from_commit + if dspri.fromDate != nil { + row[13] = time.Time(*dspri.fromDate) // from_commit_date converted to time.Time + } else { + row[13] = nil // from_commit_date + } + } else { + // from_* schema columns are null for added rows, but commit info should be populated + for i := 7; i < 12; i++ { + row[i] = nil + } + row[12] = dspri.fromName // from_commit should always be populated + if dspri.fromDate != nil { + row[13] = time.Time(*dspri.fromDate) // from_commit_date converted to time.Time + } else { + row[13] = nil // from_commit_date + } + } + + // Diff type column (index 14) + row[14] = diffType + + return row +} + +// Close implements sql.RowIter +func (dspri *doltSchemasDiffPartitionRowIter) Close(ctx *sql.Context) error { + return nil +} + +// rowsEqual compares two SQL rows for equality +func rowsEqual(row1, row2 sql.Row) bool { + if len(row1) != len(row2) { + return false + } + + for i, val1 := range row1 { + val2 := row2[i] + if val1 == nil && val2 == nil { + continue + } + if val1 == nil || val2 == nil { + return false + } + + // Handle JSON types specially - convert to string for comparison + // JSON values might have different internal representations but same content + str1 := fmt.Sprintf("%v", val1) + str2 := fmt.Sprintf("%v", val2) + if str1 != str2 { + return false + } + } + + return true +} diff --git a/go/libraries/doltcore/sqle/dolt_schemas_history_table.go b/go/libraries/doltcore/sqle/dolt_schemas_history_table.go new file mode 100644 index 00000000000..e62ad659519 --- /dev/null +++ b/go/libraries/doltcore/sqle/dolt_schemas_history_table.go @@ -0,0 +1,257 @@ +// Copyright 2025 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 sqle + +import ( + "io" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" + "github.com/dolthub/vitess/go/sqltypes" + + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/table/editor" +) + +// doltSchemasHistoryTable implements the dolt_history_dolt_schemas system table +type doltSchemasHistoryTable struct { + name string + ddb *doltdb.DoltDB + head *doltdb.Commit + db Database // Add database reference for DoltTable creation +} + +var _ sql.Table = (*doltSchemasHistoryTable)(nil) +var _ sql.PrimaryKeyTable = (*doltSchemasHistoryTable)(nil) + +// DoltSchemasHistoryTable creates a dolt_schemas history table instance +func DoltSchemasHistoryTable(ddb *doltdb.DoltDB, head *doltdb.Commit, db Database) sql.Table { + return &doltSchemasHistoryTable{ + name: doltdb.DoltHistoryTablePrefix + doltdb.SchemasTableName, + ddb: ddb, + head: head, + db: db, + } +} + +// Name implements sql.Table +func (dsht *doltSchemasHistoryTable) Name() string { + return dsht.name +} + +// String implements sql.Table +func (dsht *doltSchemasHistoryTable) String() string { + return dsht.name +} + +// Schema implements sql.Table +func (dsht *doltSchemasHistoryTable) Schema() sql.Schema { + // Base schema from dolt_schemas table + baseSch := sql.Schema{ + &sql.Column{Name: doltdb.SchemasTablesTypeCol, Type: types.MustCreateString(sqltypes.VarChar, 64, sql.Collation_utf8mb4_0900_ai_ci), Nullable: false, PrimaryKey: true, Source: dsht.name}, + &sql.Column{Name: doltdb.SchemasTablesNameCol, Type: types.MustCreateString(sqltypes.VarChar, 64, sql.Collation_utf8mb4_0900_ai_ci), Nullable: false, PrimaryKey: true, Source: dsht.name}, + &sql.Column{Name: doltdb.SchemasTablesFragmentCol, Type: types.LongText, Nullable: true, Source: dsht.name}, + &sql.Column{Name: doltdb.SchemasTablesExtraCol, Type: types.JSON, Nullable: true, Source: dsht.name}, + &sql.Column{Name: doltdb.SchemasTablesSqlModeCol, Type: types.MustCreateString(sqltypes.VarChar, 256, sql.Collation_utf8mb4_0900_ai_ci), Nullable: true, Source: dsht.name}, + } + + // Add commit history columns + historySch := make(sql.Schema, len(baseSch), len(baseSch)+3) + copy(historySch, baseSch) + + historySch = append(historySch, + &sql.Column{Name: CommitHashCol, Type: CommitHashColType, Nullable: false, PrimaryKey: true, Source: dsht.name}, + &sql.Column{Name: CommitterCol, Type: CommitterColType, Nullable: false, Source: dsht.name}, + &sql.Column{Name: CommitDateCol, Type: types.Datetime, Nullable: false, Source: dsht.name}, + ) + + return historySch +} + +// Collation implements sql.Table +func (dsht *doltSchemasHistoryTable) Collation() sql.CollationID { + return sql.Collation_Default +} + +// Partitions implements sql.Table +func (dsht *doltSchemasHistoryTable) Partitions(ctx *sql.Context) (sql.PartitionIter, error) { + // Use the same commit iterator pattern as HistoryTable + cmItr := doltdb.CommitItrForRoots[*sql.Context](dsht.ddb, dsht.head) + return &commitPartitioner{cmItr: cmItr}, nil +} + +// PartitionRows implements sql.Table +func (dsht *doltSchemasHistoryTable) PartitionRows(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) { + cp := partition.(*commitPartition) + return &doltSchemasHistoryRowIter{ + ctx: ctx, + ddb: dsht.ddb, + commit: cp.cm, + history: dsht, + }, nil +} + +// PrimaryKeySchema implements sql.PrimaryKeyTable +func (dsht *doltSchemasHistoryTable) PrimaryKeySchema() sql.PrimaryKeySchema { + return sql.PrimaryKeySchema{ + Schema: dsht.Schema(), + PkOrdinals: []int{0, 1, 5}, // type, name, commit_hash + } +} + +// doltSchemasHistoryRowIter iterates through dolt_schemas rows for a single commit +type doltSchemasHistoryRowIter struct { + ctx *sql.Context + ddb *doltdb.DoltDB + commit *doltdb.Commit + history *doltSchemasHistoryTable // Add reference to parent table + rows []sql.Row + idx int +} + +// Next implements sql.RowIter +func (dshri *doltSchemasHistoryRowIter) Next(ctx *sql.Context) (sql.Row, error) { + if dshri.rows == nil { + // Initialize rows from the commit's dolt_schemas table + err := dshri.loadRows() + if err != nil { + return nil, err + } + } + + if dshri.idx >= len(dshri.rows) { + return nil, io.EOF + } + + row := dshri.rows[dshri.idx] + dshri.idx++ + + return row, nil +} + +func (dshri *doltSchemasHistoryRowIter) loadRows() error { + root, err := dshri.commit.GetRootValue(dshri.ctx) + if err != nil { + return err + } + + // Get the table at this commit + tbl, ok, err := root.GetTable(dshri.ctx, doltdb.TableName{Name: doltdb.SchemasTableName}) + if err != nil { + return err + } + if !ok { + // No dolt_schemas table in this commit, return empty rows + dshri.rows = make([]sql.Row, 0) + return nil + } + + // Get commit metadata + commitHash, err := dshri.commit.HashOf() + if err != nil { + return err + } + commitMeta, err := dshri.commit.GetCommitMeta(dshri.ctx) + if err != nil { + return err + } + + // Convert commit metadata to SQL values + commitHashStr := commitHash.String() + committerStr := commitMeta.Name + " <" + commitMeta.Email + ">" + commitDate := commitMeta.Time() + + // Get the schema + sch, err := tbl.GetSchema(dshri.ctx) + if err != nil { + return err + } + + // Create a DoltTable using the database reference we now have + doltTable, err := NewDoltTable(doltdb.SchemasTableName, sch, tbl, dshri.history.db, editor.Options{}) + if err != nil { + return err + } + + // Lock the table to this specific commit's root + lockedTable, err := doltTable.LockedToRoot(dshri.ctx, root) + if err != nil { + return err + } + + // Get partitions and read rows + partitions, err := lockedTable.Partitions(dshri.ctx) + if err != nil { + return err + } + + var baseRows []sql.Row + for { + partition, err := partitions.Next(dshri.ctx) + if err == io.EOF { + break + } + if err != nil { + return err + } + + rowIter, err := lockedTable.PartitionRows(dshri.ctx, partition) + if err != nil { + return err + } + + for { + row, err := rowIter.Next(dshri.ctx) + if err == io.EOF { + break + } + if err != nil { + return err + } + baseRows = append(baseRows, row) + } + + err = rowIter.Close(dshri.ctx) + if err != nil { + return err + } + } + + err = partitions.Close(dshri.ctx) + if err != nil { + return err + } + + // Add commit metadata to each row + rows := make([]sql.Row, 0, len(baseRows)) + for _, baseRow := range baseRows { + // Append commit columns to the base row + sqlRow := make(sql.Row, len(baseRow)+3) + copy(sqlRow, baseRow) + sqlRow[len(baseRow)] = commitHashStr + sqlRow[len(baseRow)+1] = committerStr + sqlRow[len(baseRow)+2] = commitDate + + rows = append(rows, sqlRow) + } + + dshri.rows = rows + return nil +} + +// Close implements sql.RowIter +func (dshri *doltSchemasHistoryRowIter) Close(ctx *sql.Context) error { + return nil +} diff --git a/go/libraries/doltcore/sqle/integration_test/dolt_schemas_history_diff_test.go b/go/libraries/doltcore/sqle/integration_test/dolt_schemas_history_diff_test.go new file mode 100644 index 00000000000..e7c4d2301a6 --- /dev/null +++ b/go/libraries/doltcore/sqle/integration_test/dolt_schemas_history_diff_test.go @@ -0,0 +1,331 @@ +// Copyright 2024 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 integration_test + +import ( + "context" + "fmt" + "testing" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/stretchr/testify/require" + + cmd "github.com/dolthub/dolt/go/cmd/dolt/commands" + "github.com/dolthub/dolt/go/libraries/doltcore/dtestutils" + "github.com/dolthub/dolt/go/libraries/doltcore/env" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle" +) + +func TestDoltSchemasHistoryTable(t *testing.T) { + SkipByDefaultInCI(t) + ctx := context.Background() + dEnv := setupDoltSchemasHistoryTests(t) + defer dEnv.DoltDB(ctx).Close() + for _, test := range doltSchemasHistoryTableTests() { + t.Run(test.name, func(t *testing.T) { + testDoltSchemasHistoryTable(t, test, dEnv) + }) + } +} + +func TestDoltSchemasDiffTable(t *testing.T) { + SkipByDefaultInCI(t) + ctx := context.Background() + dEnv := setupDoltSchemasDiffTests(t) + defer dEnv.DoltDB(ctx).Close() + for _, test := range doltSchemasDiffTableTests() { + t.Run(test.name, func(t *testing.T) { + testDoltSchemasDiffTable(t, test, dEnv) + }) + } +} + +type doltSchemasTableTest struct { + name string + setup []testCommand + query string + rows []sql.Row +} + +// Global variables to store commit hashes for test validation +var ( + DOLT_SCHEMAS_HEAD string + DOLT_SCHEMAS_HEAD_1 string + DOLT_SCHEMAS_HEAD_2 string + DOLT_SCHEMAS_INIT string +) + +var setupDoltSchemasCommon = []testCommand{ + // Create initial view + {cmd.SqlCmd{}, args{"-q", "CREATE VIEW test_view AS SELECT 1 as col1"}}, + {cmd.AddCmd{}, args{"."}}, + {cmd.CommitCmd{}, args{"-m", "first commit: added test_view"}}, + + // Create a trigger + {cmd.SqlCmd{}, args{"-q", "CREATE TABLE test_table (id INT PRIMARY KEY, name VARCHAR(50))"}}, + {cmd.SqlCmd{}, args{"-q", `CREATE TRIGGER test_trigger + BEFORE INSERT ON test_table + FOR EACH ROW + SET NEW.name = UPPER(NEW.name)`}}, + {cmd.AddCmd{}, args{"."}}, + {cmd.CommitCmd{}, args{"-m", "second commit: added test_table and test_trigger"}}, + + // Modify the view + {cmd.SqlCmd{}, args{"-q", "DROP VIEW test_view"}}, + {cmd.SqlCmd{}, args{"-q", "CREATE VIEW test_view AS SELECT 1 as col1, 2 as col2"}}, + {cmd.AddCmd{}, args{"."}}, + {cmd.CommitCmd{}, args{"-m", "third commit: modified test_view"}}, + + // Add an event + {cmd.SqlCmd{}, args{"-q", `CREATE EVENT test_event + ON SCHEDULE EVERY 1 DAY + DO INSERT INTO test_table VALUES (1, 'daily')`}}, + {cmd.AddCmd{}, args{"."}}, + {cmd.CommitCmd{}, args{"-m", "fourth commit: added test_event"}}, + + {cmd.LogCmd{}, args{}}, +} + +func doltSchemasHistoryTableTests() []doltSchemasTableTest { + return []doltSchemasTableTest{ + { + name: "verify dolt_history_dolt_schemas has all required columns", + query: "SELECT COUNT(*) FROM (SELECT type, name, fragment, extra, sql_mode, commit_hash, committer, commit_date FROM dolt_history_dolt_schemas LIMIT 0) AS schema_check", + rows: []sql.Row{ + {int64(0)}, // Should return 0 rows but verify all columns exist + }, + }, + { + name: "check correct number of history entries", + query: "SELECT COUNT(*) FROM dolt_history_dolt_schemas", + rows: []sql.Row{ + {int64(8)}, // view(4 commits) + trigger(3 commits) + event(1 commit) = 8 total + }, + }, + { + name: "filter for trigger history only", + query: "SELECT COUNT(*) FROM dolt_history_dolt_schemas WHERE type = 'trigger'", + rows: []sql.Row{ + {int64(3)}, // Trigger appears in 3 commits + }, + }, + { + name: "filter for objects in earliest commit", + query: "SELECT COUNT(*) FROM dolt_history_dolt_schemas WHERE type = 'view' AND name = 'test_view'", + rows: []sql.Row{ + {int64(4)}, // View appears in all 4 commits where it exists + }, + }, + { + name: "filter for view changes only", + query: "SELECT COUNT(*) FROM dolt_history_dolt_schemas WHERE type = 'view'", + rows: []sql.Row{ + {int64(4)}, // View appears in 4 commits (created, exists with trigger, modified, exists with event) + }, + }, + { + name: "check commit_hash is not null", + query: "SELECT COUNT(*) FROM dolt_history_dolt_schemas WHERE commit_hash IS NOT NULL", + rows: []sql.Row{ + {int64(8)}, // Total number of schema entries across all commits + }, + }, + { + name: "filter by multiple types", + query: "SELECT type, name FROM dolt_history_dolt_schemas WHERE type IN ('trigger', 'event') AND commit_hash = '" + "%s" + "' ORDER BY type, name", + rows: []sql.Row{ + {"event", "test_event"}, + {"trigger", "test_trigger"}, + }, + }, + { + name: "check committer column exists", + query: "SELECT COUNT(*) FROM dolt_history_dolt_schemas WHERE committer IS NOT NULL", + rows: []sql.Row{ + {int64(8)}, // All entries should have committer info + }, + }, + } +} + +var setupDoltSchemasDiffCommon = []testCommand{ + // Start with a clean state + {cmd.SqlCmd{}, args{"-q", "CREATE VIEW original_view AS SELECT 1 as id"}}, + {cmd.SqlCmd{}, args{"-q", "CREATE TABLE diff_table (id INT PRIMARY KEY)"}}, + {cmd.SqlCmd{}, args{"-q", `CREATE TRIGGER original_trigger + BEFORE INSERT ON diff_table + FOR EACH ROW + SET NEW.id = NEW.id + 1`}}, + {cmd.AddCmd{}, args{"."}}, + {cmd.CommitCmd{}, args{"-m", "base commit with original schemas"}}, + + // Make changes for diff (working directory changes) + {cmd.SqlCmd{}, args{"-q", "DROP VIEW original_view"}}, + {cmd.SqlCmd{}, args{"-q", "CREATE VIEW original_view AS SELECT 1 as id, 'modified' as status"}}, // modified + {cmd.SqlCmd{}, args{"-q", "CREATE VIEW new_view AS SELECT 'added' as status"}}, // added + {cmd.SqlCmd{}, args{"-q", "DROP TRIGGER original_trigger"}}, // removed + {cmd.SqlCmd{}, args{"-q", `CREATE EVENT new_event + ON SCHEDULE EVERY 1 HOUR + DO SELECT 1`}}, // added +} + +func doltSchemasDiffTableTests() []doltSchemasTableTest { + return []doltSchemasTableTest{ + { + name: "verify dolt_diff_dolt_schemas has all required columns", + query: "SELECT COUNT(*) FROM (SELECT to_type, to_name, to_fragment, to_extra, to_sql_mode, to_commit, to_commit_date, from_type, from_name, from_fragment, from_extra, from_sql_mode, from_commit, from_commit_date, diff_type FROM dolt_diff_dolt_schemas LIMIT 0) AS schema_check", + rows: []sql.Row{ + {int64(0)}, // Should return 0 rows but verify all columns exist + }, + }, + { + name: "check complete history is shown", + query: "SELECT COUNT(*) FROM dolt_diff_dolt_schemas", + rows: []sql.Row{ + {int64(6)}, // Initial commit: 2 added (original_view, original_trigger) + Working changes: 4 changes (new_event, new_view added; original_view modified; original_trigger removed) + }, + }, + { + name: "verify working changes are included", + query: "SELECT COUNT(*) FROM dolt_diff_dolt_schemas WHERE to_commit = 'WORKING'", + rows: []sql.Row{ + {int64(4)}, // Working changes: 2 added + 1 modified + 1 removed + }, + }, + { + name: "verify initial commit changes are included", + query: "SELECT COUNT(*) FROM dolt_diff_dolt_schemas WHERE to_commit != 'WORKING'", + rows: []sql.Row{ + {int64(2)}, // Initial commit: original_view + original_trigger added + }, + }, + { + name: "filter for added schemas across all history", + query: "SELECT COUNT(*) FROM dolt_diff_dolt_schemas WHERE diff_type = 'added'", + rows: []sql.Row{ + {int64(4)}, // All added schemas: original_view, original_trigger (initial) + new_event, new_view (working) + }, + }, + { + name: "filter for modified schemas only", + query: "SELECT to_type, to_name FROM dolt_diff_dolt_schemas WHERE diff_type = 'modified' ORDER BY to_type, to_name", + rows: []sql.Row{ + {"view", "original_view"}, // View was modified between HEAD and WORKING + }, + }, + { + name: "filter for removed schemas only", + query: "SELECT from_type, from_name FROM dolt_diff_dolt_schemas WHERE diff_type = 'removed' ORDER BY from_type, from_name", + rows: []sql.Row{ + {"trigger", "original_trigger"}, // Trigger was removed between HEAD and WORKING + }, + }, + { + name: "check working changes show correct commit info", + query: "SELECT DISTINCT to_commit FROM dolt_diff_dolt_schemas WHERE to_commit = 'WORKING'", + rows: []sql.Row{ + {"WORKING"}, // Working changes should have to_commit as WORKING + }, + }, + } +} + +func setupDoltSchemasHistoryTests(t *testing.T) *env.DoltEnv { + dEnv := dtestutils.CreateTestEnv() + ctx := context.Background() + cliCtx, verr := cmd.NewArgFreeCliContext(ctx, dEnv, dEnv.FS) + require.NoError(t, verr) + + for _, c := range setupDoltSchemasCommon { + exitCode := c.cmd.Exec(ctx, c.cmd.Name(), c.args, dEnv, cliCtx) + require.Equal(t, 0, exitCode) + } + + // Get commit hashes for test validation + root, err := dEnv.WorkingRoot(ctx) + require.NoError(t, err) + + rows, err := sqle.ExecuteSelect(ctx, dEnv, root, "SELECT commit_hash FROM dolt_log ORDER BY date DESC") + require.NoError(t, err) + require.Equal(t, 5, len(rows)) // 4 commits + initial commit + + DOLT_SCHEMAS_HEAD = rows[0][0].(string) + DOLT_SCHEMAS_HEAD_1 = rows[1][0].(string) + DOLT_SCHEMAS_HEAD_2 = rows[2][0].(string) + DOLT_SCHEMAS_INIT = rows[4][0].(string) // Skip one to get to the first real commit + + return dEnv +} + +func setupDoltSchemasDiffTests(t *testing.T) *env.DoltEnv { + dEnv := dtestutils.CreateTestEnv() + ctx := context.Background() + cliCtx, verr := cmd.NewArgFreeCliContext(ctx, dEnv, dEnv.FS) + require.NoError(t, verr) + + for _, c := range setupDoltSchemasDiffCommon { + exitCode := c.cmd.Exec(ctx, c.cmd.Name(), c.args, dEnv, cliCtx) + require.Equal(t, 0, exitCode) + } + + return dEnv +} + +func testDoltSchemasHistoryTable(t *testing.T, test doltSchemasTableTest, dEnv *env.DoltEnv) { + ctx := context.Background() + cliCtx, verr := cmd.NewArgFreeCliContext(ctx, dEnv, dEnv.FS) + require.NoError(t, verr) + + for _, c := range test.setup { + exitCode := c.cmd.Exec(ctx, c.cmd.Name(), c.args, dEnv, cliCtx) + require.Equal(t, 0, exitCode) + } + + root, err := dEnv.WorkingRoot(ctx) + require.NoError(t, err) + + // Replace placeholder in query with actual commit hash + query := test.query + if query == fmt.Sprintf("SELECT type, name FROM dolt_history_dolt_schemas WHERE commit_hash = '%s' ORDER BY type, name", "%s") { + query = fmt.Sprintf("SELECT type, name FROM dolt_history_dolt_schemas WHERE commit_hash = '%s' ORDER BY type, name", DOLT_SCHEMAS_INIT) + } + if query == "SELECT type, name FROM dolt_history_dolt_schemas WHERE type IN ('trigger', 'event') AND commit_hash = '"+"%s"+"' ORDER BY type, name" { + query = fmt.Sprintf("SELECT type, name FROM dolt_history_dolt_schemas WHERE type IN ('trigger', 'event') AND commit_hash = '%s' ORDER BY type, name", DOLT_SCHEMAS_HEAD) + } + + actRows, err := sqle.ExecuteSelect(ctx, dEnv, root, query) + require.NoError(t, err) + + require.ElementsMatch(t, test.rows, actRows) +} + +func testDoltSchemasDiffTable(t *testing.T, test doltSchemasTableTest, dEnv *env.DoltEnv) { + ctx := context.Background() + cliCtx, verr := cmd.NewArgFreeCliContext(ctx, dEnv, dEnv.FS) + require.NoError(t, verr) + + for _, c := range test.setup { + exitCode := c.cmd.Exec(ctx, c.cmd.Name(), c.args, dEnv, cliCtx) + require.Equal(t, 0, exitCode) + } + + root, err := dEnv.WorkingRoot(ctx) + require.NoError(t, err) + + actRows, err := sqle.ExecuteSelect(ctx, dEnv, root, test.query) + require.NoError(t, err) + + require.ElementsMatch(t, test.rows, actRows) +} diff --git a/integration-tests/bats/system-tables.bats b/integration-tests/bats/system-tables.bats index af0c88ca2c0..aa6ebd589dc 100644 --- a/integration-tests/bats/system-tables.bats +++ b/integration-tests/bats/system-tables.bats @@ -476,6 +476,183 @@ SQL [[ $output =~ "1,1234567890,13,1,,,modified" ]] || false } +@test "system-tables: query dolt_history_dolt_schemas system table" { + # Set up test data with views, triggers, and events across multiple commits + dolt sql -q "CREATE VIEW test_view AS SELECT 1 as col1" + dolt add . + dolt commit -m "add test view" + + dolt sql -q "CREATE TABLE test_table (id INT PRIMARY KEY, name VARCHAR(50))" + dolt sql -q "CREATE TRIGGER test_trigger BEFORE INSERT ON test_table FOR EACH ROW SET NEW.name = UPPER(NEW.name)" + dolt add . + dolt commit -m "add table and trigger" + + dolt sql -q "DROP VIEW test_view" + dolt sql -q "CREATE VIEW test_view AS SELECT 1 as col1, 2 as col2" + dolt add . + dolt commit -m "modify test view" + + dolt sql -q "CREATE EVENT test_event ON SCHEDULE EVERY 1 DAY DO SELECT 1" + dolt add . + dolt commit -m "add event" + + # Test that the table exists and has correct schema + run dolt sql -r csv -q 'DESCRIBE dolt_history_dolt_schemas' + [ "$status" -eq 0 ] + [[ "$output" =~ "type,varchar(64)" ]] || false + [[ "$output" =~ "name,varchar(64)" ]] || false + [[ "$output" =~ "fragment,longtext" ]] || false + [[ "$output" =~ "extra,json" ]] || false + [[ "$output" =~ "sql_mode,varchar(256)" ]] || false + [[ "$output" =~ "commit_hash,char(32)" ]] || false + [[ "$output" =~ "committer,varchar(1024)" ]] || false + [[ "$output" =~ "commit_date,datetime" ]] || false + + # Test that we have schema objects in history (view appears in all 4 commits, trigger in 3, event in 1) + run dolt sql -q 'SELECT COUNT(*) FROM dolt_history_dolt_schemas WHERE type = "view"' + [ "$status" -eq 0 ] + # Should have 4 view entries (view exists in all 4 commits) + [[ "$output" =~ "4" ]] || false + + # Test that we have trigger history (trigger appears in 3 commits after creation) + run dolt sql -q 'SELECT COUNT(*) FROM dolt_history_dolt_schemas WHERE type = "trigger"' + [ "$status" -eq 0 ] + # Should have 3 trigger entries (trigger exists in last 3 commits) + [[ "$output" =~ "3" ]] || false + + # Test that we have event history (event appears in 1 commit) + run dolt sql -q 'SELECT COUNT(*) FROM dolt_history_dolt_schemas WHERE type = "event"' + [ "$status" -eq 0 ] + # Should have 1 event entry (event only in last commit) + [[ "$output" =~ "1" ]] || false + + # Test filtering by schema object type works + run dolt sql -q 'SELECT DISTINCT type FROM dolt_history_dolt_schemas ORDER BY type' + [ "$status" -eq 0 ] + [[ "$output" =~ "event" ]] || false + [[ "$output" =~ "trigger" ]] || false + [[ "$output" =~ "view" ]] || false + + # Test commit metadata is present for all entries + run dolt sql -q 'SELECT COUNT(*) FROM dolt_history_dolt_schemas WHERE commit_hash IS NOT NULL AND committer IS NOT NULL' + [ "$status" -eq 0 ] + # Should have 8 total entries (4 view + 3 trigger + 1 event) + [[ "$output" =~ "8" ]] || false +} + +@test "system-tables: query dolt_diff_dolt_schemas system table" { + # dolt_diff_dolt_schemas starts empty + run dolt sql -q 'SELECT COUNT(*) FROM dolt_diff_dolt_schemas' + [ "$status" -eq 0 ] + [[ "$output" =~ " 0 " ]] || false + + # Set up test data for diff scenarios + dolt sql -q "CREATE VIEW original_view AS SELECT 1 as id" + dolt sql -q "CREATE TABLE diff_table (id INT PRIMARY KEY)" + dolt sql -q "CREATE TRIGGER original_trigger BEFORE INSERT ON diff_table FOR EACH ROW SET NEW.id = NEW.id + 1" + + # Before we commit our schema changes we should see two new rows in the + # diff table where to_commit='WORKING' + run dolt sql -q "SELECT COUNT(*) FROM dolt_diff_dolt_schemas where to_commit='WORKING'" + [ "$status" -eq 0 ] + [[ "$output" =~ " 2 " ]] || false + + dolt add . + dolt commit -m "base commit with original schemas" + + # After commit, this should still contain two changes, just now the from comit and two commit should be populated with commits not working + run dolt sql -q 'SELECT COUNT(*) FROM dolt_diff_dolt_schemas' + [ "$status" -eq 0 ] + [[ "$output" =~ "2" ]] || false + + # Make changes for diff (working directory changes) + dolt sql -q "DROP VIEW original_view" + dolt sql -q "CREATE VIEW original_view AS SELECT 1 as id, 'modified' as status" # modified + dolt sql -q "CREATE VIEW new_view AS SELECT 'added' as status" # added + dolt sql -q "DROP TRIGGER original_trigger" # removed + dolt sql -q "CREATE EVENT new_event ON SCHEDULE EVERY 1 HOUR DO SELECT 1" # added + + # Test that the table exists and has correct schema + run dolt sql -r csv -q 'DESCRIBE dolt_diff_dolt_schemas' + [ "$status" -eq 0 ] + [[ "$output" =~ "to_type,varchar(64)" ]] || false + [[ "$output" =~ "to_name,varchar(64)" ]] || false + [[ "$output" =~ "to_fragment,longtext" ]] || false + [[ "$output" =~ "to_extra,json" ]] || false + [[ "$output" =~ "to_sql_mode,varchar(256)" ]] || false + [[ "$output" =~ "to_commit,varchar(1023)" ]] || false + [[ "$output" =~ "to_commit_date,datetime(6)" ]] || false + [[ "$output" =~ "from_type,varchar(64)" ]] || false + [[ "$output" =~ "from_name,varchar(64)" ]] || false + [[ "$output" =~ "from_fragment,longtext" ]] || false + [[ "$output" =~ "from_extra,json" ]] || false + [[ "$output" =~ "from_sql_mode,varchar(256)" ]] || false + [[ "$output" =~ "from_commit,varchar(1023)" ]] || false + [[ "$output" =~ "from_commit_date,datetime(6)" ]] || false + [[ "$output" =~ "diff_type,varchar(1023)" ]] || false + + # Test actual diff functionality - should show complete history plus working changes + # Initial commit: 2 added (original_view, original_trigger) + # Working changes: 4 changes (original_view modified, new_view added, new_event added, original_trigger removed) + # Total: 6 changes + run dolt sql -q 'SELECT COUNT(*) FROM dolt_diff_dolt_schemas' + [ "$status" -eq 0 ] + [[ "$output" =~ " 6 " ]] || false + + # Test that we have changes of different types + run dolt sql -q 'SELECT COUNT(*) FROM dolt_diff_dolt_schemas WHERE diff_type = "added"' + [ "$status" -eq 0 ] + [[ "$output" =~ " 4 " ]] || false # initial: original_view, original_trigger + working: new_view, new_event + + run dolt sql -q 'SELECT COUNT(*) FROM dolt_diff_dolt_schemas WHERE diff_type = "modified"' + [ "$status" -eq 0 ] + [[ "$output" =~ " 1 " ]] || false # original_view + + run dolt sql -q 'SELECT COUNT(*) FROM dolt_diff_dolt_schemas WHERE diff_type = "removed"' + [ "$status" -eq 0 ] + [[ "$output" =~ " 1 " ]] || false # original_trigger + + # Test that we can identify specific changes + run dolt sql -q 'SELECT to_name FROM dolt_diff_dolt_schemas WHERE diff_type = "added" ORDER BY to_name' + [ "$status" -eq 0 ] + [[ "$output" =~ "new_event" ]] || false + [[ "$output" =~ "new_view" ]] || false + + run dolt sql -q 'SELECT to_name FROM dolt_diff_dolt_schemas WHERE diff_type = "modified"' + [ "$status" -eq 0 ] + [[ "$output" =~ "original_view" ]] || false + + run dolt sql -q 'SELECT from_name FROM dolt_diff_dolt_schemas WHERE diff_type = "removed"' + [ "$status" -eq 0 ] + [[ "$output" =~ "original_trigger" ]] || false + + # Test that diff_type values are correct + run dolt sql -q 'SELECT DISTINCT diff_type FROM dolt_diff_dolt_schemas ORDER BY diff_type' + [ "$status" -eq 0 ] + [[ "$output" =~ "added" ]] || false + [[ "$output" =~ "modified" ]] || false + [[ "$output" =~ "removed" ]] || false + + # Test that from_commit is always populated (should be commit hashes) + run dolt sql -q 'SELECT COUNT(*) FROM dolt_diff_dolt_schemas WHERE from_commit IS NOT NULL' + [ "$status" -eq 0 ] + [[ "$output" =~ "6" ]] || false + + # Test that from_commit is a valid commit hash (not "EMPTY" or "WORKING") + run dolt sql -q 'SELECT DISTINCT from_commit FROM dolt_diff_dolt_schemas' + [ "$status" -eq 0 ] + [[ ! "$output" =~ "EMPTY" ]] || false + [[ ! "$output" =~ "WORKING" ]] || false + # Should be a 32-character hash + [[ "$output" =~ [a-z0-9]{32} ]] || false + + # Test timestamp conversion works correctly (was causing conversion errors) + run dolt sql -q "SELECT * FROM dolt_diff_dolt_schemas LIMIT 1" -r vertical + [ "$status" -eq 0 ] + [[ "$output" =~ "to_commit_date:" ]] || false + [[ "$output" =~ "from_commit_date:" ]] || false +} + @test "system-tables: query dolt_history_ system table" { dolt sql -q "create table test (pk int, c1 int, primary key(pk))" dolt add test