Skip to content

fix(storage/sql): skip empty bindings to GORM Where for nil-only filters#173

Open
Lutherwaves wants to merge 2 commits into
mainfrom
fix/buildquery-empty-bindings-pgx-simple-protocol
Open

fix(storage/sql): skip empty bindings to GORM Where for nil-only filters#173
Lutherwaves wants to merge 2 commits into
mainfrom
fix/buildquery-empty-bindings-pgx-simple-protocol

Conversation

@Lutherwaves

@Lutherwaves Lutherwaves commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Problem

SQLAdapter.{Get,List,Update,Delete,Count} accept a map[string]any filter. When every value in the filter is nil, buildQuery produces a clause like col IS NULL plus an empty map[string]any{} of bindings. That empty map is forwarded to GORM as a parameter to .Where(query, bindings).

Under pgx5 with PreferSimpleProtocol: true (which is what OpenConnection configures for Postgres), the empty map cannot be encoded:

unable to encode map[string]interface{}{} into text format for unknown type (OID 0): cannot find encode plan

The call returns an error for shapes that should be valid:

adapter.Count(&Foo{}, map[string]any{"deleted_at": nil})    // count non-deleted rows
adapter.List(&dst, "id", map[string]any{"archived_at": nil}, 50, "")  // list non-archived
adapter.Get(&dst, map[string]any{"resolved_at": nil})       // first unresolved

Filters with at least one non-nil value (e.g. {"id": id, "deleted_at": nil}) work fine because the bindings map is non-empty.

Fix

Add SQLAdapter.applyFilter(q, filter) that calls buildQuery and forwards bindings to .Where only when the bindings map is non-empty. The five methods that fed buildQuery output into .Where now route through it.

No behavior change for filters with at least one non-nil value.

Test plan

  • go build ./...
  • go vet ./...
  • Existing tests pass
  • TestSQLAdapterNilOnlyFilterDoesNotForwardEmptyBindings covers Count, Get, List with nil-only filters. The in-memory sqlite adapter tolerates the broken empty-map shape, so the test asserts behavioral correctness rather than the wire-level crash; the comment block on the test documents what to verify against a real pgx5 simple-protocol harness.

Summary by CodeRabbit

  • Bug Fixes
    • Improved filtering in database query operations to reliably handle null-value filter conditions.

Review Change Stack

@Lutherwaves Lutherwaves self-assigned this Apr 28, 2026
@Lutherwaves Lutherwaves force-pushed the fix/buildquery-empty-bindings-pgx-simple-protocol branch from d304a47 to 79b7488 Compare May 4, 2026 21:55
@Lutherwaves Lutherwaves marked this pull request as ready for review May 4, 2026 22:12
@Lutherwaves Lutherwaves closed this May 4, 2026
@Lutherwaves Lutherwaves reopened this May 4, 2026
@Lutherwaves

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a5812238-a903-49b2-982c-9055ca9e58cb

📥 Commits

Reviewing files that changed from the base of the PR and between 403465e and 39f53b5.

📒 Files selected for processing (2)
  • storage/sql.go
  • storage/sql_test.go

📝 Walkthrough

Walkthrough

The SQL adapter now centralizes filter application in a new internal applyFilter helper that omits empty parameter bindings when calling GORM's Where; GetContext, UpdateContext, DeleteContext, ListContext (filtered), and CountContext (filtered) were updated to use it, and a test verifies nil-only filters don't forward empty bindings.

Changes

Filter Parameter Handling Refinement

