package tdb import ( "errors" "fmt" "git.keganmyers.com/terribleplan/tdb/stringy" bolt "go.etcd.io/bbolt" ) type foreignConstraint struct { domestic *table foreign *table field dbField index indexish } type foreignSimpleConstraint foreignConstraint type foreignArrayConstraint foreignConstraint func validateForeignRaw(b *bolt.Bucket, foreignKey []byte) ConstraintValidationStatus { if b.Get(foreignKey) == nil { return ConstraintViolation } return ConstraintValidated } func newSimpleForeignConstraint(domestic *table, foreign string, field dbField, index indexish) (constraintish, error) { if domestic == nil { return nil, errors.New("[constraint] [foreign] unable to create: no domestic table") } if !field.IsUint64() { return nil, fmt.Errorf("[constraint] [foreign] unable to create: '%s'.'%s' is not a uint64", domestic.name, field.Name) } if foreign == "" { return nil, errors.New("[constraint] [foreign] unable to create: no foreign table") } foreignTable, ok := domestic.db.tables[foreign] if !ok { return nil, fmt.Errorf("[constraint] [foreign] unable to create: no such table '%s'", foreign) } if index == nil { domestic.debugLogf("[constraint] [foreign] warning: creating constraint on '%s'.'%s' without index. will not check when foreign records are removed (to avoid table scan)", domestic.name, field.Name) } return &foreignSimpleConstraint{ domestic: domestic, foreign: foreignTable, field: field, index: index, }, nil } func (c *foreignSimpleConstraint) validate(tx *Tx, pv dbPtrValue) error { // foreign keys must all be uint64, those are the only supported primary keys foreignId := pv.dangerous_Field(c.field).Uint() // the foreign constraint is not responsible for enforcing nullability if foreignId == 0 { return nil } foreignKey := []byte(stringy.LiteralUintToString(foreignId)) if validateForeignRaw(c.foreign.bucket(tx), foreignKey) == ConstraintViolation { c.domestic.debugLogf("[constraint] [foreign] violation: '%s' with Id '%s' does not exist", c.foreign.name, foreignKey) return fmt.Errorf("[constraint] [foreign] violation: '%s' with Id '%s' does not exist", c.foreign.name, foreignKey) } return nil } func newArrayForeignConstraint(domestic *table, foreign string, field dbField, index indexish) (constraintish, error) { if domestic == nil { return nil, errors.New("[constraint] [foreign] unable to create: no domestic table") } if !field.IsUint64Slice() { return nil, fmt.Errorf("[constraint] [foreign] unable to create: '%s'.'%s' is not a uint64 array", domestic.name, field.Name) } if foreign == "" { return nil, errors.New("[constraint] [foreign] unable to create: no foreign table") } foreignTable, ok := domestic.db.tables[foreign] if !ok { return nil, fmt.Errorf("[constraint] [foreign] unable to create: no such table '%s'", foreign) } if index == nil { domestic.debugLogf("[constraint] [foreign] warning: creating constraint on '%s'.'%s' without index. will not check when foreign records are removed (to avoid table scan)", domestic.name, field.Name) } return &foreignArrayConstraint{ domestic: domestic, foreign: foreignTable, field: field, index: index, }, nil } func (c *foreignArrayConstraint) validate(tx *Tx, pv dbPtrValue) error { // foreign keys must all be uint64, those are the only supported primary keys foreignIds := pv.dangerous_Field(c.field) foreignIdsLen := foreignIds.Len() for i := 0; i < foreignIdsLen; i++ { foreignId := foreignIds.Index(i).Uint() // the foreign constraint is not responsible for enforcing nullability if foreignId == 0 { continue } foreignKey := []byte(stringy.LiteralUintToString(foreignId)) if validateForeignRaw(c.foreign.bucket(tx), foreignKey) == ConstraintViolation { c.domestic.debugLogf("[constraint] [foreign] violation: '%s' with Id '%s' does not exist", c.foreign.name, foreignKey) return fmt.Errorf("[constraint] [foreign] violation: '%s' with Id '%s' does not exist", c.foreign.name, foreignKey) } } return nil }