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:
@@ -3,6 +3,7 @@ package dbml
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
||||
@@ -842,9 +843,9 @@ func TestConstraintNaming(t *testing.T) {
|
||||
t.Logf("Available constraints: %v", getKeys(postsTable.Constraints))
|
||||
}
|
||||
|
||||
// Test unique index naming: uidx_table_columns
|
||||
if _, exists := postsTable.Indexes["uidx_posts_slug"]; !exists {
|
||||
t.Error("Expected unique index 'uidx_posts_slug' not found")
|
||||
// Test unique index naming: ukey_table_columns, matching the unique constraint name
|
||||
if _, exists := postsTable.Indexes["ukey_posts_slug"]; !exists {
|
||||
t.Error("Expected unique index 'ukey_posts_slug' not found")
|
||||
t.Logf("Available indexes: %v", getKeys(postsTable.Indexes))
|
||||
}
|
||||
|
||||
@@ -1121,3 +1122,69 @@ func TestReader_DuplicateIndexNameInTableKept(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReader_ForeignKeyDeferral(t *testing.T) {
|
||||
dbml := `Table users {
|
||||
id integer [pk]
|
||||
}
|
||||
|
||||
Table posts {
|
||||
id integer [pk]
|
||||
user_id integer [ref: > users.id, deferrable, initially deferred]
|
||||
editor_id integer
|
||||
plain_id integer [ref: > users.id]
|
||||
}
|
||||
|
||||
Table comments {
|
||||
id integer [pk]
|
||||
post_id integer
|
||||
author_id integer
|
||||
}
|
||||
|
||||
Ref: comments.post_id > posts.id [delete: cascade, deferrable]
|
||||
Ref: comments.author_id > users.id [deferrable, initially deferred]
|
||||
`
|
||||
path := filepath.Join(t.TempDir(), "deferral.dbml")
|
||||
if err := os.WriteFile(path, []byte(dbml), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := NewReader(&readers.ReaderOptions{FilePath: path}).ReadDatabase()
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDatabase() error = %v", err)
|
||||
}
|
||||
|
||||
find := func(table, column string) *models.Constraint {
|
||||
for _, s := range db.Schemas {
|
||||
for _, tb := range s.Tables {
|
||||
if tb.Name != table {
|
||||
continue
|
||||
}
|
||||
for _, c := range tb.Constraints {
|
||||
if c.Type == models.ForeignKeyConstraint && len(c.Columns) == 1 && c.Columns[0] == column {
|
||||
return c
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatalf("FK %s.%s not found", table, column)
|
||||
return nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
table, column string
|
||||
want string
|
||||
}{
|
||||
{"posts", "user_id", "DEFERRABLE INITIALLY DEFERRED"},
|
||||
{"posts", "plain_id", ""},
|
||||
{"comments", "post_id", "DEFERRABLE"},
|
||||
{"comments", "author_id", "DEFERRABLE INITIALLY DEFERRED"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := find(tt.table, tt.column).DeferralClause(); got != tt.want {
|
||||
t.Errorf("%s.%s deferral = %q, want %q", tt.table, tt.column, got, tt.want)
|
||||
}
|
||||
}
|
||||
if got := find("comments", "post_id").OnDelete; !strings.EqualFold(got, "cascade") {
|
||||
t.Errorf("delete action lost, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user