-
Notifications
You must be signed in to change notification settings - Fork 4
Exclude the ColdFront schema from ACE checks #157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -157,13 +157,20 @@ func (c *SchemaDiffCmd) parseSkipList() error { | |
| return nil | ||
| } | ||
|
|
||
| // coldFrontSchemaName holds the internal state of the pgEdge ColdFront | ||
| // extension, not user data. ACE never diffs or repairs it. | ||
| const coldFrontSchemaName = "coldfront" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of checking a hard-coded schema name, is there a more general approach we can take and exclude all foreign tables? Or are there gotchas taking that approach? |
||
|
|
||
| func (c *SchemaDiffCmd) Validate() error { | ||
| if c.ClusterName == "" { | ||
| return fmt.Errorf("cluster name is required") | ||
| } | ||
| if c.SchemaName == "" { | ||
| return fmt.Errorf("schema name is required") | ||
| } | ||
| if c.SchemaName == coldFrontSchemaName { | ||
| return fmt.Errorf("schema %q is reserved for the pgEdge ColdFront extension; ACE does not diff it", coldFrontSchemaName) | ||
| } | ||
|
|
||
| nodeList, err := utils.ParseNodes(c.Nodes) | ||
| if err != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,119 @@ | ||||||||||||||||||||||||||||||||||
| // /////////////////////////////////////////////////////////////////////////// | ||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||
| // # ACE - Active Consistency Engine | ||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||
| // Copyright (C) 2023 - 2026, pgEdge (https://www.pgedge.com/) | ||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||
| // This software is released under the PostgreSQL License: | ||||||||||||||||||||||||||||||||||
| // https://opensource.org/license/postgresql | ||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||
| // /////////////////////////////////////////////////////////////////////////// | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| package integration | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||
| "bytes" | ||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||
| "testing" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| "github.com/jackc/pgx/v5/pgxpool" | ||||||||||||||||||||||||||||||||||
| "github.com/pgedge/ace/internal/consistency/diff" | ||||||||||||||||||||||||||||||||||
| "github.com/pgedge/ace/pkg/logger" | ||||||||||||||||||||||||||||||||||
| "github.com/stretchr/testify/assert" | ||||||||||||||||||||||||||||||||||
| "github.com/stretchr/testify/require" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // TestSchemaDiff_ColdFrontSchemaRejected verifies that schema-diff refuses | ||||||||||||||||||||||||||||||||||
| // to diff the ColdFront schema. The rejection happens in Validate(), so no | ||||||||||||||||||||||||||||||||||
| // setup is required. | ||||||||||||||||||||||||||||||||||
| func TestSchemaDiff_ColdFrontSchemaRejected(t *testing.T) { | ||||||||||||||||||||||||||||||||||
| nodes := fmt.Sprintf("%s,%s", serviceN1, serviceN2) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| task := newTestSchemaDiffTask("coldfront", nodes) | ||||||||||||||||||||||||||||||||||
| err := task.SchemaTableDiff() | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| require.Error(t, err, "schema-diff must refuse to diff the coldfront schema") | ||||||||||||||||||||||||||||||||||
| assert.Contains(t, err.Error(), "reserved for the pgEdge ColdFront extension") | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // TestRepsetDiff_ColdFrontSchemaExcluded verifies that repset-diff skips | ||||||||||||||||||||||||||||||||||
| // tables in the ColdFront schema even when they are part of the repset. | ||||||||||||||||||||||||||||||||||
| // The exclusion keys off the schema name only, so the extension itself | ||||||||||||||||||||||||||||||||||
| // does not need to be installed. | ||||||||||||||||||||||||||||||||||
| func TestRepsetDiff_ColdFrontSchemaExcluded(t *testing.T) { | ||||||||||||||||||||||||||||||||||
| ctx := context.Background() | ||||||||||||||||||||||||||||||||||
| const repsetName = "default" | ||||||||||||||||||||||||||||||||||
| const coldfrontTable = "claims_stub" | ||||||||||||||||||||||||||||||||||
| qualifiedColdfront := fmt.Sprintf("coldfront.%s", coldfrontTable) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| pools := []*pgxpool.Pool{pgCluster.Node1Pool, pgCluster.Node2Pool} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Diverge the data between nodes: a broken exclusion would report it | ||||||||||||||||||||||||||||||||||
| // as a difference. | ||||||||||||||||||||||||||||||||||
| createSQL := fmt.Sprintf(` | ||||||||||||||||||||||||||||||||||
| CREATE SCHEMA IF NOT EXISTS coldfront; | ||||||||||||||||||||||||||||||||||
| CREATE TABLE IF NOT EXISTS %s ( | ||||||||||||||||||||||||||||||||||
| id INT PRIMARY KEY, | ||||||||||||||||||||||||||||||||||
| val TEXT | ||||||||||||||||||||||||||||||||||
| )`, qualifiedColdfront) | ||||||||||||||||||||||||||||||||||
| for _, pool := range pools { | ||||||||||||||||||||||||||||||||||
| _, err := pool.Exec(ctx, createSQL) | ||||||||||||||||||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| _, err := pgCluster.Node1Pool.Exec(ctx, fmt.Sprintf( | ||||||||||||||||||||||||||||||||||
| `INSERT INTO %s (id, val) VALUES (1, 'from_n1') ON CONFLICT DO NOTHING`, qualifiedColdfront)) | ||||||||||||||||||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||||||||||||||||||
| _, err = pgCluster.Node2Pool.Exec(ctx, fmt.Sprintf( | ||||||||||||||||||||||||||||||||||
| `INSERT INTO %s (id, val) VALUES (1, 'from_n2') ON CONFLICT DO NOTHING`, qualifiedColdfront)) | ||||||||||||||||||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for _, pool := range pools { | ||||||||||||||||||||||||||||||||||
| _, err := pool.Exec(ctx, | ||||||||||||||||||||||||||||||||||
| fmt.Sprintf(`SELECT spock.repset_add_table('%s', '%s');`, repsetName, qualifiedColdfront)) | ||||||||||||||||||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| t.Cleanup(func() { | ||||||||||||||||||||||||||||||||||
| for _, pool := range pools { | ||||||||||||||||||||||||||||||||||
| pool.Exec(ctx, fmt.Sprintf( | ||||||||||||||||||||||||||||||||||
| `SELECT spock.repset_remove_table('%s', '%s');`, repsetName, qualifiedColdfront)) | ||||||||||||||||||||||||||||||||||
| pool.Exec(ctx, fmt.Sprintf(`DROP TABLE IF EXISTS %s CASCADE`, qualifiedColdfront)) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+77
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Handle cleanup errors. Line 79 discards the Based on learnings, cleanup callbacks should log expected cleanup failures with Proposed fix t.Cleanup(func() {
for _, pool := range pools {
- pool.Exec(ctx, fmt.Sprintf(
- `SELECT spock.repset_remove_table('%s', '%s');`, repsetName, qualifiedColdfront))
- pool.Exec(ctx, fmt.Sprintf(`DROP TABLE IF EXISTS %s CASCADE`, qualifiedColdfront))
+ if _, err := pool.Exec(ctx, fmt.Sprintf(
+ `SELECT spock.repset_remove_table('%s', '%s');`, repsetName, qualifiedColdfront)); err != nil {
+ t.Logf("Warning: could not remove %s from repset %s: %v", qualifiedColdfront, repsetName, err)
+ }
+ if _, err := pool.Exec(ctx, fmt.Sprintf(`DROP TABLE IF EXISTS %s CASCADE`, qualifiedColdfront)); err != nil {
+ t.Logf("Warning: could not drop table %s: %v", qualifiedColdfront, err)
+ }
}
})📝 Committable suggestion
Suggested change
🧰 Tools🪛 ast-grep (0.45.1)[warning] 78-79: Detected a SQL statement built with 'fmt.Sprintf' and passed directly to 'db.Exec'/'db.ExecContext'. Interpolating values into a query string lets an attacker inject arbitrary SQL. Use parameterized queries instead: pass the SQL with placeholders ('?' or '') as the query argument and supply the values as separate arguments, e.g. 'db.Exec("UPDATE t SET x = ? WHERE id = ?", x, id)'. (sql-injection-exec-sprintf-go) [warning] 80-80: Detected a SQL statement built with 'fmt.Sprintf' and passed directly to 'db.Exec'/'db.ExecContext'. Interpolating values into a query string lets an attacker inject arbitrary SQL. Use parameterized queries instead: pass the SQL with placeholders ('?' or '') as the query argument and supply the values as separate arguments, e.g. 'db.Exec("UPDATE t SET x = ? WHERE id = ?", x, id)'. (sql-injection-exec-sprintf-go) 🪛 golangci-lint (2.12.2)[error] 79-79: Error return value of (errcheck) 🤖 Prompt for AI AgentsSources: Learnings, Linters/SAST tools |
||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Control table: ordinary tables must still be diffed. | ||||||||||||||||||||||||||||||||||
| controlQualified := createRepsetDiffTable(t, "cf_excl_control_tbl", repsetName, false) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| r, w, err := os.Pipe() | ||||||||||||||||||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||||||||||||||||||
| logger.SetOutput(w) | ||||||||||||||||||||||||||||||||||
| t.Cleanup(func() { logger.SetOutput(os.Stderr) }) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| task := newTestRepsetDiffTask(repsetName) | ||||||||||||||||||||||||||||||||||
| diffErr := diff.RepsetDiff(task) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| w.Close() | ||||||||||||||||||||||||||||||||||
| var buf bytes.Buffer | ||||||||||||||||||||||||||||||||||
| buf.ReadFrom(r) | ||||||||||||||||||||||||||||||||||
| logOutput := buf.String() | ||||||||||||||||||||||||||||||||||
| t.Logf("Captured log output:\n%s", logOutput) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| require.NoError(t, diffErr) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| summarySection := extractSummarySection(t, logOutput) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| skippedSection := extractBetween(summarySection, "table(s) were skipped:", "table(s)") | ||||||||||||||||||||||||||||||||||
| assert.Contains(t, skippedSection, qualifiedColdfront, | ||||||||||||||||||||||||||||||||||
| "the ColdFront table should be reported as skipped") | ||||||||||||||||||||||||||||||||||
| assert.Contains(t, skippedSection, "pgEdge ColdFront schema", | ||||||||||||||||||||||||||||||||||
| "the skipped entry should state why it was excluded") | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| identicalSection := extractBetween(summarySection, "table(s) are identical:", "table(s)") | ||||||||||||||||||||||||||||||||||
| assert.Contains(t, identicalSection, controlQualified, | ||||||||||||||||||||||||||||||||||
| "the control table should still be diffed") | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| diffSection := extractBetween(summarySection, "table(s) have differences:", "") | ||||||||||||||||||||||||||||||||||
| assert.NotContains(t, diffSection, coldfrontTable, | ||||||||||||||||||||||||||||||||||
| "the ColdFront table must not be reported as differing") | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Exclude ColdFront entries from
missingTables.A ColdFront table that belongs to the repset on only one node remains in
missingTables.RepsetDiffthen reports that internal table as missing, even though it is skipped fromtableList. FiltermissingTableswith the samecoldFrontPrefixbefore assigningc.missingTables. Add an integration case with asymmetric ColdFront repset membership.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents