diff --git a/go.mod b/go.mod index 72b08c73a3..bc64bd29fd 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi v0.0.0-20241119094239-f4e529af734d github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 github.com/dolthub/go-icu-regex v0.0.0-20250303123116-549b8d7cad00 - github.com/dolthub/go-mysql-server v0.19.1-0.20250311212537-909b08b2a5d3 + github.com/dolthub/go-mysql-server v0.19.1-0.20250313005113-73b3865b4145 github.com/dolthub/sqllogictest/go v0.0.0-20240618184124-ca47f9354216 github.com/dolthub/vitess v0.0.0-20250304211657-920ca9ec2b9a github.com/fatih/color v1.13.0 diff --git a/go.sum b/go.sum index 452e9106b7..e8a1f13c81 100644 --- a/go.sum +++ b/go.sum @@ -266,8 +266,8 @@ github.com/dolthub/fslock v0.0.3 h1:iLMpUIvJKMKm92+N1fmHVdxJP5NdyDK5bK7z7Ba2s2U= github.com/dolthub/fslock v0.0.3/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0= github.com/dolthub/go-icu-regex v0.0.0-20250303123116-549b8d7cad00 h1:rh2ij2yTYKJWlX+c8XRg4H5OzqPewbU1lPK8pcfVmx8= github.com/dolthub/go-icu-regex v0.0.0-20250303123116-549b8d7cad00/go.mod h1:ylU4XjUpsMcvl/BKeRRMXSH7e7WBrPXdSLvnRJYrxEA= -github.com/dolthub/go-mysql-server v0.19.1-0.20250311212537-909b08b2a5d3 h1:2Ae4/qoJSIx/rtcyqm2uSyO1dCJGDFuyx7LKTuujb00= -github.com/dolthub/go-mysql-server v0.19.1-0.20250311212537-909b08b2a5d3/go.mod h1:yr+Vv47/YLOKMgiEY+QxHTlbIVpTuiVtkEZ5l+xruY4= +github.com/dolthub/go-mysql-server v0.19.1-0.20250313005113-73b3865b4145 h1:ye9o0LXu3IuBSp5GA45s3IATkhtEMEuqHvvjIBTm6eI= +github.com/dolthub/go-mysql-server v0.19.1-0.20250313005113-73b3865b4145/go.mod h1:yr+Vv47/YLOKMgiEY+QxHTlbIVpTuiVtkEZ5l+xruY4= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20240701202509-2b217167d718 h1:lT7hE5k+0nkBdj/1UOSFwjWpNxf+LCApbRHgnCA17XE= diff --git a/server/analyzer/foreign_key.go b/server/analyzer/foreign_key.go new file mode 100755 index 0000000000..0020e6a78b --- /dev/null +++ b/server/analyzer/foreign_key.go @@ -0,0 +1,74 @@ +// 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 analyzer + +import ( + "strings" + + "github.com/cockroachdb/errors" + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" + + "github.com/dolthub/doltgresql/server/functions/framework" + "github.com/dolthub/doltgresql/server/types" +) + +// validateForeignKeyDefinition validates that the given foreign key definition is valid for creation +func validateForeignKeyDefinition(ctx *sql.Context, fkDef sql.ForeignKeyConstraint, cols map[string]*sql.Column, parentCols map[string]*sql.Column) error { + for i := range fkDef.Columns { + col := cols[strings.ToLower(fkDef.Columns[i])] + parentCol := parentCols[strings.ToLower(fkDef.ParentColumns[i])] + if !foreignKeyComparableTypes(col.Type, parentCol.Type) { + return errors.Errorf("Key columns %q and %q are of incompatible types: %s and %s", col.Name, parentCol.Name, col.Type.String(), parentCol.Type.String()) + } + } + return nil +} + +// foreignKeyComparableTypes returns whether the two given types are able to be used as parent/child columns in a +// foreign key. +func foreignKeyComparableTypes(from sql.Type, to sql.Type) bool { + dtFrom, ok := from.(*types.DoltgresType) + if !ok { + return false // should never be possible + } + + dtTo, ok := to.(*types.DoltgresType) + if !ok { + return false // should never be possible + } + + if dtFrom.Equals(dtTo) { + return true + } + + fromLiteral := expression.NewLiteral(dtFrom.Zero(), from) + toLiteral := expression.NewLiteral(dtTo.Zero(), to) + + // a foreign key between two different types is valid if there is an equality operator on the two types + // TODO: there are some subtleties in postgres not captured by this logic, e.g. a foreign key from double -> int + // is valid, but the reverse is not. This works fine, but is more permissive than postgres is. + eq := framework.GetBinaryFunction(framework.Operator_BinaryEqual).Compile("=", fromLiteral, toLiteral) + if eq == nil || eq.StashedError() != nil { + return false + } + + // Additionally, we need to be able to convert freely between the two types in both directions, since we do this + // during the process of enforcing the constraints + forwardConversion := types.GetAssignmentCast(dtFrom, dtTo) + reverseConversion := types.GetAssignmentCast(dtTo, dtFrom) + + return forwardConversion != nil && reverseConversion != nil +} diff --git a/server/analyzer/init.go b/server/analyzer/init.go index 7086aadf94..cfc9fcdc5b 100644 --- a/server/analyzer/init.go +++ b/server/analyzer/init.go @@ -16,6 +16,7 @@ package analyzer import ( "github.com/dolthub/go-mysql-server/sql/analyzer" + "github.com/dolthub/go-mysql-server/sql/plan" ) // IDs are basically arbitrary, we just need to ensure that they do not conflict with existing IDs @@ -91,6 +92,15 @@ func Init() { analyzer.Rule{Id: ruleId_AddDomainConstraintsToCasts, Apply: AddDomainConstraintsToCasts}, analyzer.Rule{Id: ruleId_ReplaceNode, Apply: ReplaceNode}, analyzer.Rule{Id: ruleId_InsertContextRootFinalizer, Apply: InsertContextRootFinalizer}) + + initEngine() +} + +func initEngine() { + // This technically takes place at execution time rather than as part of analysis, but we don't have a better + // place to put it. Our foreign key validation logic is different from MySQL's, and since it's not an analyzer rule + // we can't swap out a rule like the rest of the logic in this packge, we have to do a function swap. + plan.ValidateForeignKeyDefinition = validateForeignKeyDefinition } // insertAnalyzerRules inserts the given rule(s) before or after the given analyzer.RuleId, returning an updated slice. diff --git a/server/ast/alter_table.go b/server/ast/alter_table.go index e24a4bb905..afca2212da 100644 --- a/server/ast/alter_table.go +++ b/server/ast/alter_table.go @@ -181,7 +181,10 @@ func nodeAlterTableAddConstraint( IfExists: ifExists, TableSpec: &vitess.TableSpec{ Constraints: []*vitess.ConstraintDefinition{ - {Details: foreignKeyDefinition}, + { + Name: bareIdentifier(constraintDef.Name), + Details: foreignKeyDefinition, + }, }, }, }, nil @@ -192,6 +195,14 @@ func nodeAlterTableAddConstraint( } } +// bareIdentifier returns the string representation of a name without any quoting +// (quoted is the default Name.String() behavior) +func bareIdentifier(id tree.Name) string { + ctx := tree.NewFmtCtx(tree.FmtBareIdentifiers) + id.Format(ctx) + return ctx.CloseAndGetString() +} + // nodeAlterTableAddColumn converts a tree.AlterTableAddColumn instance into an equivalent vitess.DDL instance. func nodeAlterTableAddColumn(ctx *Context, node *tree.AlterTableAddColumn, tableName vitess.TableName, ifExists bool) (*vitess.DDL, error) { if node.IfNotExists { diff --git a/server/ast/constraint_table_def.go b/server/ast/constraint_table_def.go index 41c6c57e8d..cc4f0addf6 100644 --- a/server/ast/constraint_table_def.go +++ b/server/ast/constraint_table_def.go @@ -128,6 +128,7 @@ func nodeUniqueConstraintTableDef( Table: tableName, IfExists: ifExists, IndexSpec: &vitess.IndexSpec{ + ToName: vitess.NewColIdent(bareIdentifier(node.Name)), Action: "create", Type: indexType, Columns: columns, diff --git a/server/cast/init.go b/server/cast/init.go index 1f36aa9a35..9c80e3ad6a 100644 --- a/server/cast/init.go +++ b/server/cast/init.go @@ -14,6 +14,11 @@ package cast +import ( + "github.com/dolthub/doltgresql/server/functions/framework" + "github.com/dolthub/doltgresql/server/types" +) + // Init initializes all casts in this package. func Init() { initBool() @@ -40,4 +45,10 @@ func Init() { initTimestampTZ() initTimeTZ() initVarChar() + + // This is a hack to get around import cycles. The types package needs these references for type conversions in + // some contexts + types.GetImplicitCast = framework.GetImplicitCast + types.GetAssignmentCast = framework.GetAssignmentCast + types.GetExplicitCast = framework.GetExplicitCast } diff --git a/server/functions/framework/cast.go b/server/functions/framework/cast.go index 38c8c7d8bd..40a6ca3bae 100644 --- a/server/functions/framework/cast.go +++ b/server/functions/framework/cast.go @@ -26,26 +26,22 @@ import ( // TODO: Right now, all casts are global. We should decide how to handle this in the presence of branches, sessions, etc. -// TypeCastFunction is a function that takes a value of a particular kind of type, and returns it as another kind of type. -// The targetType given should match the "To" type used to obtain the cast. -type TypeCastFunction func(ctx *sql.Context, val any, targetType *pgtypes.DoltgresType) (any, error) - // getCastFunction is used to recursively call the cast function for when the inner logic sees that it has two array // types. This sidesteps providing -type getCastFunction func(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType) TypeCastFunction +type getCastFunction func(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType) pgtypes.TypeCastFunction // TypeCast is used to cast from one type to another. type TypeCast struct { FromType *pgtypes.DoltgresType ToType *pgtypes.DoltgresType - Function TypeCastFunction + Function pgtypes.TypeCastFunction } // explicitTypeCastMutex is used to lock the explicit type cast map and array when writing. var explicitTypeCastMutex = &sync.RWMutex{} // explicitTypeCastsMap is a map that maps: from -> to -> function. -var explicitTypeCastsMap = map[id.Type]map[id.Type]TypeCastFunction{} +var explicitTypeCastsMap = map[id.Type]map[id.Type]pgtypes.TypeCastFunction{} // explicitTypeCastsArray is a slice that holds all registered explicit casts from the given type. var explicitTypeCastsArray = map[id.Type][]*pgtypes.DoltgresType{} @@ -54,7 +50,7 @@ var explicitTypeCastsArray = map[id.Type][]*pgtypes.DoltgresType{} var assignmentTypeCastMutex = &sync.RWMutex{} // assignmentTypeCastsMap is a map that maps: from -> to -> function. -var assignmentTypeCastsMap = map[id.Type]map[id.Type]TypeCastFunction{} +var assignmentTypeCastsMap = map[id.Type]map[id.Type]pgtypes.TypeCastFunction{} // assignmentTypeCastsArray is a slice that holds all registered assignment casts from the given type. var assignmentTypeCastsArray = map[id.Type][]*pgtypes.DoltgresType{} @@ -63,7 +59,7 @@ var assignmentTypeCastsArray = map[id.Type][]*pgtypes.DoltgresType{} var implicitTypeCastMutex = &sync.RWMutex{} // implicitTypeCastsMap is a map that maps: from -> to -> function. -var implicitTypeCastsMap = map[id.Type]map[id.Type]TypeCastFunction{} +var implicitTypeCastsMap = map[id.Type]map[id.Type]pgtypes.TypeCastFunction{} // implicitTypeCastsArray is a slice that holds all registered implicit casts from the given type. var implicitTypeCastsArray = map[id.Type][]*pgtypes.DoltgresType{} @@ -126,7 +122,7 @@ func GetPotentialImplicitCasts(fromType id.Type) []*pgtypes.DoltgresType { // GetExplicitCast returns the explicit type cast function that will cast the "from" type to the "to" type. Returns nil // if such a cast is not valid. -func GetExplicitCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType) TypeCastFunction { +func GetExplicitCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType) pgtypes.TypeCastFunction { if tcf := getCast(explicitTypeCastMutex, explicitTypeCastsMap, fromType, toType, GetExplicitCast); tcf != nil { return tcf } else if tcf = getCast(assignmentTypeCastMutex, assignmentTypeCastsMap, fromType, toType, GetExplicitCast); tcf != nil { @@ -170,7 +166,7 @@ func GetExplicitCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresTyp // GetAssignmentCast returns the assignment type cast function that will cast the "from" type to the "to" type. Returns // nil if such a cast is not valid. -func GetAssignmentCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType) TypeCastFunction { +func GetAssignmentCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType) pgtypes.TypeCastFunction { if tcf := getCast(assignmentTypeCastMutex, assignmentTypeCastsMap, fromType, toType, GetAssignmentCast); tcf != nil { return tcf } else if tcf = getCast(implicitTypeCastMutex, implicitTypeCastsMap, fromType, toType, GetAssignmentCast); tcf != nil { @@ -199,7 +195,7 @@ func GetAssignmentCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresT // GetImplicitCast returns the implicit type cast function that will cast the "from" type to the "to" type. Returns nil // if such a cast is not valid. -func GetImplicitCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType) TypeCastFunction { +func GetImplicitCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType) pgtypes.TypeCastFunction { if tcf := getCast(implicitTypeCastMutex, implicitTypeCastsMap, fromType, toType, GetImplicitCast); tcf != nil { return tcf } @@ -213,14 +209,14 @@ func GetImplicitCast(fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresTyp // addTypeCast registers the given type cast. func addTypeCast(mutex *sync.RWMutex, - castMap map[id.Type]map[id.Type]TypeCastFunction, + castMap map[id.Type]map[id.Type]pgtypes.TypeCastFunction, castArray map[id.Type][]*pgtypes.DoltgresType, cast TypeCast) error { mutex.Lock() defer mutex.Unlock() toMap, ok := castMap[cast.FromType.ID] if !ok { - toMap = map[id.Type]TypeCastFunction{} + toMap = map[id.Type]pgtypes.TypeCastFunction{} castMap[cast.FromType.ID] = toMap castArray[cast.FromType.ID] = nil } @@ -244,8 +240,8 @@ func getPotentialCasts(mutex *sync.RWMutex, castArray map[id.Type][]*pgtypes.Dol // getCast returns the type cast function that will cast the "from" type to the "to" type. Returns nil if such a cast is // not valid. func getCast(mutex *sync.RWMutex, - castMap map[id.Type]map[id.Type]TypeCastFunction, - fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType, outerFunc getCastFunction) TypeCastFunction { + castMap map[id.Type]map[id.Type]pgtypes.TypeCastFunction, + fromType *pgtypes.DoltgresType, toType *pgtypes.DoltgresType, outerFunc getCastFunction) pgtypes.TypeCastFunction { mutex.RLock() defer mutex.RUnlock() diff --git a/server/functions/framework/compiled_function.go b/server/functions/framework/compiled_function.go index 55b17c3b82..53b6e77931 100644 --- a/server/functions/framework/compiled_function.go +++ b/server/functions/framework/compiled_function.go @@ -403,7 +403,7 @@ func (c *CompiledFunction) resolveOperator(argTypes []*pgtypes.DoltgresType, ove rightUnknownType := argTypes[1].ID == pgtypes.Unknown.ID if (leftUnknownType && !rightUnknownType) || (!leftUnknownType && rightUnknownType) { var typ *pgtypes.DoltgresType - casts := []TypeCastFunction{identityCast, identityCast} + casts := []pgtypes.TypeCastFunction{identityCast, identityCast} if leftUnknownType { casts[0] = UnknownLiteralCast typ = argTypes[1] @@ -484,7 +484,7 @@ func (c *CompiledFunction) typeCompatibleOverloads(fnOverloads []Overload, argTy var compatible []overloadMatch for _, overload := range fnOverloads { isConvertible := true - overloadCasts := make([]TypeCastFunction, len(argTypes)) + overloadCasts := make([]pgtypes.TypeCastFunction, len(argTypes)) // Polymorphic parameters must be gathered so that we can later verify that they all have matching base types var polymorphicParameters []*pgtypes.DoltgresType var polymorphicTargets []*pgtypes.DoltgresType diff --git a/server/functions/framework/overloads.go b/server/functions/framework/overloads.go index a5d65f248b..427e9a3c85 100644 --- a/server/functions/framework/overloads.go +++ b/server/functions/framework/overloads.go @@ -165,7 +165,7 @@ func (o *Overload) coalesceVariadicValues(returnValues []any) []any { // as the type cast functions required to convert every argument to its appropriate parameter type type overloadMatch struct { params Overload - casts []TypeCastFunction + casts []pgtypes.TypeCastFunction } // Valid returns whether this overload is valid (has a callable function) diff --git a/server/types/type.go b/server/types/type.go index 5cfbb898df..2c4a3a7bcd 100644 --- a/server/types/type.go +++ b/server/types/type.go @@ -364,6 +364,33 @@ func (t *DoltgresType) Convert(v interface{}) (interface{}, sql.ConvertInRange, return nil, sql.OutOfRange, ErrUnhandledType.New(t.String(), v) } +// GetImplicitCast is a reference to the implicit cast logic in the functions/framework package, which we can't use +// here due to import cycles +var GetImplicitCast func(fromType *DoltgresType, toType *DoltgresType) TypeCastFunction + +// GetAssignmentCast is a reference to the assignment cast logic in the functions/framework package, which we can't use +// here due to import cycles +var GetAssignmentCast func(fromType *DoltgresType, toType *DoltgresType) TypeCastFunction + +// GetExplicitCast is a reference to the explicit cast logic in the functions/framework package, which we can't use +// here due to import cycles +var GetExplicitCast func(fromType *DoltgresType, toType *DoltgresType) TypeCastFunction + +// ConvertToType implements the types.ExtendedType interface. +func (t *DoltgresType) ConvertToType(ctx *sql.Context, typ types.ExtendedType, val any) (any, error) { + dt, ok := typ.(*DoltgresType) + if !ok { + return nil, errors.Errorf("expected DoltgresType, got %T", typ) + } + + castFn := GetAssignmentCast(dt, t) + if castFn == nil { + return nil, errors.Errorf("no assignment cast from %s to %s", dt.Name(), t.Name()) + } + + return castFn(ctx, val, t) +} + // DomainUnderlyingBaseType returns an underlying base type of this domain type. // It can be a nested domain type, so it recursively searches for a valid base type. func (t *DoltgresType) DomainUnderlyingBaseType() *DoltgresType { @@ -851,3 +878,7 @@ func (t *DoltgresType) DeserializeValue(ctx context.Context, val []byte) (any, e return globalFunctionRegistry.GetFunction(t.ReceiveFunc).CallVariadic(nil, val) } } + +// TypeCastFunction is a function that takes a value of a particular kind of type, and returns it as another kind of type. +// The targetType given should match the "To" type used to obtain the cast. +type TypeCastFunction func(ctx *sql.Context, val any, targetType *DoltgresType) (any, error) diff --git a/testing/go/foreign_keys_test.go b/testing/go/foreign_keys_test.go index a1b8694070..cadf01794c 100755 --- a/testing/go/foreign_keys_test.go +++ b/testing/go/foreign_keys_test.go @@ -44,6 +44,381 @@ func TestForeignKeys(t *testing.T) { }, }, }, + { + Name: "named constraint", + SetUpScript: []string{ + `CREATE TABLE parent (a INT PRIMARY KEY, b int)`, + `CREATE TABLE child (a INT PRIMARY KEY, b INT)`, + `INSERT INTO parent VALUES (1, 1)`, + `ALTER TABLE child ADD CONSTRAINT fk123 FOREIGN KEY (b) REFERENCES parent(a)`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "INSERT INTO child VALUES (1, 1)", + }, + { + Query: "INSERT INTO child VALUES (2, 1)", + }, + { + Query: "INSERT INTO child VALUES (2, 2)", + ExpectedErr: "fk123", + }, + }, + }, + { + Name: "unnamed constraint", + SetUpScript: []string{ + `CREATE TABLE parent (a INT PRIMARY KEY, b int)`, + `CREATE TABLE child (a INT PRIMARY KEY, b INT)`, + `INSERT INTO parent VALUES (1, 1)`, + `ALTER TABLE child ADD FOREIGN KEY (b) REFERENCES parent(a)`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "INSERT INTO child VALUES (1, 1)", + }, + { + Query: "INSERT INTO child VALUES (2, 1)", + }, + { + Query: "INSERT INTO child VALUES (2, 2)", + ExpectedErr: "child_b_fkey", + }, + }, + }, + { + Name: "text foreign key", + SetUpScript: []string{ + `CREATE TABLE parent (a text PRIMARY KEY, b int)`, + `CREATE TABLE child (a INT PRIMARY KEY, b text, FOREIGN KEY (b) REFERENCES parent(a))`, + `INSERT INTO parent VALUES ('a', 1)`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "INSERT INTO child VALUES (1, 'a')", + }, + { + Query: "INSERT INTO child VALUES (2, 'a')", + }, + { + Query: "INSERT INTO child VALUES (3, 'b')", + ExpectedErr: "Foreign key violation", + }, + }, + }, + { + Name: "type compatibility", + SetUpScript: []string{ + `create table parent (i2 int2, i4 int4, i8 int8, f float, d double precision, v varchar, vl varchar(100), t text, j json, ts timestamp);`, + "alter table parent add constraint u1 unique (i2);", + "alter table parent add constraint u2 unique (i4);", + "alter table parent add constraint u3 unique (i8);", + "alter table parent add constraint u4 unique (d);", + "alter table parent add constraint u5 unique (f);", + "alter table parent add constraint u6 unique (v);", + "alter table parent add constraint u7 unique (vl);", + "alter table parent add constraint u8 unique (t);", + "alter table parent add constraint u9 unique (ts);", + `create table child (i2 int2, i4 int4, i8 int8, f float, d double precision, v varchar, vl varchar(100), t text, j json, ts timestamp);`, + "insert into parent values (1, 1, 1, 1.0, 1.0, 'a', 'a', 'a', '{\"a\": 1}', '2021-01-01 00:00:00');", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "alter table child add constraint fi2i2 foreign key (i2) references parent(i2)", + }, + { + Query: "alter table child add constraint fi2i4 foreign key (i2) references parent(i4)", + }, + { + Query: "alter table child add constraint fi2i8 foreign key (i2) references parent(i8);", + }, + { + Query: "alter table child add constraint fi2f foreign key (i2) references parent(f);", + }, + { + Query: "alter table child add constraint fi2d foreign key (i2) references parent(d);", + }, + { + Query: "alter table child add constraint fi2v foreign key (i2) references parent(v);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint fi2vl foreign key (i2) references parent(vl);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint fi2t foreign key (i2) references parent(t);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint fi2ts foreign key (i2) references parent(ts);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint fi4i2 foreign key (i4) references parent(i2);", + }, + { + Query: "alter table child add constraint fi4i4 foreign key (i4) references parent(i4);", + }, + { + Query: "alter table child add constraint fi4i8 foreign key (i4) references parent(i8);", + }, + { + Query: "alter table child add constraint fi4f foreign key (i4) references parent(f);", + }, + { + Query: "alter table child add constraint fi8i2 foreign key (i8) references parent(i2);", + }, + { + Query: "alter table child add constraint fi8i4 foreign key (i8) references parent(i4);", + }, + { + Query: "alter table child add constraint fi8d foreign key (i8) references parent(d);", + }, + { + Query: "alter table child add constraint fi8t foreign key (i8) references parent(t);", + ExpectedErr: "incompatible types", + }, + { + Skip: true, // this isn't allowed in postgres, but works with our constraints currently + Query: "alter table child add constraint ffi2 foreign key (f) references parent(i2);", + ExpectedErr: "incompatible types", + }, + { + Skip: true, // this isn't allowed in postgres, but works with our constraints currently + Query: "alter table child add constraint ffi4 foreign key (f) references parent(i4);", + ExpectedErr: "incompatible types", + }, + { + Skip: true, // this isn't allowed in postgres, but works with our constraints currently + Query: "alter table child add constraint ffi8 foreign key (f) references parent(i8);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint ffd foreign key (f) references parent(d);", + }, + { + Query: "alter table child add constraint fdf foreign key (d) references parent(f);", + }, + { + Query: "alter table child add constraint fft foreign key (f) references parent(t);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint ffv foreign key (f) references parent(v);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint fvv foreign key (v) references parent(v);", + }, + { + Query: "alter table child add constraint fvvl foreign key (v) references parent(vl);", + }, + { + Query: "alter table child add constraint fvi8 foreign key (v) references parent(i8);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint fvf foreign key (v) references parent(f);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint fvts foreign key (v) references parent(ts);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint fvj foreign key (v) references parent(j);", + ExpectedErr: "incompatible types", + }, + { + Skip: true, // varchar -> text should work, but key detection is broken. Should work when toast types are done + Query: "alter table child add constraint fvt foreign key (f) references parent(t);", + }, + { + Query: "alter table child add constraint fvllv foreign key (vl) references parent(vl);", + }, + { + Query: "alter table child add constraint fvlv foreign key (vl) references parent(v);", + }, + { + Skip: true, // varchar -> text should work, but key detection is broken. Should work when toast types are done + Query: "alter table child add constraint fvlt foreign key (vl) references parent(t);", + }, + { + Skip: true, // varchar -> text should work, but key detection is broken. Should work when toast types are done + Query: "alter table child add constraint ftt foreign key (t) references parent(t);", + }, + { + Query: "alter table child add constraint ftv foreign key (t) references parent(v);", + }, + { + Query: "alter table child add constraint ftvl foreign key (t) references parent(vl);", + }, + { + Query: "alter table child add constraint fti8 foreign key (t) references parent(i8);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint ftsts foreign key (ts) references parent(ts);", + }, + { + Query: "alter table child add constraint ftst foreign key (ts) references parent(t);", + ExpectedErr: "incompatible types", + }, + { + Query: "alter table child add constraint ftsi8 foreign key (ts) references parent(i8);", + ExpectedErr: "incompatible types", + }, + { + Query: "insert into child values (1, 1, 1, 1.0, 1.0, 'a', 'a', 'a', '{\"a\": 1}', '2021-01-01 00:00:00');", + }, + { + Query: "insert into child values (1, 2, 1, 1.0, 1.0, 'a', 'a', 'a', '{\"a\": 1}', '2021-01-01 00:00:00');", + ExpectedErr: "Foreign key", + }, + { + Query: "insert into child values (1, 1, 1, 2.0, 1.0, 'a', 'a', 'a', '{\"a\": 1}', '2021-01-01 00:00:00');", + ExpectedErr: "Foreign key", + }, + { + Query: "insert into child values (1, 1, 1, 1.0, 1.0, 'a', 'a', 'b', '{\"a\": 1}', '2021-01-01 00:00:00');", + ExpectedErr: "Foreign key", + }, + { + Query: "insert into child values (1, 1, 1, 1.0, 1.0, 'a', 'a', 'a', '{\"a\": 1}', '2021-01-01 00:00:01');", + ExpectedErr: "Foreign key", + }, + }, + }, + { + Name: "type conversion: text to varchar", + SetUpScript: []string{ + `CREATE TABLE parent (a INT PRIMARY KEY, b varchar(100))`, + `CREATE TABLE child (c INT PRIMARY KEY, d text)`, + `INSERT INTO parent VALUES (1, 'abc'), (2, 'def')`, + `alter table parent add constraint ub unique (b)`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "alter table child add constraint fk foreign key (d) references parent(b)", + }, + { + Query: "insert into child values (1, 'abc')", + }, + { + Query: "insert into child values (2, 'xyz')", + ExpectedErr: "Foreign key", + }, + { + Query: "delete from parent where b = 'def'", + }, + { + Query: "delete from parent where b = 'abc'", + ExpectedErr: "Foreign key", + }, + }, + }, + { + Name: "type conversion: integer to double", + SetUpScript: []string{ + `CREATE TABLE parent (a INT PRIMARY KEY, b double precision)`, + `CREATE TABLE child (c INT PRIMARY KEY, d int)`, + `INSERT INTO parent VALUES (1, 1), (3, 3)`, + `alter table parent add constraint ub unique (b)`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "alter table child add constraint fk foreign key (d) references parent(b)", + }, + { + Query: "select * from parent where b = 1.0", + Expected: []sql.Row{ + {1, 1.0}, + }, + }, + { + Query: "insert into child values (1, 1)", + }, + { + Query: "insert into child values (2, 1)", + }, + { + Query: "insert into child values (2, 2)", + ExpectedErr: "Foreign key", + }, + { + Query: "delete from parent where b = 3.0", + }, + { + Query: "delete from parent where b = 1.0", + ExpectedErr: "Foreign key", + }, + }, + }, + { + Name: "type conversion: value out of bounds, child larger", + SetUpScript: []string{ + `CREATE TABLE parent (a INT PRIMARY KEY, b int2)`, + `CREATE TABLE child (c INT PRIMARY KEY, d int8)`, + `INSERT INTO parent VALUES (1, 1), (3, 3)`, + `alter table parent add constraint ub unique (b)`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "alter table child add constraint fk foreign key (d) references parent(b)", + }, + { + Query: "insert into child values (1, 1)", + }, + { + Query: "insert into child values (2, 2)", + ExpectedErr: "Foreign key", + }, + { + Query: "insert into child values (2, 65536)", // above maximum int2 + ExpectedErr: "Foreign key", + }, + { + Query: "delete from parent where b = 3", + }, + { + Query: "delete from parent where b = 1", + ExpectedErr: "Foreign key", + }, + }, + }, + { + Name: "type conversion: value out of bound, parent larger", + SetUpScript: []string{ + `CREATE TABLE parent (a INT PRIMARY KEY, b int8)`, + `CREATE TABLE child (c INT PRIMARY KEY, d int2)`, + `INSERT INTO parent VALUES (1, 1), (65536, 65536)`, // above maximum int2 + `alter table parent add constraint ub unique (b)`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: "alter table child add constraint fk foreign key (d) references parent(b)", + }, + { + Query: "insert into child values (1, 1)", + }, + { + Query: "insert into child values (2, 2)", + ExpectedErr: "Foreign key", + }, + { + Query: "insert into child values (2, 65536)", + ExpectedErr: "out of range", + }, + { + Query: "delete from parent where b = 65536", + }, + { + Query: "delete from parent where b = 1", + ExpectedErr: "Foreign key", + }, + }, + }, { Name: "foreign key with dolt_add, dolt_commit", SetUpScript: []string{ @@ -402,7 +777,7 @@ func TestForeignKeys(t *testing.T) { "INSERT INTO parent.parent VALUES (0, 0), (1, 1), (2,2)", "SELECT DOLT_COMMIT('-Am', 'new tables')", "INSERT INTO child.child VALUES (2, 'two', 2)", - "ALTER TABLE child.child ADD FOREIGN KEY (test_pk) REFERENCES parent(pk)", + "ALTER TABLE child.child ADD CONSTRAINT fk1 FOREIGN KEY (test_pk) REFERENCES parent(pk)", }, Assertions: []ScriptTestAssertion{ { @@ -410,7 +785,7 @@ func TestForeignKeys(t *testing.T) { ExpectedErr: "Foreign key violation", }, { - Query: "alter table child DROP constraint child_test_pk_fkey;", + Query: "alter table child DROP constraint fk1;", SkipResultsCheck: true, }, { diff --git a/testing/go/insert_test.go b/testing/go/insert_test.go index 950b628542..18a933b357 100755 --- a/testing/go/insert_test.go +++ b/testing/go/insert_test.go @@ -168,5 +168,22 @@ func TestInsert(t *testing.T) { }, }, }, + { + Name: "types", + SetUpScript: []string{ + `create table child (i2 int2, i4 int4, i8 int8, f float, d double precision, v varchar, vl varchar(100), t text, j json, ts timestamp);`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `insert into child values (1, 2, 3, 4.5, 6.7, 'hello', 'world', 'text', '{"a": 1}', '2021-01-01 00:00:00');`, + }, + { + Query: `select * from child;`, + Expected: []sql.Row{ + {int16(1), int32(2), int64(3), float32(4.5), float64(6.7), "hello", "world", "text", `{"a": 1}`, "2021-01-01 00:00:00"}, + }, + }, + }, + }, }) }