feat(pgsql): unique constraints on apply, matching unique index names, FK deferral
- migration writer now emits unique constraints (direct apply and live diff previously dropped them); idempotent via the existing DO block - unique constraint adopts a same-named model unique index with ADD CONSTRAINT ... UNIQUE USING INDEX instead of building a second index - dbml reader names unnamed unique indexes ukey_<table>_<cols> to match the unique constraint naming - foreign key deferral: Constraint.Deferrable/InitiallyDeferred are *bool (nil = unspecified, never diffed); writers emit DEFERRABLE, DEFERRABLE INITIALLY DEFERRED and NOT DEFERRABLE; live reader reads condeferrable and condeferred; dbml reader/writer support deferrable and initially deferred - tests and README/naming docs updated
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package models
|
||||
|
||||
// DeferralClause returns the SQL deferral clause for a foreign key constraint
|
||||
// ("DEFERRABLE", "DEFERRABLE INITIALLY DEFERRED" or "NOT DEFERRABLE"), or ""
|
||||
// when the model does not specify one. InitiallyDeferred without Deferrable
|
||||
// implies DEFERRABLE; InitiallyDeferred=false alone specifies nothing.
|
||||
func (d *Constraint) DeferralClause() string {
|
||||
deferrable, initially, set := d.effectiveDeferral()
|
||||
switch {
|
||||
case !set:
|
||||
return ""
|
||||
case !deferrable:
|
||||
return "NOT DEFERRABLE"
|
||||
case initially != nil && *initially:
|
||||
return "DEFERRABLE INITIALLY DEFERRED"
|
||||
default:
|
||||
return "DEFERRABLE"
|
||||
}
|
||||
}
|
||||
|
||||
// effectiveDeferral resolves the optional flags. set is false when the model says nothing
|
||||
// about deferral; initially is nil when the initial mode is unspecified.
|
||||
func (d *Constraint) effectiveDeferral() (deferrable bool, initially *bool, set bool) {
|
||||
switch {
|
||||
case d.Deferrable != nil:
|
||||
return *d.Deferrable, d.InitiallyDeferred, true
|
||||
case d.InitiallyDeferred != nil && *d.InitiallyDeferred:
|
||||
return true, d.InitiallyDeferred, true
|
||||
}
|
||||
return false, nil, false
|
||||
}
|
||||
|
||||
// DeferralMatches reports whether two constraints agree on deferral. An unspecified side
|
||||
// matches anything, and the initial mode is only compared when both sides specify it.
|
||||
func DeferralMatches(a, b *Constraint) bool {
|
||||
da, ia, aSet := a.effectiveDeferral()
|
||||
db, ib, bSet := b.effectiveDeferral()
|
||||
if !aSet || !bSet {
|
||||
return true
|
||||
}
|
||||
if da != db {
|
||||
return false
|
||||
}
|
||||
if !da || ia == nil || ib == nil {
|
||||
return true
|
||||
}
|
||||
return *ia == *ib
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package models
|
||||
|
||||
import "testing"
|
||||
|
||||
func boolPtr(b bool) *bool { return &b }
|
||||
|
||||
func TestConstraint_DeferralClause(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c Constraint
|
||||
want string
|
||||
}{
|
||||
{"unspecified", Constraint{}, ""},
|
||||
{"deferrable", Constraint{Deferrable: boolPtr(true)}, "DEFERRABLE"},
|
||||
{"deferrable initially deferred", Constraint{Deferrable: boolPtr(true), InitiallyDeferred: boolPtr(true)}, "DEFERRABLE INITIALLY DEFERRED"},
|
||||
{"deferrable initially immediate", Constraint{Deferrable: boolPtr(true), InitiallyDeferred: boolPtr(false)}, "DEFERRABLE"},
|
||||
{"initially deferred implies deferrable", Constraint{InitiallyDeferred: boolPtr(true)}, "DEFERRABLE INITIALLY DEFERRED"},
|
||||
{"initially immediate alone is unspecified", Constraint{InitiallyDeferred: boolPtr(false)}, ""},
|
||||
{"explicitly not deferrable", Constraint{Deferrable: boolPtr(false)}, "NOT DEFERRABLE"},
|
||||
{"not deferrable ignores initially deferred", Constraint{Deferrable: boolPtr(false), InitiallyDeferred: boolPtr(true)}, "NOT DEFERRABLE"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.c.DeferralClause(); got != tt.want {
|
||||
t.Errorf("DeferralClause() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeferralMatches(t *testing.T) {
|
||||
live := func(def, init bool) *Constraint {
|
||||
return &Constraint{Deferrable: boolPtr(def), InitiallyDeferred: boolPtr(init)}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
model, other *Constraint
|
||||
want bool
|
||||
}{
|
||||
{"model unspecified vs deferrable live", &Constraint{}, live(true, false), true},
|
||||
{"model unspecified vs not deferrable live", &Constraint{}, live(false, false), true},
|
||||
{"deferrable vs deferrable live", &Constraint{Deferrable: boolPtr(true)}, live(true, true), true},
|
||||
{"deferrable vs not deferrable live", &Constraint{Deferrable: boolPtr(true)}, live(false, false), false},
|
||||
{"not deferrable vs deferrable live", &Constraint{Deferrable: boolPtr(false)}, live(true, false), false},
|
||||
{"initially deferred vs initially immediate live", &Constraint{InitiallyDeferred: boolPtr(true)}, live(true, false), false},
|
||||
{"initially deferred vs initially deferred live", &Constraint{InitiallyDeferred: boolPtr(true)}, live(true, true), true},
|
||||
{"explicit initially immediate vs initially deferred live", &Constraint{Deferrable: boolPtr(true), InitiallyDeferred: boolPtr(false)}, live(true, true), false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := DeferralMatches(tt.model, tt.other); got != tt.want {
|
||||
t.Errorf("DeferralMatches() = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got := DeferralMatches(tt.other, tt.model); got != tt.want {
|
||||
t.Errorf("DeferralMatches() reversed = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -329,8 +329,8 @@ type Constraint struct {
|
||||
ReferencedColumns []string `json:"referenced_columns" yaml:"referenced_columns" xml:"referenced_columns"`
|
||||
OnDelete string `json:"on_delete" yaml:"on_delete" xml:"on_delete"` // CASCADE, SET NULL, RESTRICT, etc.
|
||||
OnUpdate string `json:"on_update" yaml:"on_update" xml:"on_update"`
|
||||
Deferrable bool `json:"deferrable,omitempty" yaml:"deferrable,omitempty" xml:"deferrable,omitempty"`
|
||||
InitiallyDeferred bool `json:"initially_deferred,omitempty" yaml:"initially_deferred,omitempty" xml:"initially_deferred,omitempty"`
|
||||
Deferrable *bool `json:"deferrable,omitempty" yaml:"deferrable,omitempty" xml:"deferrable,omitempty"` // nil: unspecified, left as is
|
||||
InitiallyDeferred *bool `json:"initially_deferred,omitempty" yaml:"initially_deferred,omitempty" xml:"initially_deferred,omitempty"` // nil: unspecified, left as is
|
||||
Sequence uint `json:"sequence,omitempty" yaml:"sequence,omitempty" xml:"sequence,omitempty"`
|
||||
GUID string `json:"guid" yaml:"guid" xml:"guid"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user