Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
111 changes: 111 additions & 0 deletions cmd/ignore_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,117 @@ GRANT SELECT ON users TO app_user;
}
}

// TestIgnoreIndexes tests that indexes matching .pgschemaignore [indexes] patterns
// are excluded from dump and plan output.
// Reproduces https://github.com/pgplex/pgschema/issues/406
func TestIgnoreIndexes(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 index plus a manually-added index that
// is not part of the declared schema (simulates a perf hotfix index
// added directly to a production database).
setupSQL := `
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT
);

CREATE INDEX products_name_idx ON products(name);
CREATE INDEX manual_perf_idx ON products(category);
`
_, 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 index whose name starts with "manual_"
ignoreContent := `[indexes]
patterns = ["manual_*"]
`
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_name_idx") {
t.Error("Dump should include products_name_idx (not ignored)")
}

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

t.Run("plan", func(t *testing.T) {
// Desired schema declares products_name_idx but not manual_perf_idx.
// Without the ignore the plan would emit DROP INDEX manual_perf_idx;
// with the ignore the plan should not reference manual_perf_idx at all.
schemaSQL := `
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
category TEXT
);

CREATE INDEX products_name_idx ON products(name);
`
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, "manual_perf_idx") {
t.Errorf("Plan should not reference manual_perf_idx (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
7 changes: 7 additions & 0 deletions cmd/util/ignoreloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type TomlConfig struct {
Procedures ProcedureIgnoreConfig `toml:"procedures,omitempty"`
Types TypeIgnoreConfig `toml:"types,omitempty"`
Sequences SequenceIgnoreConfig `toml:"sequences,omitempty"`
Indexes IndexIgnoreConfig `toml:"indexes,omitempty"`
Privileges PrivilegeIgnoreConfig `toml:"privileges,omitempty"`
DefaultPrivileges DefaultPrivilegeIgnoreConfig `toml:"default_privileges,omitempty"`
}
Expand Down Expand Up @@ -68,6 +69,11 @@ type SequenceIgnoreConfig struct {
Patterns []string `toml:"patterns,omitempty"`
}

// IndexIgnoreConfig represents index-specific ignore configuration
type IndexIgnoreConfig 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 @@ -111,6 +117,7 @@ func LoadIgnoreFileWithStructureFromPath(filePath string) (*ir.IgnoreConfig, err
Procedures: tomlConfig.Procedures.Patterns,
Types: tomlConfig.Types.Patterns,
Sequences: tomlConfig.Sequences.Patterns,
Indexes: tomlConfig.Indexes.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 @@ -41,6 +41,9 @@ patterns = ["type_test_*"]

[sequences]
patterns = ["seq_temp_*"]

[indexes]
patterns = ["idx_temp_*"]
`

err := os.WriteFile(testFile, []byte(tomlContent), 0644)
Expand Down Expand Up @@ -79,6 +82,10 @@ patterns = ["seq_temp_*"]
if len(config.Procedures) != 1 || config.Procedures[0] != "sp_temp_*" {
t.Errorf("Expected procedure patterns [\"sp_temp_*\"], got %v", config.Procedures)
}

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

func TestLoadIgnoreFileWithStructure_ValidTOML(t *testing.T) {
Expand Down
9 changes: 9 additions & 0 deletions ir/ignore.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type IgnoreConfig struct {
Procedures []string `toml:"procedures,omitempty"`
Types []string `toml:"types,omitempty"`
Sequences []string `toml:"sequences,omitempty"`
Indexes []string `toml:"indexes,omitempty"`
Privileges []string `toml:"privileges,omitempty"`
DefaultPrivileges []string `toml:"default_privileges,omitempty"`
}
Expand Down Expand Up @@ -74,6 +75,14 @@ func (c *IgnoreConfig) ShouldIgnoreSequence(sequenceName string) bool {
return c.shouldIgnore(sequenceName, c.Sequences)
}

// ShouldIgnoreIndex checks if an index should be ignored based on the patterns
func (c *IgnoreConfig) ShouldIgnoreIndex(indexName string) bool {
if c == nil {
return false
}
return c.shouldIgnore(indexName, c.Indexes)
}

// 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 @@ -107,6 +107,7 @@ func TestIgnoreConfig_AllObjectTypes(t *testing.T) {
Procedures: []string{"sp_*"},
Types: []string{"type_*"},
Sequences: []string{"seq_*"},
Indexes: []string{"idx_*"},
}

// Test each object type
Expand All @@ -127,6 +128,8 @@ func TestIgnoreConfig_AllObjectTypes(t *testing.T) {
{config.ShouldIgnoreType, "user_status", false},
{config.ShouldIgnoreSequence, "seq_temp", true},
{config.ShouldIgnoreSequence, "user_id_seq", false},
{config.ShouldIgnoreIndex, "idx_temp", true},
{config.ShouldIgnoreIndex, "users_pkey", false},
}

for _, tt := range tests {
Expand Down Expand Up @@ -159,6 +162,9 @@ func TestIgnoreConfig_NilConfig(t *testing.T) {
if config.ShouldIgnoreSequence("any_sequence") {
t.Error("nil config should not ignore any sequence")
}
if config.ShouldIgnoreIndex("any_index") {
t.Error("nil config should not ignore any index")
}
}

func TestMatchPattern(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions ir/inspector.go
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,11 @@ func (i *Inspector) buildIndexes(ctx context.Context, schema *IR, targetSchema s
tableName := indexRow.Tablename
indexName := indexRow.Indexname

// Check if index should be ignored
if i.ignoreConfig != nil && i.ignoreConfig.ShouldIgnoreIndex(indexName) {
continue
}

dbSchema := schema.getOrCreateSchema(schemaName)

// Extract values with null safety
Expand Down