Skip to content
30 changes: 2 additions & 28 deletions enginetest/queries/script_queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -10856,81 +10856,68 @@ where
},
Assertions: []ScriptTestAssertion{
{
Skip: true,
Query: "create table child_datetime0 (dt datetime, foreign key (dt) references parent_datetime6(dt));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child_datetime0 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
{
Skip: true,
Query: "create table child_datetime6 (dt datetime(6), foreign key (dt) references parent_datetime0(dt));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child_datetime6 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},

{
Skip: true,
Query: "create table child1_timestamp0 (ts timestamp, foreign key (ts) references parent_datetime0(dt));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child1_timestamp0 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
{
Skip: true,
Query: "create table child2_timestamp0 (ts timestamp, foreign key (ts) references parent_datetime6(dt));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child2_timestamp0 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},

{
Skip: true,
Query: "create table child1_timestamp6 (ts timestamp(6), foreign key (ts) references parent_datetime0(dt));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child1_timestamp6 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
{
Skip: true,
Query: "create table child2_timestamp6 (ts timestamp(6), foreign key (ts) references parent_datetime6(dt));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child2_timestamp6 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
{
Skip: true,
Query: "insert into child2_timestamp6 values ('2001-02-03 12:34:56.123456');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
Expand Down Expand Up @@ -10966,81 +10953,68 @@ where
},
Assertions: []ScriptTestAssertion{
{
Skip: true,
Query: "create table child_timestamp0 (ts timestamp, foreign key (ts) references parent_timestamp6(ts));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child_timestamp0 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
{
Skip: true,
Query: "create table child_timestamp6 (ts timestamp(6), foreign key (ts) references parent_timestamp0(ts));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child_timestamp6 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},

{
Skip: true,
Query: "create table child1_datetime0 (dt datetime, foreign key (dt) references parent_timestamp0(ts));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child1_datetime0 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
{
Skip: true,
Query: "create table child2_datetime0 (dt datetime, foreign key (dt) references parent_timestamp6(ts));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child2_datetime0 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},

{
Skip: true,
Query: "create table child1_datetime6 (dt datetime(6), foreign key (dt) references parent_timestamp0(ts));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child1_datetime6 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
{
Skip: true,
Query: "create table child2_datetime6 (dt datetime(6), foreign key (dt) references parent_timestamp6(ts));",
Expected: []sql.Row{
{types.NewOkResult(0)},
},
},
{
Skip: true,
Query: "insert into child2_datetime6 values ('2001-02-03 12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
{
Skip: true,
Query: "insert into child2_datetime6 values ('2001-02-03 12:34:56.123456');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
},
Expand Down Expand Up @@ -11082,9 +11056,9 @@ where
},
},
{
Skip: true,
Query: "insert into child_time0 values ('12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
Skip: true, // TODO: Fix TIME precision handling in foreign key constraints (https://github.com/dolthub/dolt/issues/9544)
},
{
Query: "create table child_time6 (t time(6), foreign key (t) references parent_time0(t));",
Expand All @@ -11093,9 +11067,9 @@ where
},
},
{
Skip: true,
Query: "insert into child_time6 values ('12:34:56');",
ExpectedErr: sql.ErrForeignKeyChildViolation,
Skip: true, // TODO: Fix TIME precision handling in foreign key constraints (https://github.com/dolthub/dolt/issues/9544)
},
},
},
Expand Down
10 changes: 10 additions & 0 deletions sql/plan/alter_foreign_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,16 @@ func foreignKeyComparableTypes(ctx *sql.Context, type1 sql.Type, type2 sql.Type)
t1 := type1.Type()
t2 := type2.Type()

// Handle time-related types with different precisions or cross-type references
if (types.IsTime(type1) || types.IsTimespan(type1)) && (types.IsTime(type2) || types.IsTimespan(type2)) {
// MySQL allows time-related types to reference each other in foreign keys:
// - DATETIME can reference DATETIME with different precision
// - TIMESTAMP can reference TIMESTAMP with different precision
// - DATETIME can reference TIMESTAMP and vice versa
// - TIME can reference TIME with different precision
return true
}

// Handle same-type cases for special types
if t1 == t2 {
switch t1 {
Expand Down
51 changes: 33 additions & 18 deletions sql/plan/foreign_key_editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ func (reference *ForeignKeyReferenceHandler) CheckReference(ctx *sql.Context, ro
}
defer rowIter.Close(ctx)

_, err = rowIter.Next(ctx)
parentRow, err := rowIter.Next(ctx)
if err != nil && err != io.EOF {
// For SET types, conversion failures during foreign key validation should be treated as foreign key violations
if sql.ErrConvertingToSet.Is(err) || sql.ErrInvalidSetValue.Is(err) {
Expand All @@ -518,9 +518,10 @@ func (reference *ForeignKeyReferenceHandler) CheckReference(ctx *sql.Context, ro
}
if err == nil {
// We have a parent row, but check for type-specific validation
if validationErr := reference.validateDecimalConstraints(row); validationErr != nil {
if validationErr := reference.validateColumnTypeConstraints(ctx, row, parentRow); validationErr != nil {
return validationErr
}

// We have a parent row so throw no error
return nil
}
Expand Down Expand Up @@ -548,33 +549,46 @@ func (reference *ForeignKeyReferenceHandler) CheckReference(ctx *sql.Context, ro
reference.ForeignKey.ParentTable, reference.RowMapper.GetKeyString(row))
}

// validateDecimalConstraints checks that decimal foreign key columns have compatible scales.
func (reference *ForeignKeyReferenceHandler) validateDecimalConstraints(row sql.Row) error {
if reference.RowMapper.Index == nil {
// validateColumnTypeConstraints enforces foreign key type validation between child and parent columns in a foreign key relationship.
func (reference *ForeignKeyReferenceHandler) validateColumnTypeConstraints(ctx *sql.Context, childRow sql.Row, parentRow sql.Row) error {
mapper := reference.RowMapper
if mapper.Index == nil {
return nil
}
indexColumnTypes := reference.RowMapper.Index.ColumnExpressionTypes()
for parentIdx, parentCol := range indexColumnTypes {
if parentIdx >= len(reference.RowMapper.IndexPositions) {

for parentIdx, parentCol := range mapper.Index.ColumnExpressionTypes() {
if parentIdx >= len(mapper.IndexPositions) {
break
}

parentType := parentCol.Type
childColIdx := reference.RowMapper.IndexPositions[parentIdx]
childType := reference.RowMapper.SourceSch[childColIdx].Type
childDecimal, ok := childType.(sql.DecimalType)
if !ok {
continue
childType := mapper.SourceSch[mapper.IndexPositions[parentIdx]].Type
hasViolation := false

// For decimal types, scales must match exactly
childDecimal, childOk := childType.(sql.DecimalType)
parentDecimal, parentOk := parentType.(sql.DecimalType)
if childOk && parentOk {
hasViolation = childDecimal.Scale() != parentDecimal.Scale()
}
parentDecimal, ok := parentType.(sql.DecimalType)
if !ok {
continue

// For time types, require exact type matching (including precision)
// TODO: The TIME type currently normalizes all precisions to TIME(6) internally,
// which means TIME and TIME(n) are all treated as TIME(6). This prevents proper
// precision validation between different TIME types in foreign keys.
// See time.go:"TIME is implemented as TIME(6)."
isChildTime := types.IsTime(childType) || types.IsTimespan(childType)
isParentTime := types.IsTime(parentType) || types.IsTimespan(parentType)
if isChildTime && isParentTime {
hasViolation = hasViolation || !childType.Equals(parentType)
}
if childDecimal.Scale() != parentDecimal.Scale() {

if hasViolation {
return sql.ErrForeignKeyChildViolation.New(
reference.ForeignKey.Name,
reference.ForeignKey.Table,
reference.ForeignKey.ParentTable,
reference.RowMapper.GetKeyString(row),
mapper.GetKeyString(childRow),
)
}
}
Expand Down Expand Up @@ -638,6 +652,7 @@ func (mapper *ForeignKeyRowMapper) GetIter(ctx *sql.Context, row sql.Row, refChe
}

targetType := mapper.SourceSch[rowPos].Type

// Transform the type of the value in this row to the one in the other table for the index lookup, if necessary
if mapper.TargetTypeConversions != nil && mapper.TargetTypeConversions[rowPos] != nil {
var err error
Expand Down
Loading