Layer / File(s) Summary
Core Filter Helper
storage/sql.go
New applyFilter(q *gorm.DB, filter map[string]any) *gorm.DB builds the query and bindings via buildQuery and calls q.Where(query) if bindings is empty, otherwise q.Where(query, bindings).
Method Wiring
storage/sql.go
GetContext, UpdateContext, DeleteContext, ListContext (when filter non-empty), and CountContext (when filter non-empty) now call applyFilter instead of constructing query, bindings and calling q.Where(query, bindings) directly.
Test Coverage
storage/sql_test.go
Adds nilFilterItem fixture and TestSQLAdapterNilOnlyFilterDoesNotForwardEmptyBindings which creates nil_filter_items, inserts live and soft-deleted rows, then asserts Count, Get, and List with {"deleted_at": nil} return only the live row without errors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title exceeds the 50-character requirement at 72 characters, violating the stated constraint despite being descriptive and directly related to the changeset. Condense the title to under 50 characters while preserving the key information. Consider: 'fix(storage/sql): skip empty bindings for nil filters' (53 chars) or similar abbreviation.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 fix/buildquery-empty-bindings-pgx-simple-protocol

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@storage/sql_test.go`:
- Around line 328-369: Extend
TestSQLAdapterNilOnlyFilterDoesNotForwardEmptyBindings to also assert the Update
and Delete code paths: call sql.Update (targeting e.g. the non-key field or a
dummy field) with filter map[string]any{"deleted_at": nil} and verify only the
"live" row is affected (check the row was updated and the "gone" row remains
unchanged), and call sql.Delete with the same nil-only filter and verify only
the "live" row is removed (confirm Count/List/Get reflect that deletion while
the "gone" row remains). Use the existing nilFilterItem records ("live" and
"gone") and the same sql variable and filter map to locate the code paths for
sql.Update and sql.Delete.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 1fee8c0f-faf4-48aa-9669-c66d24a2b0b3

📥 Commits

Reviewing files that changed from the base of the PR and between 0255b0b and 403465e.

📒 Files selected for processing (2)
  • storage/sql.go
  • storage/sql_test.go

Comment thread storage/sql_test.go
Comment on lines +328 to +369
func TestSQLAdapterNilOnlyFilterDoesNotForwardEmptyBindings(t *testing.T) {
m := storage.GetMemoryAdapterInstance()
if err := m.Execute(`CREATE TABLE IF NOT EXISTS nil_filter_items (id TEXT PRIMARY KEY, deleted_at TEXT)`); err != nil {
t.Fatalf("create table: %v", err)
}
if err := m.Execute(`DELETE FROM nil_filter_items`); err != nil {
t.Fatalf("truncate: %v", err)
}
sql := m.DB

if err := sql.Create(&nilFilterItem{Id: "live"}); err != nil {
t.Fatalf("Create live: %v", err)
}
deletedTs := "2026-01-01"
if err := sql.Create(&nilFilterItem{Id: "gone", DeletedAt: &deletedTs}); err != nil {
t.Fatalf("Create gone: %v", err)
}

total, err := sql.Count(&[]nilFilterItem{}, map[string]any{"deleted_at": nil})
if err != nil {
t.Fatalf("Count with nil-only filter: %v", err)
}
if total != 1 {
t.Fatalf("Count = %d; want 1 (only the non-deleted row)", total)
}

var got nilFilterItem
if err := sql.Get(&got, map[string]any{"deleted_at": nil}); err != nil {
t.Fatalf("Get with nil-only filter: %v", err)
}
if got.Id != "live" {
t.Fatalf("Get id = %q; want %q", got.Id, "live")
}

var listed []nilFilterItem
if _, err := sql.List(&listed, "id", map[string]any{"deleted_at": nil}, 10, ""); err != nil {
t.Fatalf("List with nil-only filter: %v", err)
}
if len(listed) != 1 || listed[0].Id != "live" {
t.Fatalf("List = %+v; want one row id=live", listed)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Extend this regression test to cover Update and Delete paths changed in this PR.

Count/Get/List are covered, but Update/Delete were also switched to applyFilter. Adding one nil-only filter assertion for each will lock in coverage for all touched entrypoints.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@storage/sql_test.go` around lines 328 - 369, Extend
TestSQLAdapterNilOnlyFilterDoesNotForwardEmptyBindings to also assert the Update
and Delete code paths: call sql.Update (targeting e.g. the non-key field or a
dummy field) with filter map[string]any{"deleted_at": nil} and verify only the
"live" row is affected (check the row was updated and the "gone" row remains
unchanged), and call sql.Delete with the same nil-only filter and verify only
the "live" row is removed (confirm Count/List/Get reflect that deletion while
the "gone" row remains). Use the existing nilFilterItem records ("live" and
"gone") and the same sql variable and filter map to locate the code paths for
sql.Update and sql.Delete.

When a filter contains only nil values (e.g. {"deleted_at": nil}),
buildQuery returns the SQL clause "deleted_at IS NULL" plus an empty
map[string]any{} as bindings. Forwarding that empty map as a parameter
to gorm's .Where(query, bindings) is fine on most drivers, but under
pgx5 with PreferSimpleProtocol: true (which is enabled by the default
Postgres setup in OpenConnection) it fails with:

    unable to encode map[string]interface{}{} into text format for
    unknown type (OID 0): cannot find encode plan

This 500s every Count/Get/List/Update/Delete call where every filter
value happens to be nil — a common shape for "non-deleted rows" counts.

Fix by introducing applyFilter, which forwards bindings only when the
map is non-empty. All five SQLAdapter methods that previously passed
buildQuery's output to .Where now go through it.

No behavior change for filters with at least one non-nil value.
Pins the fix in the previous commit by exercising Count/Get/List with a
nil-only filter ({"deleted_at": nil}). Sqlite tolerates the broken empty-
map shape that pgx5 simple-protocol rejects, so the test asserts the
behavioral contract — nil-only filters return the expected rows — rather
than the wire-level crash. If a future cleanup reintroduces empty
map[string]any{} forwarding to gorm.Where, this test still passes under
sqlite but the matching pgx5 path will regress; the comment block on the
test documents that linkage so the next reader knows what to test against
a real Postgres harness.
@Lutherwaves Lutherwaves force-pushed the fix/buildquery-empty-bindings-pgx-simple-protocol branch from 403465e to 39f53b5 Compare May 27, 2026 14:30
@Lutherwaves Lutherwaves added bug Something isn't working go Pull requests that update go code labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant