Skip to content
Merged
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
24 changes: 24 additions & 0 deletions postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,33 @@ package main

import (
"database/sql"
"errors"
"fmt"
"io"
"os"
"path/filepath"

"github.com/jackc/pgx/v5/pgconn"
)

// isPGUnavailableError reports whether err indicates that the queried object
// is not installed/available (missing extension, table, function, or schema).
// These are treated as skips rather than failures.
func isPGUnavailableError(err error) bool {
var pgErr *pgconn.PgError
if !errors.As(err, &pgErr) {
return false
}
switch pgErr.Code {
case "42P01", // undefined_table
"42704", // undefined_object
"42883", // undefined_function
"3F000": // invalid_schema_name
return true
}
return false
}

// postgresConfigFileTasks defines tasks for collecting PostgreSQL configuration files (sorted alphabetically by name)
var postgresConfigFileTasks = []SimpleConfigFileTask{
{
Expand Down Expand Up @@ -154,6 +175,9 @@ func execPGQueryOnDB(dbname string, cfg *Config, query string, w io.Writer) erro

rows, err := db.Query(query)
if err != nil {
if isPGUnavailableError(err) {
return NewSkipError(err.Error())
}
return fmt.Errorf("query failed: %w", err)
}
defer closeErrCheck(rows, "query rows")
Expand Down
44 changes: 44 additions & 0 deletions postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
package main

import (
"bytes"
"errors"
"strings"
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/jackc/pgx/v5/pgconn"
)

// TestPostgreSQLCollectors verifies all expected PostgreSQL collectors are registered
Expand Down Expand Up @@ -151,3 +156,42 @@ func TestPostgreSQLTasksStructure(t *testing.T) {
}
}
}

// TestPGQueryCollectorUnavailableAsSkip verifies that PG errors for missing
// extensions (undefined_table/function/object, invalid_schema) are returned
// as SkipError, while real errors (permission denied) are returned as-is.
func TestPGQueryCollectorUnavailableAsSkip(t *testing.T) {
tests := []struct {
name string
pgErr *pgconn.PgError
wantSkip bool
}{
{"undefined_table is skip", &pgconn.PgError{Code: "42P01", Message: "relation \"pg_stat_statements\" does not exist"}, true},
{"undefined_function is skip", &pgconn.PgError{Code: "42883", Message: "function does not exist"}, true},
{"undefined_object is skip", &pgconn.PgError{Code: "42704", Message: "type does not exist"}, true},
{"invalid_schema is skip", &pgconn.PgError{Code: "3F000", Message: "schema \"pgstatviz\" does not exist"}, true},
{"permission_denied is real error", &pgconn.PgError{Code: "42501", Message: "permission denied"}, false},
{"syntax_error is real error", &pgconn.PgError{Code: "42601", Message: "syntax error"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to create mock: %v", err)
}
mock.ExpectQuery("SELECT").WillReturnError(tt.pgErr)

collector := pgQueryCollector(db, "SELECT 1")
err = collector(&Config{}, &bytes.Buffer{})

if err == nil {
t.Fatal("expected error, got nil")
}
var skipErr SkipError
isSkip := errors.As(err, &skipErr)
if isSkip != tt.wantSkip {
t.Errorf("errors.As SkipError = %v, want %v (err: %v)", isSkip, tt.wantSkip, err)
}
})
}
}
3 changes: 3 additions & 0 deletions radar.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,9 @@ func pgQueryCollector(db *sql.DB, query string) func(*Config, io.Writer) error {
}
rows, err := db.Query(query)
if err != nil {
if isPGUnavailableError(err) {
return NewSkipError(err.Error())
}
return err
}
defer closeErrCheck(rows, "query rows")
Expand Down
Loading