Skip to content

test: support per-suite databases on external postgres#1931

Merged
moshloop merged 1 commit into
mainfrom
test/external-postgres-per-suite-db
May 1, 2026
Merged

test: support per-suite databases on external postgres#1931
moshloop merged 1 commit into
mainfrom
test/external-postgres-per-suite-db

Conversation

@adityathebe
Copy link
Copy Markdown
Member

@adityathebe adityathebe commented Apr 28, 2026

Duty tests can run against an always-on Postgres server via DUTY_DB_URL, but create mode reused predictable database names and rewrote URLs with string replacement.

Generate a unique database name for each suite process so multiple suites can share the same Postgres server safely. Build per-database URLs with net/url and quote database identifiers for create/drop operations.

This keeps embedded Postgres behavior unchanged while enabling faster local test runs against a long-lived Postgres server.

Summary by CodeRabbit

  • Tests
    • Enhanced database initialization and cleanup for better reliability, with randomized database naming to improve test isolation.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Benchstat (RLS)

Base: 68180c4fd9a3d4400fddcedd737d14b0d436031f
Head: 05ce2409ba5a3a2be0a1cdd73f8f0a0ba19cc201

⚠️ 7 regression(s) detected (threshold: >5%)

Benchmark Base Head Change p-value
RLS/Sample-15000/catalog_changes/Without_RLS-4 3.436m 3.788m +10.22% 🔴 0.002
RLS/Sample-15000/config_changes/Without_RLS-4 3.446m 3.795m +10.13% 🔴 0.002
RLS/Sample-15000/config_names/Without_RLS-4 9.146m 9.631m +5.30% 🔴 0.002
RLS/Sample-15000/config_summary/Without_RLS-4 43.60m 45.42m +4.19% 0.002
RLS/Sample-15000/config_types/With_RLS-4 85.52m 86.82m +1.51% 0.002
RLS/Sample-15000/config_detail/With_RLS-4 84.21m 85.19m +1.16% 0.002
RLS/Sample-15000/config_types/Without_RLS-4 2.627m 2.656m +1.09% 0.026
✅ 2 improvement(s)
Benchmark Base Head Change p-value
RLS/Sample-15000/configs/With_RLS-4 86.12m 84.31m -2.11% 0.002
RLS/Sample-15000/catalog_changes/With_RLS-4 89.59m 88.42m -1.31% 0.041

Failed: 3 benchmark(s) regressed by more than 5%:
RLS/Sample-15000/catalog_changes/Without_RLS-4: 3.436m -> 3.788m (+10.22%)
RLS/Sample-15000/config_changes/Without_RLS-4: 3.446m -> 3.795m (+10.13%)
RLS/Sample-15000/config_names/Without_RLS-4: 9.146m -> 9.631m (+5.30%)

Full benchstat output
goos: linux
goarch: amd64
pkg: github.com/flanksource/duty/bench
cpu: AMD EPYC 9V74 80-Core Processor                
                                               │ bench-base.txt │           bench-head.txt            │
                                               │     sec/op     │    sec/op     vs base               │
