diff --git a/go/libraries/doltcore/doltdb/system_table.go b/go/libraries/doltcore/doltdb/system_table.go index b3ca29234f4..a753e099274 100644 --- a/go/libraries/doltcore/doltdb/system_table.go +++ b/go/libraries/doltcore/doltdb/system_table.go @@ -164,6 +164,7 @@ var writeableSystemTables = []string{ IgnoreTableName, RebaseTableName, WorkflowsTableName, + WorkflowEventsTableName, } var persistedSystemTables = []string{ @@ -320,7 +321,7 @@ const ( ) const ( - // WorkflowsTableName is the dolt CI system table name + // WorkflowsTableName is the dolt CI workflows system table name WorkflowsTableName = "dolt_ci_workflows" // WorkflowsNameColName is the name of the column storing the name of the workflow. @@ -331,6 +332,18 @@ const ( // WorkflowsUpdatedAtColName is the name of the column storing the update time of the row entry. WorkflowsUpdatedAtColName = "updated_at" + + // WorkflowEventsTableName is the dolt CI workflow events system table name + WorkflowEventsTableName = "dolt_ci_workflow_events" + + // WorkflowEventsIdPkColName is the name of the primary key id column on the workflow events table. + WorkflowEventsIdPkColName = "id" + + // WorkflowEventsWorkflowNameFkColName is the name of the workflows name foreign key in the workflow events table. + WorkflowEventsWorkflowNameFkColName = "workflow_name_fk" + + // WorkflowEventsEventTypeColName is the name of the event type column in the workflow events table. + WorkflowEventsEventTypeColName = "event_type" ) const ( diff --git a/go/libraries/doltcore/schema/reserved_tags.go b/go/libraries/doltcore/schema/reserved_tags.go index d16c1e15c5b..2a1053851ce 100644 --- a/go/libraries/doltcore/schema/reserved_tags.go +++ b/go/libraries/doltcore/schema/reserved_tags.go @@ -126,4 +126,13 @@ const ( // WorkflowsUpdatedAtTag is the tag of the updated_at column in the workflows table WorkflowsUpdatedAtTag + + // WorkflowEventsIdTag is the tag of the id column in the workflow events table + WorkflowEventsIdTag + + // WorkflowEventsWorkflowNameFkTag is the tag of the workflow name fk column in the workflow events table + WorkflowEventsWorkflowNameFkTag + + // WorkflowEventsEventTypeTag is the tag of the events typ column in the workflow events table + WorkflowEventsEventTypeTag ) diff --git a/go/libraries/doltcore/sqle/ci_workflow_events.go b/go/libraries/doltcore/sqle/ci_workflow_events.go new file mode 100644 index 00000000000..a33aee2cc96 --- /dev/null +++ b/go/libraries/doltcore/sqle/ci_workflow_events.go @@ -0,0 +1,401 @@ +// 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 sqle + +import ( + "fmt" + "github.com/dolthub/dolt/go/libraries/doltcore/branch_control" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/schema" + "github.com/dolthub/dolt/go/libraries/doltcore/schema/typeinfo" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dtables" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/index" + "github.com/dolthub/dolt/go/store/hash" + stypes "github.com/dolthub/dolt/go/store/types" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" + "github.com/dolthub/vitess/go/sqltypes" +) + +const workflowEventsDefaultRowCount = 10 + +var _ sql.StatisticsTable = (*WorkflowEventsTable)(nil) +var _ sql.Table = (*WorkflowEventsTable)(nil) + +var _ sql.UpdatableTable = (*WorkflowEventsTable)(nil) + +var _ sql.DeletableTable = (*WorkflowEventsTable)(nil) +var _ sql.InsertableTable = (*WorkflowEventsTable)(nil) + +var _ sql.ReplaceableTable = (*WorkflowEventsTable)(nil) + +// WorkflowEventsTable is a sql.Table implementation that implements a system table which stores dolt CI workflow events +type WorkflowEventsTable struct { + dbName string + db Database + ddb *doltdb.DoltDB + backingTable dtables.VersionableTable +} + +// NewWorkflowEventsTable creates a WorkflowEventsTable +func NewWorkflowEventsTable(_ *sql.Context, db Database, backingTable dtables.VersionableTable) sql.Table { + return &WorkflowEventsTable{db: db, ddb: db.GetDoltDB(), backingTable: backingTable, dbName: db.Name()} +} + +// NewEmptyWorkflowEventsTable creates a WorkflowEventsTable +func NewEmptyWorkflowEventsTable(_ *sql.Context, db Database) sql.Table { + return &WorkflowEventsTable{db: db, dbName: db.Name(), ddb: db.GetDoltDB(), backingTable: nil} +} + +func (w *WorkflowEventsTable) Name() string { + return doltdb.WorkflowEventsTableName +} + +func (w *WorkflowEventsTable) String() string { + return doltdb.WorkflowEventsTableName +} + +// Schema is a sql.Table interface function that gets the sql.Schema of the dolt_ignore system table. +func (w *WorkflowEventsTable) Schema() sql.Schema { + return []*sql.Column{ + {Name: doltdb.WorkflowEventsIdPkColName, Type: types.MustCreateString(sqltypes.VarChar, 36, sql.Collation_utf8mb4_0900_ai_ci), Source: doltdb.WorkflowEventsTableName, PrimaryKey: true, Nullable: false}, + {Name: doltdb.WorkflowEventsWorkflowNameFkColName, Type: types.MustCreateString(sqltypes.VarChar, 2048, sql.Collation_utf8mb4_0900_ai_ci), Source: doltdb.WorkflowEventsTableName, PrimaryKey: false, Nullable: false}, + {Name: doltdb.WorkflowEventsEventTypeColName, Type: types.Int32, Source: doltdb.WorkflowEventsTableName, PrimaryKey: false, Nullable: false}, + } +} + +func (w *WorkflowEventsTable) Collation() sql.CollationID { + return sql.Collation_Default +} + +func (w *WorkflowEventsTable) Partitions(ctx *sql.Context) (sql.PartitionIter, error) { + if w.backingTable == nil { + // no backing table; return an empty iter. + return index.SinglePartitionIterFromNomsMap(nil), nil + } + return w.backingTable.Partitions(ctx) +} + +func (w *WorkflowEventsTable) PartitionRows(context *sql.Context, partition sql.Partition) (sql.RowIter, error) { + if w.backingTable == nil { + // no backing table; return an empty iter. + return sql.RowsToRowIter(), nil + } + return w.backingTable.PartitionRows(context, partition) +} + +func (w *WorkflowEventsTable) DataLength(ctx *sql.Context) (uint64, error) { + numBytesPerRow := schema.SchemaAvgLength(w.Schema()) + numRows, _, err := w.RowCount(ctx) + if err != nil { + return 0, err + } + return numBytesPerRow * numRows, nil +} + +func (w *WorkflowEventsTable) RowCount(_ *sql.Context) (uint64, bool, error) { + return workflowEventsDefaultRowCount, false, nil +} + +// Inserter returns an Inserter for this table. The Inserter will get one call to Insert() for each row to be +// inserted, and will end with a call to Close() to finalize the insert operation. +func (w *WorkflowEventsTable) Inserter(context *sql.Context) sql.RowInserter { + return newWorkflowEventsWriter(w) +} + +// Updater returns a RowUpdater for this table. The RowUpdater will have Update called once for each row to be +// updated, followed by a call to Close() when all rows have been processed. +func (w *WorkflowEventsTable) Updater(ctx *sql.Context) sql.RowUpdater { + return newWorkflowEventsWriter(w) +} + +// Deleter returns a RowDeleter for this table. The RowDeleter will get one call to Delete for each row to be deleted, +// and will end with a call to Close() to finalize the delete operation. +func (w *WorkflowEventsTable) Deleter(context *sql.Context) sql.RowDeleter { + return newWorkflowEventsWriter(w) +} + +// Replacer returns a RowReplacer for this table. The RowReplacer will have Insert and optionally Delete called once +// for each row, followed by a call to Close() when all rows have been processed. +func (w *WorkflowEventsTable) Replacer(ctx *sql.Context) sql.RowReplacer { + return newWorkflowEventsWriter(w) +} + +var _ sql.RowReplacer = (*workflowEventsWriter)(nil) +var _ sql.RowUpdater = (*workflowEventsWriter)(nil) +var _ sql.RowInserter = (*workflowEventsWriter)(nil) +var _ sql.RowDeleter = (*workflowEventsWriter)(nil) + +type workflowEventsWriter struct { + it *WorkflowEventsTable + errDuringStatementBegin error + prevHash *hash.Hash + tableWriter dsess.TableWriter +} + +func newWorkflowEventsWriter(it *WorkflowEventsTable) *workflowEventsWriter { + return &workflowEventsWriter{it, nil, nil, nil} +} + +// StatementBegin is called before the first operation of a statement. Integrators should mark the state of the data +// in some way that it may be returned to in the case of an error. +func (w *workflowEventsWriter) StatementBegin(ctx *sql.Context) { + dbName := ctx.GetCurrentDatabase() + dSess := dsess.DSessFromSess(ctx.Session) + + // check write perms + if err := dsess.CheckAccessForDb(ctx, w.it.db, branch_control.Permissions_Write); err != nil { + w.errDuringStatementBegin = err + return + } + + // TODO: this needs to use a revision qualified name + roots, _ := dSess.GetRoots(ctx, dbName) + dbState, ok, err := dSess.LookupDbState(ctx, dbName) + if err != nil { + w.errDuringStatementBegin = err + return + } + if !ok { + w.errDuringStatementBegin = fmt.Errorf("no root value found in session") + return + } + + prevHash, err := roots.Working.HashOf() + if err != nil { + w.errDuringStatementBegin = err + return + } + + w.prevHash = &prevHash + + found, err := roots.Working.HasTable(ctx, doltdb.TableName{Name: doltdb.WorkflowEventsTableName}) + + if err != nil { + w.errDuringStatementBegin = err + return + } + + if !found { + // TODO: This is effectively a duplicate of the schema declaration above in a different format. + // We should find a way to not repeat ourselves. + colCollection := schema.NewColCollection( + schema.Column{ + Name: doltdb.WorkflowEventsIdPkColName, + Tag: schema.WorkflowEventsIdTag, + Kind: stypes.StringKind, + IsPartOfPK: true, + TypeInfo: typeinfo.FromKind(stypes.StringKind), + Default: "", + AutoIncrement: false, + Comment: "", + Constraints: []schema.ColConstraint{schema.NotNullConstraint{}}, + }, + schema.Column{ + Name: doltdb.WorkflowEventsWorkflowNameFkColName, + Tag: schema.WorkflowEventsWorkflowNameFkTag, + Kind: stypes.StringKind, + IsPartOfPK: false, + TypeInfo: typeinfo.FromKind(stypes.StringKind), + Default: "", + AutoIncrement: false, + Comment: "", + Constraints: []schema.ColConstraint{schema.NotNullConstraint{}}, + }, + schema.Column{ + Name: doltdb.WorkflowEventsEventTypeColName, + Tag: schema.WorkflowEventsEventTypeTag, + Kind: stypes.IntKind, + IsPartOfPK: false, + TypeInfo: typeinfo.FromKind(stypes.IntKind), + Default: "", + AutoIncrement: false, + Comment: "", + Constraints: []schema.ColConstraint{schema.NotNullConstraint{}}, + }, + ) + + newSchema, err := schema.NewSchema(colCollection, nil, schema.Collation_Default, nil, nil) + if err != nil { + w.errDuringStatementBegin = err + return + } + + // underlying table doesn't exist. Record this, then create the table. + newRootValue, err := doltdb.CreateEmptyTable(ctx, roots.Working, doltdb.TableName{Name: doltdb.WorkflowEventsTableName}, newSchema) + if err != nil { + w.errDuringStatementBegin = err + return + } + + if dbState.WorkingSet() == nil { + w.errDuringStatementBegin = doltdb.ErrOperationNotSupportedInDetachedHead + return + } + + // We use WriteSession.SetWorkingSet instead of DoltSession.SetWorkingRoot because we want to avoid modifying the root + // until the end of the transaction, but we still want the WriteSession to be able to find the newly + // created table. + + if ws := dbState.WriteSession(); ws != nil { + err = ws.SetWorkingSet(ctx, dbState.WorkingSet().WithWorkingRoot(newRootValue)) + if err != nil { + w.errDuringStatementBegin = err + return + } + } + + tbl, exists, err := newRootValue.GetTable(ctx, doltdb.TableName{Name: doltdb.WorkflowEventsTableName}) + if err != nil { + w.errDuringStatementBegin = err + return + } + + if !exists { + w.errDuringStatementBegin = fmt.Errorf("failed to create %s table", doltdb.WorkflowEventsTableName) + return + } + + sfkc := sql.ForeignKeyConstraint{ + Name: fmt.Sprintf("%s_%s", doltdb.WorkflowEventsTableName, doltdb.WorkflowEventsWorkflowNameFkColName), + Database: w.it.dbName, + Table: doltdb.WorkflowEventsTableName, + Columns: []string{doltdb.WorkflowEventsWorkflowNameFkColName}, + ParentDatabase: w.it.dbName, + ParentTable: doltdb.WorkflowsTableName, + ParentColumns: []string{doltdb.WorkflowsNameColName}, + OnDelete: sql.ForeignKeyReferentialAction_Cascade, + OnUpdate: sql.ForeignKeyReferentialAction_DefaultAction, + IsResolved: false, + } + + onUpdateRefAction, err := parseFkReferentialAction(sfkc.OnUpdate) + if err != nil { + w.errDuringStatementBegin = err + return + } + + onDeleteRefAction, err := parseFkReferentialAction(sfkc.OnDelete) + if err != nil { + w.errDuringStatementBegin = err + return + } + + doltFk, err := createDTableForeignKey(ctx, newRootValue, tbl, newSchema, sfkc, onUpdateRefAction, onDeleteRefAction, w.it.db.schemaName) + if err != nil { + w.errDuringStatementBegin = err + return + } + + fkc, err := newRootValue.GetForeignKeyCollection(ctx) + if err != nil { + w.errDuringStatementBegin = err + return + } + + err = fkc.AddKeys(doltFk) + if err != nil { + w.errDuringStatementBegin = err + return + } + + newRootValue, err = newRootValue.PutForeignKeyCollection(ctx, fkc) + if err != nil { + w.errDuringStatementBegin = err + return + } + + err = dSess.SetWorkingRoot(ctx, dbName, newRootValue) + if err != nil { + w.errDuringStatementBegin = err + return + } + + dbState, ok, err = dSess.LookupDbState(ctx, dbName) + if err != nil { + w.errDuringStatementBegin = err + return + } + if !ok { + w.errDuringStatementBegin = fmt.Errorf("no root value found in session") + return + } + } + + if ws := dbState.WriteSession(); ws != nil { + tableWriter, err := ws.GetTableWriter(ctx, doltdb.TableName{Name: doltdb.WorkflowEventsTableName}, dbName, dSess.SetWorkingRoot) + if err != nil { + w.errDuringStatementBegin = err + return + } + w.tableWriter = tableWriter + tableWriter.StatementBegin(ctx) + } +} + +// DiscardChanges is called if a statement encounters an error, and all current changes since the statement beginning +// should be discarded. +func (w *workflowEventsWriter) DiscardChanges(ctx *sql.Context, errorEncountered error) error { + if w.tableWriter != nil { + return w.tableWriter.DiscardChanges(ctx, errorEncountered) + } + return nil +} + +// StatementComplete is called after the last operation of the statement, indicating that it has successfully completed. +// The mark set in StatementBegin may be removed, and a new one should be created on the next StatementBegin. +func (w *workflowEventsWriter) StatementComplete(ctx *sql.Context) error { + if w.tableWriter != nil { + return w.tableWriter.StatementComplete(ctx) + } + return nil +} + +// Insert inserts the row given, returning an error if it cannot. Insert will be called once for each row to process +// for the insert operation, which may involve many rows. After all rows in an operation have been processed, Close +// is called. +func (w *workflowEventsWriter) Insert(ctx *sql.Context, r sql.Row) error { + if err := w.errDuringStatementBegin; err != nil { + return err + } + return w.tableWriter.Insert(ctx, r) +} + +// Update the given row. Provides both the old and new rows. +func (w *workflowEventsWriter) Update(ctx *sql.Context, old sql.Row, new sql.Row) error { + if err := w.errDuringStatementBegin; err != nil { + return err + } + return w.tableWriter.Update(ctx, old, new) +} + +// Delete deletes the given row. Returns ErrDeleteRowNotFound if the row was not found. Delete will be called once for +// each row to process for the delete operation, which may involve many rows. After all rows have been processed, +// Close is called. +func (w workflowEventsWriter) Delete(ctx *sql.Context, r sql.Row) error { + if err := w.errDuringStatementBegin; err != nil { + return err + } + return w.tableWriter.Delete(ctx, r) +} + +// Close finalizes the delete operation, persisting the result. +func (w *workflowEventsWriter) Close(ctx *sql.Context) error { + if w.tableWriter != nil { + return w.tableWriter.Close(ctx) + } + return nil +} diff --git a/go/libraries/doltcore/sqle/dtables/ci_workflows.go b/go/libraries/doltcore/sqle/ci_workflows.go similarity index 98% rename from go/libraries/doltcore/sqle/dtables/ci_workflows.go rename to go/libraries/doltcore/sqle/ci_workflows.go index 6f1351d7404..cb0b83bb0c7 100644 --- a/go/libraries/doltcore/sqle/dtables/ci_workflows.go +++ b/go/libraries/doltcore/sqle/ci_workflows.go @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dtables +package sqle import ( "fmt" + "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dtables" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" @@ -45,11 +46,11 @@ var _ sql.ReplaceableTable = (*WorkflowsTable)(nil) // WorkflowsTable is a sql.Table implementation that implements a system table which stores dolt CI workflows type WorkflowsTable struct { ddb *doltdb.DoltDB - backingTable VersionableTable + backingTable dtables.VersionableTable } // NewWorkflowsTable creates a WorkflowsTable -func NewWorkflowsTable(_ *sql.Context, ddb *doltdb.DoltDB, backingTable VersionableTable) sql.Table { +func NewWorkflowsTable(_ *sql.Context, ddb *doltdb.DoltDB, backingTable dtables.VersionableTable) sql.Table { return &WorkflowsTable{ddb: ddb, backingTable: backingTable} } diff --git a/go/libraries/doltcore/sqle/database.go b/go/libraries/doltcore/sqle/database.go index ef2d325f70e..164328f99a5 100644 --- a/go/libraries/doltcore/sqle/database.go +++ b/go/libraries/doltcore/sqle/database.go @@ -408,10 +408,21 @@ func (db Database) getTableInsensitive(ctx *sql.Context, head *doltdb.Commit, ds return nil, false, err } if backingTable == nil { - dt, found = dtables.NewEmptyWorkflowsTable(ctx), true + dt, found = NewEmptyWorkflowsTable(ctx), true } else { versionableTable := backingTable.(dtables.VersionableTable) - dt, found = dtables.NewWorkflowsTable(ctx, db.ddb, versionableTable), true + dt, found = NewWorkflowsTable(ctx, db.ddb, versionableTable), true + } + case doltdb.WorkflowEventsTableName: + backingTable, _, err := db.getTable(ctx, root, doltdb.WorkflowEventsTableName) + if err != nil { + return nil, false, err + } + if backingTable == nil { + dt, found = NewEmptyWorkflowEventsTable(ctx, db), true + } else { + versionableTable := backingTable.(dtables.VersionableTable) + dt, found = NewWorkflowEventsTable(ctx, db, versionableTable), true } case doltdb.LogTableName: if head == nil { diff --git a/go/libraries/doltcore/sqle/dtable_fk.go b/go/libraries/doltcore/sqle/dtable_fk.go new file mode 100644 index 00000000000..954ff5fce85 --- /dev/null +++ b/go/libraries/doltcore/sqle/dtable_fk.go @@ -0,0 +1,122 @@ +// 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 sqle + +import ( + "fmt" + "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" + "github.com/dolthub/dolt/go/libraries/doltcore/schema" + "github.com/dolthub/go-mysql-server/sql" +) + +func createDTableForeignKey( + ctx *sql.Context, + root doltdb.RootValue, + tbl *doltdb.Table, + sch schema.Schema, + sqlFk sql.ForeignKeyConstraint, + onUpdateRefAction, onDeleteRefAction doltdb.ForeignKeyReferentialAction, + schemaName string) (doltdb.ForeignKey, error) { + if !sqlFk.IsResolved { + return doltdb.ForeignKey{ + Name: sqlFk.Name, + TableName: sqlFk.Table, + TableIndex: "", + TableColumns: nil, + ReferencedTableName: sqlFk.ParentTable, + ReferencedTableIndex: "", + ReferencedTableColumns: nil, + OnUpdate: onUpdateRefAction, + OnDelete: onDeleteRefAction, + UnresolvedFKDetails: doltdb.UnresolvedFKDetails{ + TableColumns: sqlFk.Columns, + ReferencedTableColumns: sqlFk.ParentColumns, + }, + }, nil + } + colTags := make([]uint64, len(sqlFk.Columns)) + for i, col := range sqlFk.Columns { + tableCol, ok := sch.GetAllCols().GetByNameCaseInsensitive(col) + if !ok { + return doltdb.ForeignKey{}, fmt.Errorf("table `%s` does not have column `%s`", sqlFk.Table, col) + } + colTags[i] = tableCol.Tag + } + + var refTbl *doltdb.Table + var refSch schema.Schema + if sqlFk.IsSelfReferential() { + refTbl = tbl + refSch = sch + } else { + var ok bool + var err error + // TODO: the parent table can be in another schema + + refTbl, _, ok, err = doltdb.GetTableInsensitive(ctx, root, doltdb.TableName{Name: sqlFk.ParentTable, Schema: schemaName}) + if err != nil { + return doltdb.ForeignKey{}, err + } + if !ok { + return doltdb.ForeignKey{}, fmt.Errorf("referenced table `%s` does not exist", sqlFk.ParentTable) + } + refSch, err = refTbl.GetSchema(ctx) + if err != nil { + return doltdb.ForeignKey{}, err + } + } + + refColTags := make([]uint64, len(sqlFk.ParentColumns)) + for i, name := range sqlFk.ParentColumns { + refCol, ok := refSch.GetAllCols().GetByNameCaseInsensitive(name) + if !ok { + return doltdb.ForeignKey{}, fmt.Errorf("table `%s` does not have column `%s`", sqlFk.ParentTable, name) + } + refColTags[i] = refCol.Tag + } + + var tableIndexName, refTableIndexName string + tableIndex, ok, err := findIndexWithPrefix(sch, sqlFk.Columns) + if err != nil { + return doltdb.ForeignKey{}, err + } + // Use secondary index if found; otherwise it will use empty string, indicating primary key + if ok { + tableIndexName = tableIndex.Name() + } + refTableIndex, ok, err := findIndexWithPrefix(refSch, sqlFk.ParentColumns) + if err != nil { + return doltdb.ForeignKey{}, err + } + // Use secondary index if found; otherwise it will use empty string, indicating primary key + if ok { + refTableIndexName = refTableIndex.Name() + } + return doltdb.ForeignKey{ + Name: sqlFk.Name, + TableName: sqlFk.Table, + TableIndex: tableIndexName, + TableColumns: colTags, + ReferencedTableName: sqlFk.ParentTable, + ReferencedTableIndex: refTableIndexName, + ReferencedTableColumns: refColTags, + OnUpdate: onUpdateRefAction, + OnDelete: onDeleteRefAction, + UnresolvedFKDetails: doltdb.UnresolvedFKDetails{ + TableColumns: sqlFk.Columns, + ReferencedTableColumns: sqlFk.ParentColumns, + }, + }, nil +} diff --git a/go/libraries/doltcore/sqle/tables.go b/go/libraries/doltcore/sqle/tables.go index c9ade2378fe..c4f3027efec 100644 --- a/go/libraries/doltcore/sqle/tables.go +++ b/go/libraries/doltcore/sqle/tables.go @@ -2734,7 +2734,6 @@ func (t *AlterableDoltTable) AddForeignKey(ctx *sql.Context, sqlFk sql.ForeignKe if strings.ToLower(sqlFk.Database) != strings.ToLower(sqlFk.ParentDatabase) || strings.ToLower(sqlFk.Database) != strings.ToLower(t.db.Name()) { return fmt.Errorf("only foreign keys on the same database are currently supported") } - root, err := t.getRoot(ctx) if err != nil { return err @@ -2743,6 +2742,17 @@ func (t *AlterableDoltTable) AddForeignKey(ctx *sql.Context, sqlFk sql.ForeignKe if err != nil { return err } + if err := dsess.CheckAccessForDb(ctx, t.db, branch_control.Permissions_Write); err != nil { + return err + } + // empty string foreign key names are replaced with a generated name elsewhere + if sqlFk.Name != "" && !doltdb.IsValidIdentifier(sqlFk.Name) { + return fmt.Errorf("invalid foreign key name `%s`", sqlFk.Name) + } + + if strings.ToLower(sqlFk.Database) != strings.ToLower(sqlFk.ParentDatabase) || strings.ToLower(sqlFk.Database) != strings.ToLower(t.db.Name()) { + return fmt.Errorf("only foreign keys on the same database are currently supported") + } onUpdateRefAction, err := parseFkReferentialAction(sqlFk.OnUpdate) if err != nil { @@ -2762,6 +2772,7 @@ func (t *AlterableDoltTable) AddForeignKey(ctx *sql.Context, sqlFk sql.ForeignKe if err != nil { return err } + err = fkc.AddKeys(doltFk) if err != nil { return err diff --git a/integration-tests/bats/ci_config.bats b/integration-tests/bats/ci_config.bats index 416bb52cfc3..7444fab2725 100644 --- a/integration-tests/bats/ci_config.bats +++ b/integration-tests/bats/ci_config.bats @@ -60,3 +60,46 @@ teardown() { [ "$status" -eq 1 ] [[ "$output" =~ "table dolt_ci_workflows cannot be altered" ]] || false } + +@test "ci: dolt_ci_workflow_events table should exist on initialized database" { + run dolt sql -q "select * from dolt_ci_workflow_events;" + [ "$status" -eq 0 ] +} + +@test "ci: dolt_ci_workflow_events should allow user inserts and updates" { + run dolt sql -q "insert into dolt_ci_workflows (name, created_at, updated_at) values (uuid(), 'workflow_1', current_timestamp, current_timestamp);" + [ "$status" -eq 0 ] + + run dolt sql -q "select * from dolt_ci_workflows;" + [ "$status" -eq 0 ] + [[ "$output" =~ "workflow_1" ]] || false + + run dolt sql -q "insert into dolt_ci_workflow_events (id, workflow_id_fk, event_type) values (uuid(), 'workflow_1', 0);" + [ "$status" -eq 0 ] + + run dolt sql -q "select * from dolt_ci_workflow_events;" + [ "$status" -eq 0 ] + + run dolt add . + [ "$status" -eq 0 ] + + run dolt commit -m 'add dolt_ci_workflow and dolt_ci_workflow_events entries' + [ "$status" -eq 0 ] + + run dolt sql -q "select event_type from dolt_ci_workflow_events limit 1;" + [ "$status" -eq 0 ] + [[ "$output" =~ "0" ]] || false + + run dolt sql -q "update dolt_ci_workflow_events set event_type = 987 where event_type = 0;" + [ "$status" -eq 0 ] + + run dolt add dolt_ci_workflow_events + [ "$status" -eq 0 ] + + run dolt commit -m 'update dolt_ci_workflow_events entry' + [ "$status" -eq 0 ] + + run dolt sql -q "select event_type from dolt_ci_workflow_events limit 1;" + [ "$status" -eq 0 ] + [[ "$output" =~ "987" ]] || false +}