Skip to content

fix: enable goqu prepared statements globally#1579

Merged
AmanGIT07 merged 1 commit intomainfrom
fix/sql-prepared-mode
Apr 30, 2026
Merged

fix: enable goqu prepared statements globally#1579
AmanGIT07 merged 1 commit intomainfrom
fix/sql-prepared-mode

Conversation

@AmanGIT07
Copy link
Copy Markdown
Contributor

Summary

Forces every goqu dataset to use prepared statements ($N placeholders + separate args) instead of inlining values into the SQL string, by setting
goqu.SetDefaultPrepared(true) globally in cmd/serve.go::setupDB. SQL injection becomes structurally impossible at the goqu layer rather than relying on per-site
discipline.

Flipping the flag surfaced several latent bugs that worked silently under value-interpolation but broke under prepared mode. They're fixed in the same PR so the flip is safe
to ship.

Changes

  • Enable goqu.SetDefaultPrepared(true) in cmd/serve.go::setupDB.
  • 5 params-discard fixes (query, _, err := stmt.ToSQL() followed by an exec without forwarding params...):
    • relation_repository.GetByFields / ListByFields
    • organization_repository.List totalCount
    • project_repository.List totalCount
    • billing_invoice_repository.List totalCount
  • session_repository.UpdateValidityINTERVAL '? hours' (goqu substitutes ? regardless of quote context, breaking under prepared mode) → make_interval(secs => ?).
  • role_repository.Upsert / Update — drop the manual goqu.L("$N") placeholder workaround; pass real values through goqu.Record{}. Wire size grows by ~5 params on
    Upsert; database state is byte-for-byte identical.
  • policy_repository.GroupMemberCount / ProjectMemberCount / OrgMemberCountgoqu.From("policies")dialect.From("policies"). The package-level goqu.From uses
    goqu's default dialect (? placeholders), which PostgreSQL rejects in prepared mode.
  • audit_record_repository.go — added // #nosec G201 annotations with justifications on the two fmt.Sprintf calls building DECLARE … CURSOR FOR … and FETCH … FROM …
    (Postgres has no parameter binding for cursor identifiers; cursorName is server-generated via crypto/rand).
  • .golangci.yml:
    • Enable gosec with G201/G202 only.
    • Enable forbidigo with three narrow patterns: ? inside a single-quoted goqu.L, params-discard from ToSQL(), and goqu.From( (use dialect.From instead).
  • .github/pull_request_template.md — added a 4-item SQL Safety checklist.

Technical Details

Three bug shapes lint catches:

  • ? inside single-quoted goqu.L — goqu's literal templater walks char-by-char, substituting ? regardless of quote context. Prepared mode emits INTERVAL '$1 hours'
    literal + a bound arg, so Postgres rejects with bind message supplies 1 parameters, but prepared statement requires 0.
  • Discarding params from ToSQL() — works in interpolated mode (params is []); under prepared mode the SQL has $N placeholders without values bound.
  • goqu.From(...) — uses goqu's default dialect (? placeholders). PostgreSQL parses ? as a syntax error.

role_repository's old goqu.L("$1")…goqu.L("$8") was a workaround for goqu not reusing parameter positions across INSERT VALUES and ON CONFLICT DO UPDATE. With
prepared mode on, goqu emits fresh placeholders ($9..$13 for the UPDATE clause) and the values get sent twice on the wire — strings/json/UUIDs, negligible.

Test Plan

  • Manual testing completed (org/project/role lifecycle in make compose-up-dev)
  • Build and type checking passes (make build, golangci-lint run ./... — 0 issues)
  • make test passes
  • go test -v ./test/e2e/... regression suite passes (TestProjectAPI/7,8, TestCheckoutAPI/2, TestCheckFeatureEntitlementAPI/2 were failing pre-fix — all routed
    through policy_repository.*MemberCount)

SQL Safety

  • Values flow through ? placeholders, goqu.Ex{}, or goqu.Record{} — never fmt.Sprintf or + building a query that gets executed.
  • ToSQL() callers capture and forward params (query, params, err := stmt.ToSQL(); db.…Context(ctx, …, query, params...)). Never query, _, err := ….
  • No ? placeholders inside single-quoted SQL literals in goqu.L (use make_interval(hours => ?)-style functions instead).
  • Any //nolint:forbidigo or // #nosec G20x annotation has a one-line justification on the same line that a reviewer can verify.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Apr 28, 2026 11:24am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Fixed SQL query parameter binding across database repositories to prevent incomplete query execution.
    • Improved SQL security with enhanced lint checks and parameter validation.
  • Tests

    • Added comprehensive test coverage for query parameter handling across repositories.
  • Chores

    • Configured database to use prepared statements by default.
    • Updated PR template and linter configuration to enforce secure SQL query practices.

Walkthrough

This PR introduces SQL injection prevention across the codebase by enforcing parameterized query execution. It enables linter rules for detecting unsafe SQL patterns, configures goqu to use prepared statements globally, updates repository methods to capture and pass parameters from ToSQL() calls, and adds regression tests validating parameterized SQL generation.

Changes

Cohort / File(s) Summary
Configuration & Documentation
.github/pull_request_template.md, .golangci.yml
Adds SQL safety checklist to PR template and enables forbidigo/gosec linter rules to detect unsafe goqu patterns including ? in single quotes, discarded parameters, and non-dialect-scoped From() constructors.
Server Configuration
cmd/serve.go
Configures goqu globally to use prepared statements with parameterized $N placeholders instead of value inlining.
Audit & Invoice Repositories
internal/store/postgres/audit_record_repository.go, internal/store/postgres/billing_invoice_repository.go
Adds #nosec annotations for cursor-related dynamic SQL in audit repository; fixes billing invoice repository's count query to capture and pass ToSQL() parameters.
Repository Parameter Binding Fixes
internal/store/postgres/organization_repository.go, internal/store/postgres/policy_repository.go, internal/store/postgres/project_repository.go, internal/store/postgres/relation_repository.go, internal/store/postgres/role_repository.go, internal/store/postgres/session_repository.go
Updates multiple repositories to capture parameters from ToSQL() and pass them to database execution calls; changes policy repository to use dialect-scoped From(); refactors role repository to use value-based inserts/updates instead of manual placeholders; updates session repository interval construction logic.
Test Coverage & Assertions
internal/store/postgres/billing_invoice_repository_test.go, internal/store/postgres/org_users_repository_test.go
Adds regression tests for prepared-statement SQL generation using Prepared(true) with expected placeholder validation; updates existing unit tests to verify both generated SQL and parameter binding correctness.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
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.


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

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

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.

🧹 Nitpick comments (4)
.github/pull_request_template.md (1)

20-23: Clarify exception policy to avoid contradictory checklist signals.

Line 20 reads as an absolute prohibition, but Line 23 allows justified suppressions (which can include safe dynamic SQL cases). Consider explicitly noting “except narrowly scoped, reviewer-verifiable cases with inline justification” to keep author guidance unambiguous.

.golangci.yml (1)

36-37: Broaden discarded-params detection in forbidigo pattern.

The current regex only catches cases where the third variable is literally named err. This can miss equivalent discards with different variable names.

♻️ Proposed regex tweak
-        - pattern: '\w+,\s*_,\s*err\s*[:=]+[^=]*\.ToSQL\(\)'
+        - pattern: '\w+,\s*_,\s*\w+\s*[:=]+[^=]*\.ToSQL\(\)'
internal/store/postgres/relation_repository.go (1)

237-237: Use ListByFields as the timeout operation label.

Line 237 still uses "GetByFields" for the list path, which makes DB traces/metrics harder to distinguish.

Suggested tweak
-	if err = r.dbc.WithTimeout(ctx, TABLE_RELATIONS, "GetByFields", func(ctx context.Context) error {
+	if err = r.dbc.WithTimeout(ctx, TABLE_RELATIONS, "ListByFields", func(ctx context.Context) error {
internal/store/postgres/billing_invoice_repository_test.go (1)

213-214: Strengthen placeholder assertions beyond $1 presence.

Current checks can still pass if later placeholders regress. Consider also asserting placeholder count/index progression matches len(params) in each case.

Also applies to: 240-241, 253-254, 267-268, 281-282


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 469f5416-d647-4e79-b8f9-00858ab3f086

📥 Commits

Reviewing files that changed from the base of the PR and between d11eb4e and 7042b71.

📒 Files selected for processing (13)
  • .github/pull_request_template.md
  • .golangci.yml
  • cmd/serve.go
  • internal/store/postgres/audit_record_repository.go
  • internal/store/postgres/billing_invoice_repository.go
  • internal/store/postgres/billing_invoice_repository_test.go
  • internal/store/postgres/org_users_repository_test.go
  • internal/store/postgres/organization_repository.go
  • internal/store/postgres/policy_repository.go
  • internal/store/postgres/project_repository.go
  • internal/store/postgres/relation_repository.go
  • internal/store/postgres/role_repository.go
  • internal/store/postgres/session_repository.go

@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 25049997558

Coverage decreased (-0.01%) to 42.342%

Details

  • Coverage decreased (-0.01%) from the base build.
  • Patch coverage: 17 uncovered changes across 6 files (29 of 46 lines covered, 63.04%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
cmd/serve.go 4 0 0.0%
internal/store/postgres/audit_record_repository.go 4 0 0.0%
internal/store/postgres/relation_repository.go 4 0 0.0%
internal/store/postgres/billing_invoice_repository.go 2 0 0.0%
internal/store/postgres/project_repository.go 2 0 0.0%
internal/store/postgres/session_repository.go 1 0 0.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 37074
Covered Lines: 15698
Line Coverage: 42.34%
Coverage Strength: 11.75 hits per line

💛 - Coveralls

@AmanGIT07 AmanGIT07 enabled auto-merge (squash) April 28, 2026 11:56
@AmanGIT07 AmanGIT07 requested a review from rohilsurana April 29, 2026 07:33
@AmanGIT07 AmanGIT07 merged commit f375026 into main Apr 30, 2026
8 checks passed
@AmanGIT07 AmanGIT07 deleted the fix/sql-prepared-mode branch April 30, 2026 06:21
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.

3 participants