Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions cmd/ignore_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1564,6 +1564,145 @@ CREATE TABLE products (
})
}

// TestIgnoreTriggers tests that triggers matching .pgschemaignore [triggers]
// patterns are excluded from dump and plan output.
// Addresses https://github.com/pgplex/pgschema/issues/407
func TestIgnoreTriggers(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}

embeddedPG := testutil.SetupPostgres(t)
defer embeddedPG.Stop()
conn, host, port, dbname, user, password := testutil.ConnectToPostgres(t, embeddedPG)
defer conn.Close()

containerInfo := &struct {
Conn *sql.DB
Host string
Port int
DBName string
User string
Password string
}{
Conn: conn,
Host: host,
Port: port,
DBName: dbname,
User: user,
Password: password,
}

// Create a table with a managed trigger plus an extension-style trigger that
// is not part of the declared schema (simulates a trigger an extension adds
// automatically, e.g. the pgai vectorizer's _vectorizer_src_trg_* triggers).
Comment thread
tianzhou marked this conversation as resolved.
Outdated
setupSQL := `
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
updated_at TIMESTAMPTZ
);

CREATE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE FUNCTION vectorizer_noop() RETURNS trigger AS $$
BEGIN
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER products_set_updated_at
BEFORE UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION set_updated_at();

CREATE TRIGGER _vectorizer_src_trg_products
AFTER INSERT OR UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION vectorizer_noop();
Comment thread
tianzhou marked this conversation as resolved.
Outdated
`
_, err := conn.Exec(setupSQL)
if err != nil {
t.Fatalf("Failed to create test schema: %v", err)
}

originalWd, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get current working directory: %v", err)
}
defer func() {
if err := os.Chdir(originalWd); err != nil {
t.Fatalf("Failed to restore working directory: %v", err)
}
}()

tmpDir := t.TempDir()
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("Failed to change to temp directory: %v", err)
}

// Ignore any trigger whose name starts with "_vectorizer_src_trg_"
ignoreContent := `[triggers]
patterns = ["_vectorizer_src_trg_*"]
`
err = os.WriteFile(".pgschemaignore", []byte(ignoreContent), 0644)
if err != nil {
t.Fatalf("Failed to create .pgschemaignore: %v", err)
}

t.Run("dump", func(t *testing.T) {
output := executeIgnoreDumpCommand(t, containerInfo)

if !strings.Contains(output, "products_set_updated_at") {
t.Error("Dump should include products_set_updated_at (not ignored)")
}

if strings.Contains(output, "_vectorizer_src_trg_products") {
t.Error("Dump should not include _vectorizer_src_trg_products (ignored by [triggers] patterns)")
}
})

t.Run("plan", func(t *testing.T) {
// Desired schema declares the table, the function, and the managed trigger
// but not the extension trigger. Without the ignore the plan would emit
// DROP TRIGGER _vectorizer_src_trg_products; with the ignore it should not
// reference it.
schemaSQL := `
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
updated_at TIMESTAMPTZ
);

CREATE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER products_set_updated_at
BEFORE UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
`
schemaFile := "schema.sql"
err := os.WriteFile(schemaFile, []byte(schemaSQL), 0644)
if err != nil {
t.Fatalf("Failed to create schema file: %v", err)
}
defer os.Remove(schemaFile)

output := executeIgnorePlanCommand(t, containerInfo, schemaFile)

if strings.Contains(output, "_vectorizer_src_trg_products") {
t.Errorf("Plan should not reference _vectorizer_src_trg_products (ignored); got: %s", output)
}
})
}