RLS/Sample-15000/catalog_changes/Without_RLS-4      3.436m ± 1%   3.788m ±  9%  +10.22% (p=0.002 n=6)
RLS/Sample-15000/catalog_changes/With_RLS-4         89.59m ± 2%   88.42m ±  0%   -1.31% (p=0.041 n=6)
RLS/Sample-15000/config_changes/Without_RLS-4       3.446m ± 1%   3.795m ± 17%  +10.13% (p=0.002 n=6)
RLS/Sample-15000/config_changes/With_RLS-4          88.40m ± 0%   88.67m ±  1%        ~ (p=0.065 n=6)
RLS/Sample-15000/config_detail/Without_RLS-4        2.675m ± 1%   2.693m ±  2%        ~ (p=0.394 n=6)
RLS/Sample-15000/config_detail/With_RLS-4           84.21m ± 0%   85.19m ±  0%   +1.16% (p=0.002 n=6)
RLS/Sample-15000/config_names/Without_RLS-4         9.146m ± 4%   9.631m ±  1%   +5.30% (p=0.002 n=6)
RLS/Sample-15000/config_names/With_RLS-4            84.84m ± 2%   85.34m ±  0%        ~ (p=0.394 n=6)
RLS/Sample-15000/config_summary/Without_RLS-4       43.60m ± 1%   45.42m ±  2%   +4.19% (p=0.002 n=6)
RLS/Sample-15000/config_summary/With_RLS-4          499.8m ± 1%   499.1m ±  2%        ~ (p=0.937 n=6)
RLS/Sample-15000/configs/Without_RLS-4              5.042m ± 1%   5.604m ± 11%        ~ (p=0.132 n=6)
RLS/Sample-15000/configs/With_RLS-4                 86.12m ± 0%   84.31m ±  1%   -2.11% (p=0.002 n=6)
RLS/Sample-15000/analysis_types/Without_RLS-4       2.626m ± 0%   2.619m ±  2%        ~ (p=0.589 n=6)
RLS/Sample-15000/analysis_types/With_RLS-4          2.624m ± 3%   2.627m ±  1%        ~ (p=0.818 n=6)
RLS/Sample-15000/analyzer_types/Without_RLS-4       2.497m ± 1%   2.499m ±  2%        ~ (p=0.937 n=6)
RLS/Sample-15000/analyzer_types/With_RLS-4          2.516m ± 2%   2.504m ±  1%        ~ (p=0.093 n=6)
RLS/Sample-15000/change_types/Without_RLS-4         3.451m ± 1%   3.432m ±  1%        ~ (p=0.240 n=6)
RLS/Sample-15000/change_types/With_RLS-4            3.515m ± 1%   3.487m ±  3%        ~ (p=0.310 n=6)
RLS/Sample-15000/config_classes/Without_RLS-4       2.194m ± 3%   2.191m ±  1%        ~ (p=0.589 n=6)
RLS/Sample-15000/config_classes/With_RLS-4          85.13m ± 1%   85.05m ±  0%        ~ (p=0.485 n=6)
RLS/Sample-15000/config_types/Without_RLS-4         2.627m ± 1%   2.656m ±  2%   +1.09% (p=0.026 n=6)
RLS/Sample-15000/config_types/With_RLS-4            85.52m ± 0%   86.82m ±  1%   +1.51% (p=0.002 n=6)
geomean                                             13.04m        13.27m         +1.78%

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Benchstat (Other)

Base: 68180c4fd9a3d4400fddcedd737d14b0d436031f
Head: 05ce2409ba5a3a2be0a1cdd73f8f0a0ba19cc201

✅ 3 improvement(s)
Benchmark Base Head Change p-value
ResourceSelectorConfigs/tags-4 25.18m 21.49m -14.69% 0.002
ResourceSelectorQueryBuild/tags-4 12.90µ 12.61µ -2.22% 0.009
ResourceSelectorQueryBuild/name-4 32.01µ 31.76µ -0.78% 0.002
Full benchstat output
goos: linux
goarch: amd64
pkg: github.com/flanksource/duty/bench
cpu: AMD EPYC 9V74 80-Core Processor                
                                                       │ bench-base.txt │            bench-head.txt             │
                                                       │     sec/op     │     sec/op      vs base               │
InsertionForRowsWithAliases/external_users.aliases-4      716.0µ ± 136%   1291.1µ ± 138%        ~ (p=0.093 n=6)
InsertionForRowsWithAliases/config_items.external_id-4    2.035m ± 122%    1.804m ± 112%        ~ (p=0.589 n=6)
InsertionOfConfigsWithProperties-4                        4.192m ± 327%    3.191m ±  76%        ~ (p=0.240 n=6)
UpdateOfConfigsWithProperties-4                           8.604m ±  84%    9.328m ±  49%        ~ (p=0.818 n=6)
ResourceSelectorConfigs/name-4                            150.8µ ±   3%    151.8µ ±   2%        ~ (p=0.310 n=6)
ResourceSelectorConfigs/name_and_type-4                   164.7µ ±   2%    167.4µ ±   5%        ~ (p=0.240 n=6)
ResourceSelectorConfigs/tags-4                            25.18m ±  17%    21.49m ±   5%  -14.69% (p=0.002 n=6)
ResourceSelectorQueryBuild/name-4                         32.01µ ±   2%    31.76µ ±   0%   -0.78% (p=0.002 n=6)
ResourceSelectorQueryBuild/name_and_type-4                46.65µ ±   1%    46.43µ ±   1%        ~ (p=0.394 n=6)
ResourceSelectorQueryBuild/tags-4                         12.90µ ±   2%    12.61µ ±   1%   -2.22% (p=0.009 n=6)
geomean                                                   478.8µ           483.9µ          +1.07%

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Walkthrough

The test setup code now generates unique random database names (incorporating parallel-process ID, timestamp, and random bytes) instead of deterministic ones. Database operations use quoted identifiers and safer SQL syntax with IF EXISTS and FORCE clauses. Connection string derivation switches from string replacement to URL path parsing and reconstruction.

