Skip to content

Conversation

@elianddb
Copy link
Contributor

Fixes dolthub/dolt#9496
Allow DECIMAL foreign key creation with different precision/scale but enforce strict constraint validation
MySQL allows DECIMAL foreign keys with different precision/scale but rejects constraint violations based on exact scale matching

@elianddb elianddb force-pushed the elianddb/9496-decimal-foreign-keys branch 5 times, most recently from df4ab3e to b35e54d Compare July 14, 2025 23:10
Copy link
Contributor Author

@elianddb elianddb Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just decided to iteratively update error messages as I work on them. A mysql ver suffix seems to be the easiest way to differentiate error messages since it communicates where it comes from. This lessens the number of conflicts with existing test cases, and is targeted mainly to the issue at hand.

@elianddb elianddb force-pushed the elianddb/9496-decimal-foreign-keys branch 5 times, most recently from 5b50c84 to 27c16cf Compare July 15, 2025 22:12
@elianddb elianddb requested a review from jycor July 15, 2025 23:38
Copy link
Contributor

@jycor jycor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

ExpectedErr: sql.ErrForeignKeyChildViolation,
},
{
Query: "insert into child_dec_4_2 values (99.99);",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nitpick.

Move this test to be grouped up with other inserts on the same table

Comment on lines 546 to 578
// validateDecimalConstraints checks that decimal foreign key columns have compatible scales.
func (reference *ForeignKeyReferenceHandler) validateDecimalConstraints(row sql.Row) error {
if reference.RowMapper.Index == nil {
return nil
}

indexColumnTypes := reference.RowMapper.Index.ColumnExpressionTypes()
for i := range reference.ForeignKey.Columns {
if i >= len(indexColumnTypes) {
continue
}

childColIdx := reference.RowMapper.IndexPositions[i]
childType := reference.RowMapper.SourceSch[childColIdx].Type
parentType := indexColumnTypes[i].Type

// For decimal types, check scale compatibility (following existing pattern for type-specific validation)
if childDecimal, ok := childType.(sql.DecimalType); ok {
if parentDecimal, ok := parentType.(sql.DecimalType); ok {
if childDecimal.Scale() != parentDecimal.Scale() {
return sql.ErrForeignKeyChildViolation.New(reference.ForeignKey.Name, reference.ForeignKey.Table,
reference.ForeignKey.ParentTable, reference.RowMapper.GetKeyString(row))
}
}
}
}

return nil
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// validateDecimalConstraints checks that decimal foreign key columns have compatible scales.
func (reference *ForeignKeyReferenceHandler) validateDecimalConstraints(row sql.Row) error {
if reference.RowMapper.Index == nil {
return nil
}
indexColumnTypes := reference.RowMapper.Index.ColumnExpressionTypes()
for i := range reference.ForeignKey.Columns {
if i >= len(indexColumnTypes) {
continue
}
childColIdx := reference.RowMapper.IndexPositions[i]
childType := reference.RowMapper.SourceSch[childColIdx].Type
parentType := indexColumnTypes[i].Type
// For decimal types, check scale compatibility (following existing pattern for type-specific validation)
if childDecimal, ok := childType.(sql.DecimalType); ok {
if parentDecimal, ok := parentType.(sql.DecimalType); ok {
if childDecimal.Scale() != parentDecimal.Scale() {
return sql.ErrForeignKeyChildViolation.New(reference.ForeignKey.Name, reference.ForeignKey.Table,
reference.ForeignKey.ParentTable, reference.RowMapper.GetKeyString(row))
}
}
}
}
return nil
}
// validateDecimalConstraints checks that decimal foreign key columns have compatible scales.
func (reference *ForeignKeyReferenceHandler) validateDecimalConstraints(row sql.Row) error {
if reference.RowMapper.Index == nil {
return nil
}
indexColumnTypes := reference.RowMapper.Index.ColumnExpressionTypes()
for parentIdx, parentCol := range indexColumnTypes {
if parentIdx >= len(indexColumnTypes) {
break
}
parentType := parentCol.Type
childColIdx := reference.RowMapper.IndexPositions[parentIdx]
childType := reference.RowMapper.SourceSch[childColIdx].Type
childDecimal, ok := childType.(sql.DecimalType)
if !ok {
continue
}
parentDecimal, ok := parentType.(sql.DecimalType)
if !ok {
continue
}
if childDecimal.Scale() != parentDecimal.Scale() {
return sql.ErrForeignKeyChildViolation.New(
reference.ForeignKey.Name,
reference.ForeignKey.Table,
reference.ForeignKey.ParentTable,
reference.RowMapper.GetKeyString(row),
)
}
}
return nil
}

ExpectedErr: sql.ErrForeignKeyParentViolation,
Query: "insert into child_dec_3_2 values (1.23);",
Expected: []sql.Row{
{types.NewOkResult(1)},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch

elianddb and others added 11 commits July 16, 2025 10:00
Allow foreign key references between DECIMAL columns with different precision/scale to match MySQL behavior.

- Modified foreignKeyComparableTypes() to allow DECIMAL types with different precision/scale
- Updated test expectations to reflect correct behavior with current decimal implementation
- All foreign key tests pass

Fixes dolthub/dolt#9496

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Added DECIMAL foreign key compatibility test suite to foreign_key_queries.go
- Tests foreign key creation between DECIMAL columns with different precision/scale
- Tests constraint validation with both valid and invalid inserts
- Validates that the fix works for various DECIMAL type combinations
- Remove verbose comments and dead code
- Reduce nesting in shouldRejectDecimalMatch function
- Use existing patterns from codebase
- Remove unnecessary vitess import
- Simplify type checking logic

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Update ErrForeignKeyChildViolation error message to match MySQL format exactly
- Include database name, table name, constraint name, and column names
- Error now shows: "cannot add or update a child row: a foreign key constraint fails (`db`.`table`, CONSTRAINT `name` FOREIGN KEY (`cols`) REFERENCES `parent` (`parent_cols`))"
- Maintains functional behavior while providing MySQL-compatible error messages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@elianddb elianddb force-pushed the elianddb/9496-decimal-foreign-keys branch from e2ede1e to 774726d Compare July 16, 2025 17:09
@elianddb elianddb merged commit 97f03f3 into main Jul 16, 2025
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DECIMALs with foreign key behavior differs from MySQL

3 participants