// verifyPlanOutput checks that plan output excludes ignored objects
func verifyPlanOutput(t *testing.T, output string) {
// Changes that should appear in plan (regular objects)
Expand Down
8 changes: 8 additions & 0 deletions cmd/util/ignoreloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type TomlConfig struct {
Sequences SequenceIgnoreConfig `toml:"sequences,omitempty"`
Indexes IndexIgnoreConfig `toml:"indexes,omitempty"`
Constraints ConstraintIgnoreConfig `toml:"constraints,omitempty"`
Triggers TriggerIgnoreConfig `toml:"triggers,omitempty"`
Privileges PrivilegeIgnoreConfig `toml:"privileges,omitempty"`
DefaultPrivileges DefaultPrivilegeIgnoreConfig `toml:"default_privileges,omitempty"`
}
Expand Down Expand Up @@ -81,6 +82,12 @@ type ConstraintIgnoreConfig struct {
Patterns []string `toml:"patterns,omitempty"`
}

// TriggerIgnoreConfig represents trigger-specific ignore configuration
// Patterns match on trigger names
type TriggerIgnoreConfig struct {
Patterns []string `toml:"patterns,omitempty"`
}

// PrivilegeIgnoreConfig represents privilege-specific ignore configuration
// Patterns match on grantee role names
type PrivilegeIgnoreConfig struct {
Expand Down Expand Up @@ -126,6 +133,7 @@ func LoadIgnoreFileWithStructureFromPath(filePath string) (*ir.IgnoreConfig, err
Sequences: tomlConfig.Sequences.Patterns,
Indexes: tomlConfig.Indexes.Patterns,
Constraints: tomlConfig.Constraints.Patterns,
Triggers: tomlConfig.Triggers.Patterns,
Privileges: tomlConfig.Privileges.Patterns,
DefaultPrivileges: tomlConfig.DefaultPrivileges.Patterns,
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/util/ignoreloader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ patterns = ["idx_temp_*"]

[constraints]
patterns = ["fk_temp_*"]

[triggers]
patterns = ["trg_temp_*"]
`

err := os.WriteFile(testFile, []byte(tomlContent), 0644)
Expand Down Expand Up @@ -93,6 +96,10 @@ patterns = ["fk_temp_*"]
if len(config.Constraints) != 1 || config.Constraints[0] != "fk_temp_*" {
t.Errorf("Expected constraints patterns [\"fk_temp_*\"], got %v", config.Constraints)
}

if len(config.Triggers) != 1 || config.Triggers[0] != "trg_temp_*" {
t.Errorf("Expected triggers patterns [\"trg_temp_*\"], got %v", config.Triggers)
}
}

func TestLoadIgnoreFileWithStructure_ValidTOML(t *testing.T) {
Expand Down
18 changes: 18 additions & 0 deletions docs/cli/ignore.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ patterns = ["idx_temp_*", "manual_*"]
[constraints]
patterns = ["fk_legacy_*"]

[triggers]
patterns = ["_vectorizer_src_trg_*"]

[privileges]
patterns = ["deploy_bot", "admin_*"]

Expand Down Expand Up @@ -128,6 +131,21 @@ This is useful when:
Patterns match the constraint name only, which is not necessarily unique across tables. Be careful with broad patterns like `*`, as ignoring a primary key or unique constraint can leave a table without the keys it needs.
</Warning>

## Triggers

The `[triggers]` section matches triggers by **trigger name**. When a trigger is ignored, pgschema neither creates, drops, nor reports drift on it — it is left entirely to be managed out-of-band.

```toml
[triggers]
patterns = ["_vectorizer_src_trg_*"]
```

This is useful when an extension automatically creates triggers on tables you manage. For example, the [pgai](https://github.com/timescale/pgai) vectorizer adds `_vectorizer_src_trg_*` triggers to source tables; ignoring them keeps `pgschema plan` from flagging them for drop while you continue to manage the rest of the table.

<Warning>
Patterns match the trigger name only, which is not necessarily unique across tables. Be careful with broad patterns like `*`.
</Warning>

## Triggers on Ignored Tables

Triggers can be defined on ignored tables. The table structure is not managed, but the trigger itself is.
Expand Down
11 changes: 11 additions & 0 deletions ir/ignore.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type IgnoreConfig struct {
Sequences []string `toml:"sequences,omitempty"`
Indexes []string `toml:"indexes,omitempty"`
Constraints []string `toml:"constraints,omitempty"`
Triggers []string `toml:"triggers,omitempty"`
Privileges []string `toml:"privileges,omitempty"`
DefaultPrivileges []string `toml:"default_privileges,omitempty"`
}
Expand Down Expand Up @@ -94,6 +95,16 @@ func (c *IgnoreConfig) ShouldIgnoreConstraint(constraintName string) bool {
return c.shouldIgnore(constraintName, c.Constraints)
}

// ShouldIgnoreTrigger checks if a trigger should be ignored based on the patterns.
// Patterns match on the trigger name, letting users exclude triggers created
// out-of-band (e.g. triggers an extension automatically adds to tracked tables).
func (c *IgnoreConfig) ShouldIgnoreTrigger(triggerName string) bool {
Comment thread
Copilot marked this conversation as resolved.
if c == nil {
return false
}
return c.shouldIgnore(triggerName, c.Triggers)
}

// ShouldIgnorePrivilegeByObjectType checks if a privilege should be ignored based on the object name
// and its type. When an object (function, table, etc.) is ignored via its section pattern,
// privileges on that object should also be ignored.
Expand Down
6 changes: 6 additions & 0 deletions ir/ignore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func TestIgnoreConfig_AllObjectTypes(t *testing.T) {
Sequences: []string{"seq_*"},
Indexes: []string{"idx_*"},
Constraints: []string{"fk_*"},
Triggers: []string{"trg_*"},
}

// Test each object type
Expand All @@ -133,6 +134,8 @@ func TestIgnoreConfig_AllObjectTypes(t *testing.T) {
{config.ShouldIgnoreIndex, "users_pkey", false},
{config.ShouldIgnoreConstraint, "fk_orders_product", true},
{config.ShouldIgnoreConstraint, "users_pkey", false},
{config.ShouldIgnoreTrigger, "trg_audit", true},
{config.ShouldIgnoreTrigger, "set_updated_at", false},
}

for _, tt := range tests {
Expand Down Expand Up @@ -171,6 +174,9 @@ func TestIgnoreConfig_NilConfig(t *testing.T) {
if config.ShouldIgnoreConstraint("any_constraint") {
t.Error("nil config should not ignore any constraint")
}
if config.ShouldIgnoreTrigger("any_trigger") {
t.Error("nil config should not ignore any trigger")
}
}

func TestMatchPattern(t *testing.T) {
Expand Down
7 changes: 7 additions & 0 deletions ir/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,13 @@ func (i *Inspector) buildTriggers(ctx context.Context, schema *IR, targetSchema
schemaName := triggerRow.TriggerSchema
triggerName := triggerRow.TriggerName

// Check if the trigger should be ignored (e.g. triggers an extension
// automatically creates on tracked tables). Ignored triggers are excluded
// from dump, plan generation, and drift detection alike.
Comment thread
Copilot marked this conversation as resolved.
Outdated
if i.ignoreConfig != nil && i.ignoreConfig.ShouldIgnoreTrigger(triggerName) {
Comment thread
tianzhou marked this conversation as resolved.
continue
}

// Find where to store this trigger: table, view, or ignored external table
targetDBSchema := schema.getOrCreateSchema(schemaName)
var triggerMap map[string]*Trigger
Expand Down