Changes

Cohort / File(s) Summary
Test Database Setup
tests/setup/common.go
Enhanced database initialization with unique random naming, quoted SQL identifiers, and safer DROP/CREATE operations using IF EXISTS and FORCE clauses. Connection string derivation refactored to use URL path parsing instead of string replacement.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: enabling per-suite databases when running tests against an external Postgres server.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/external-postgres-per-suite-db
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch test/external-postgres-per-suite-db

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Duty tests can use DUTY_DB_URL to run against an always-on Postgres server, but create mode reused predictable database names and rewrote URLs with string replacement.

Generate a unique database name for each suite process, build per-database URLs with net/url, and quote database identifiers for create/drop operations. Embedded Postgres behavior remains unchanged.
@moshloop moshloop enabled auto-merge (rebase) May 1, 2026 07:22
@moshloop moshloop force-pushed the test/external-postgres-per-suite-db branch from 534fd2d to 05ce240 Compare May 1, 2026 07:22
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/setup/common.go`:
- Around line 132-133: The error message currently returns the raw connection
string (variable "connection") which may contain credentials; change the error
to avoid leaking secrets by reporting only the scheme (u.Scheme) or a redacted
URL instead. Update the branch that checks u.Scheme in tests/setup/common.go so
the fmt.Errorf does not interpolate "connection" but instead uses u.Scheme (or a
function that redacts userinfo from the parsed URL) to produce the error text,
e.g. "unsupported postgres connection scheme: %s" or "unsupported postgres
connection URL (redacted): %s" using the sanitized value.
- Around line 229-245: The shutdown hook uses the package-global postgresDBUrl
which is later reassigned, so capture the admin URL and any DB-specific
identifiers into invocation-local variables (e.g., localPostgresDBUrl and
localQuotedDBName) before calling execPostgres and before registering
shutdown.AddHookWithPriority; update all execPostgres calls and the hook closure
to use these local variables (referencing postgresDBUrl, PgUrl, quoteIdentifier,
execPostgres, and shutdown.AddHookWithPriority) so each SetupDB invocation
cleans up its own database instead of the possibly-reassigned global.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f00ffc4-09dc-44c7-a4a0-e509f775b38b

📥 Commits

Reviewing files that changed from the base of the PR and between 68180c4 and 05ce240.

📒 Files selected for processing (1)
  • tests/setup/common.go

Comment thread tests/setup/common.go
Comment on lines +132 to +133
if u.Scheme != "postgres" && u.Scheme != "postgresql" {
return "", fmt.Errorf("unsupported postgres connection string: %s", connection)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t include the raw DSN in this error.

connection can contain credentials from DUTY_DB_URL, so this bubbles secrets into test/CI logs on a bad scheme. Report the scheme (or a redacted URL) instead.

Suggested fix
 	if u.Scheme != "postgres" && u.Scheme != "postgresql" {
-		return "", fmt.Errorf("unsupported postgres connection string: %s", connection)
+		return "", fmt.Errorf("unsupported postgres connection scheme %q", u.Scheme)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if u.Scheme != "postgres" && u.Scheme != "postgresql" {
return "", fmt.Errorf("unsupported postgres connection string: %s", connection)
if u.Scheme != "postgres" && u.Scheme != "postgresql" {
return "", fmt.Errorf("unsupported postgres connection scheme %q", u.Scheme)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/setup/common.go` around lines 132 - 133, The error message currently
returns the raw connection string (variable "connection") which may contain
credentials; change the error to avoid leaking secrets by reporting only the
scheme (u.Scheme) or a redacted URL instead. Update the branch that checks
u.Scheme in tests/setup/common.go so the fmt.Errorf does not interpolate
"connection" but instead uses u.Scheme (or a function that redacts userinfo from
the parsed URL) to produce the error text, e.g. "unsupported postgres connection
scheme: %s" or "unsupported postgres connection URL (redacted): %s" using the
sanitized value.

Comment thread tests/setup/common.go
Comment on lines 229 to +245
postgresDBUrl = url
dbName = fmt.Sprintf("duty_gingko%d", port)
PgUrl = strings.Replace(url, "/postgres", "/"+dbName, 1)
_ = execPostgres(postgresDBUrl, "DROP DATABASE "+dbName)
if err := execPostgres(postgresDBUrl, "CREATE DATABASE "+dbName); err != nil {
dbName = randomDatabaseName("duty_test")

var err error
PgUrl, err = databaseURL(url, dbName)
if err != nil {
return context.Context{}, err
}

quotedDBName := quoteIdentifier(dbName)
_ = execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName))
if err := execPostgres(postgresDBUrl, fmt.Sprintf("CREATE DATABASE %s", quotedDBName)); err != nil {
return context.Context{}, fmt.Errorf("cannot create %s: %v", dbName, err)
}

