Skip to content

Add First and FirstToSQL ergonomic select helpers#125

Merged
cungminh2710 merged 2 commits into
mainfrom
feat/select-first-ergonomics-16060601254025061624
Jun 17, 2026
Merged

Add First and FirstToSQL ergonomic select helpers#125
cungminh2710 merged 2 commits into
mainfrom
feat/select-first-ergonomics-16060601254025061624

Conversation

@cungminh2710

Copy link
Copy Markdown
Contributor

Added First(ctx, dest) and FirstToSQL() methods to SelectQuery. First executes the query with an implicit LIMIT 1 and returns sql.ErrNoRows if the result set is empty. FirstToSQL is provided as a helper to inspect the generated SQL for single-row queries. Both methods use q.clone() to ensure no side effects on the original query builder. Unit and integration tests were added to verify correct SQL generation and runtime behavior.


PR created automatically by Jules for task 16060601254025061624 started by @cungminh2710

This change introduces First() and FirstToSQL() methods to the SelectQuery
builder, providing a convenient way to fetch a single record or inspect
the SQL for a single-record query. It aligns Rain ORM more closely with
Drizzle's findFirst ergonomics.

Co-authored-by: cungminh2710 <8063319+cungminh2710@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds two ergonomic helpers to SelectQuery: First(ctx, dest) executes a query with an implicit LIMIT 1 and scans a single struct, and FirstToSQL() returns the SQL for inspection. Both work via q.clone() so the original builder is unmodified.

  • First validates the destination with reflect (must be a non-nil pointer to a struct), rejecting slice pointers early to enforce the sql.ErrNoRows contract — directly addressing the feedback from a prior review cycle.
  • All three scan paths (scanRowsAgainstTableDirect, scanCachedRowsAgainstTable, and scanRowsWithRelations) return sql.ErrNoRows for a struct destination when no rows are found, so the documented contract holds end-to-end.
  • Unit tests cover SQL generation and integration tests cover the happy path, the sql.ErrNoRows case, and the slice-rejection guard.

Confidence Score: 5/5

Safe to merge — the two new methods are additive, well-guarded, and do not modify any existing code paths.

The reflect-based type guard in First correctly enforces the single-struct-pointer contract before any I/O happens. All three downstream scan paths already return sql.ErrNoRows for struct destinations, so the documented error contract holds in every execution branch. Both new public methods are covered by unit and integration tests. No existing behavior is changed.

No files require special attention.

Important Files Changed

Filename Overview
pkg/rain/query_select.go Adds FirstToSQL and First adjacent to ToSQL. The reflect-based type guard correctly rejects nil pointers, non-pointer values, and slice/map destinations before delegating to clone().Limit(1).Scan().
pkg/rain/query_select_test.go Adds TestSelectFirstToSQL verifying the generated SQL and bound args for a Postgres dialect query. Parallel-safe and follows the existing test pattern.
pkg/rain/sqlite_integration_test.go Adds TestSQLiteIntegrationFirst with three sub-tests: happy-path struct scan, sql.ErrNoRows on empty result, and rejection of a slice destination. Covers all documented behaviors.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant First
    participant clone as clone().Limit(1)
    participant Scan
    participant scanRows as scanRowsAgainstTableDirect

    Caller->>First: First(ctx, dest)
    First->>First: "reflect check (non-nil *struct)"
    alt invalid dest (slice, nil, non-pointer)
        First-->>Caller: error "must be a non-nil pointer to a struct"
    end
    First->>clone: q.clone().Limit(1)
    clone->>Scan: .Scan(ctx, dest)
    Scan->>scanRows: execute SQL with LIMIT 1
    alt no rows
        scanRows-->>Scan: sql.ErrNoRows
        Scan-->>Caller: sql.ErrNoRows
    else row found
        scanRows-->>Scan: nil
        Scan-->>Caller: nil (dest populated)
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant First
    participant clone as clone().Limit(1)
    participant Scan
    participant scanRows as scanRowsAgainstTableDirect

    Caller->>First: First(ctx, dest)
    First->>First: "reflect check (non-nil *struct)"
    alt invalid dest (slice, nil, non-pointer)
        First-->>Caller: error "must be a non-nil pointer to a struct"
    end
    First->>clone: q.clone().Limit(1)
    clone->>Scan: .Scan(ctx, dest)
    Scan->>scanRows: execute SQL with LIMIT 1
    alt no rows
        scanRows-->>Scan: sql.ErrNoRows
        Scan-->>Caller: sql.ErrNoRows
    else row found
        scanRows-->>Scan: nil
        Scan-->>Caller: nil (dest populated)
    end
Loading

Reviews (2): Last reviewed commit: "feat(rain): add First and FirstToSQL erg..." | Re-trigger Greptile

Comment thread pkg/rain/query_select.go Outdated
Comment thread pkg/rain/query_select.go
@cungminh2710

Copy link
Copy Markdown
Contributor Author

@jules Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.


Issue 1 of 2

pkg/rain/query_select.go:614-618
sql.ErrNoRows guarantee breaks for slice destinations

The doc comment says "Returns sql.ErrNoRows if the result set is empty," but this is only true when dest is a pointer to a struct. scanRowsAgainstTableDirect only returns sql.ErrNoRows in the reflect.Struct branch (line 269 of model.go); for a reflect.Slice destination it returns nil with an empty slice. A caller who passes *[]SomeRow to First and checks errors.Is(err, sql.ErrNoRows) will never observe the expected error, silently masking an empty-set condition.

Consider adding an early type-check at the top of First (rejecting slice pointers with a clear error, or unwrapping to confirm a struct pointer) so the documented contract is enforced regardless of what the caller passes as dest.

Issue 2 of 2

pkg/rain/query_select.go:434-437
FirstToSQL and First are not co-located

FirstToSQL is defined at line 434 (immediately after ToSQL), while its execution-time counterpart First appears at line 614 — separated by ~180 lines of SQL-writer helpers. Keeping the two public ergonomic helpers adjacent makes them easier to discover and keeps their doc comments visually paired. Consider moving First to be immediately after FirstToSQL (or vice versa) to mirror the pattern used for ToSQL and Scan.

@google-labs-jules

Copy link
Copy Markdown
Contributor

@jules Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.


Issue 1 of 2

pkg/rain/query_select.go:614-618
sql.ErrNoRows guarantee breaks for slice destinations

The doc comment says "Returns sql.ErrNoRows if the result set is empty," but this is only true when dest is a pointer to a struct. scanRowsAgainstTableDirect only returns sql.ErrNoRows in the reflect.Struct branch (line 269 of model.go); for a reflect.Slice destination it returns nil with an empty slice. A caller who passes *[]SomeRow to First and checks errors.Is(err, sql.ErrNoRows) will never observe the expected error, silently masking an empty-set condition.

Consider adding an early type-check at the top of First (rejecting slice pointers with a clear error, or unwrapping to confirm a struct pointer) so the documented contract is enforced regardless of what the caller passes as dest.

Issue 2 of 2

pkg/rain/query_select.go:434-437
FirstToSQL and First are not co-located

FirstToSQL is defined at line 434 (immediately after ToSQL), while its execution-time counterpart First appears at line 614 — separated by ~180 lines of SQL-writer helpers. Keeping the two public ergonomic helpers adjacent makes them easier to discover and keeps their doc comments visually paired. Consider moving First to be immediately after FirstToSQL (or vice versa) to mirror the pattern used for ToSQL and Scan.

I've addressed both issues. First now enforces that the destination is a non-nil pointer to a struct, which guarantees that sql.ErrNoRows will be returned correctly if no matching record is found. I've also co-located First and FirstToSQL in pkg/rain/query_select.go for better discoverability and consistency.

- Added First() and FirstToSQL() methods to SelectQuery.
- First() enforces destination is a struct pointer to guarantee sql.ErrNoRows behavior.
- Co-located First() and FirstToSQL() for better ergonomics.
- Added unit and integration tests.

Co-authored-by: cungminh2710 <8063319+cungminh2710@users.noreply.github.com>
@cungminh2710 cungminh2710 merged commit 3aacf9e into main Jun 17, 2026
5 checks passed
@cungminh2710 cungminh2710 deleted the feat/select-first-ergonomics-16060601254025061624 branch June 17, 2026 01:37
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.

1 participant