tdb/constraint_foreign.go

123 lines
4.0 KiB
Go

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
}