shutdown.AddHookWithPriority("remove postgres db", shutdown.PriorityCritical, func() {
if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE %s (FORCE)", dbName)); err != nil {
if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName)); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Capture the admin URL locally before registering cleanup.

The shutdown hook reads the package-global postgresDBUrl, which gets reassigned on every SetupDB call. If this runs more than once in a process, earlier hooks can clean up against the wrong Postgres instance. Use an invocation-local variable for the create/drop path and the hook.

Suggested fix
-		postgresDBUrl = url
+		adminURL := url
+		postgresDBUrl = adminURL
 		dbName = randomDatabaseName("duty_test")
 
 		var err error
 		PgUrl, err = databaseURL(url, dbName)
 		if err != nil {
 			return context.Context{}, err
 		}
 
 		quotedDBName := quoteIdentifier(dbName)
-		_ = execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName))
-		if err := execPostgres(postgresDBUrl, fmt.Sprintf("CREATE DATABASE %s", quotedDBName)); err != nil {
+		_ = execPostgres(adminURL, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName))
+		if err := execPostgres(adminURL, fmt.Sprintf("CREATE DATABASE %s", quotedDBName)); err != nil {
 			return context.Context{}, fmt.Errorf("cannot create %s: %v", dbName, err)
 		}
 
 		shutdown.AddHookWithPriority("remove postgres db", shutdown.PriorityCritical, func() {
-			if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName)); err != nil {
+			if err := execPostgres(adminURL, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName)); err != nil {
 				logger.Errorf("execPostgres: %v", err)
 			}
 		})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
postgresDBUrl = url
dbName = fmt.Sprintf("duty_gingko%d", port)
PgUrl = strings.Replace(url, "/postgres", "/"+dbName, 1)
_ = execPostgres(postgresDBUrl, "DROP DATABASE "+dbName)
if err := execPostgres(postgresDBUrl, "CREATE DATABASE "+dbName); err != nil {
dbName = randomDatabaseName("duty_test")
var err error
PgUrl, err = databaseURL(url, dbName)
if err != nil {
return context.Context{}, err
}
quotedDBName := quoteIdentifier(dbName)
_ = execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName))
if err := execPostgres(postgresDBUrl, fmt.Sprintf("CREATE DATABASE %s", quotedDBName)); err != nil {
return context.Context{}, fmt.Errorf("cannot create %s: %v", dbName, err)
}
shutdown.AddHookWithPriority("remove postgres db", shutdown.PriorityCritical, func() {
if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE %s (FORCE)", dbName)); err != nil {
if err := execPostgres(postgresDBUrl, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName)); err != nil {
adminURL := url
postgresDBUrl = adminURL
dbName = randomDatabaseName("duty_test")
var err error
PgUrl, err = databaseURL(url, dbName)
if err != nil {
return context.Context{}, err
}
quotedDBName := quoteIdentifier(dbName)
_ = execPostgres(adminURL, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName))
if err := execPostgres(adminURL, fmt.Sprintf("CREATE DATABASE %s", quotedDBName)); err != nil {
return context.Context{}, fmt.Errorf("cannot create %s: %v", dbName, err)
}
shutdown.AddHookWithPriority("remove postgres db", shutdown.PriorityCritical, func() {
if err := execPostgres(adminURL, fmt.Sprintf("DROP DATABASE IF EXISTS %s (FORCE)", quotedDBName)); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/setup/common.go` around lines 229 - 245, The shutdown hook uses the
package-global postgresDBUrl which is later reassigned, so capture the admin URL
and any DB-specific identifiers into invocation-local variables (e.g.,
localPostgresDBUrl and localQuotedDBName) before calling execPostgres and before
registering shutdown.AddHookWithPriority; update all execPostgres calls and the
hook closure to use these local variables (referencing postgresDBUrl, PgUrl,
quoteIdentifier, execPostgres, and shutdown.AddHookWithPriority) so each SetupDB
invocation cleans up its own database instead of the possibly-reassigned global.

@moshloop moshloop merged commit 7ce008e into main May 1, 2026
17 of 19 checks passed
@moshloop moshloop deleted the test/external-postgres-per-suite-db branch May 1, 2026 07:27
This was referenced May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants