Skip to content

feat(rain): implement First, TxOptions, and relation column selection#123

Closed
cungminh2710 wants to merge 2 commits into
mainfrom
feat/parity-improvements-2311615599171583350
Closed

feat(rain): implement First, TxOptions, and relation column selection#123
cungminh2710 wants to merge 2 commits into
mainfrom
feat/parity-improvements-2311615599171583350

Conversation

@cungminh2710

Copy link
Copy Markdown
Contributor

This PR adds several feature parity improvements inspired by Drizzle ORM:

  1. Implements .First() on SelectQuery and PreparedSelectQuery for ergonomic single-row fetching.
  2. Adds BeginTx and RunInTxOpts to DB for sql.TxOptions support.
  3. Adds Columns selection to RelationConfig, allowing users to restrict fetched columns in relational queries while ensuring mapping keys are preserved.

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

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 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds three feature-parity improvements to the rain ORM: First() on both SelectQuery and PreparedSelectQuery, BeginTx/RunInTxOpts for passing *sql.TxOptions, and a Columns field on RelationConfig to restrict which columns are fetched for related rows (with an automatic safety net that always includes the join key).

  • SelectQuery.First correctly clones the query and adds LIMIT 1 before executing; rain.go's transaction refactor is clean and backward-compatible.
  • PreparedSelectQuery.First is a direct alias to Scan with no LIMIT 1 baked into the prepared statement — every call fetches all matching rows from the database, silently diverging from the behaviour of SelectQuery.First.
  • The target-column safety check inside RelationConfig.Columns handling is duplicated verbatim in both loadRelatedRows and loadRelatedManyToManyRows, making future maintenance error-prone.

Confidence Score: 3/5

The transaction and relation-column-restriction changes are safe to merge; PreparedSelectQuery.First should be addressed before shipping to avoid silent full-table scans.

PreparedSelectQuery.First fetches every matching row from the database on every call — the prepared statement is compiled without LIMIT 1, so callers who migrate from SelectQuery.First will unknowingly issue unbounded queries. The remaining changes (BeginTx/RunInTxOpts, SelectQuery.First, and the relation Columns restriction) are correct and low-risk.

pkg/rain/prepared_select.go — the new First method needs either a LIMIT 1 baked into the prepared statement or removal with clear documentation that callers must add .Limit(1) themselves before calling .Prepare().

Important Files Changed

Filename Overview
pkg/rain/prepared_select.go Adds PreparedSelectQuery.First, but it is a direct alias to Scan with no LIMIT 1 applied — unlike SelectQuery.First, which clones and adds LIMIT 1 before executing. All matching rows are fetched from the database on every call.
pkg/rain/query_select.go Adds SelectQuery.First (correctly applies LIMIT 1 on a clone) and adds Columns []schema.Expression to RelationConfig. Both changes are straightforward and correct.
pkg/rain/rain.go Refactors Begin/RunInTx to delegate to new BeginTx/RunInTxOpts methods that accept *sql.TxOptions. The refactoring is clean and backward-compatible.
pkg/rain/relation_loading.go Applies config.Columns column restriction to both loadRelatedRows and loadRelatedManyToManyRows, with a safety check to auto-include TargetColumn. Logic is correct but duplicated in two places.

Sequence Diagram

sequenceDiagram
    participant C as Caller
    participant SQ as SelectQuery
    participant PSQ as PreparedSelectQuery
    participant DB as Database

    Note over C,DB: SelectQuery.First (correct)
    C->>SQ: First(ctx, dest)
    SQ->>SQ: clone().Limit(1)
    SQ->>DB: SELECT ... LIMIT 1
    DB-->>SQ: ≤1 row
    SQ-->>C: first row / ErrNoRows

    Note over C,DB: PreparedSelectQuery.First (missing LIMIT 1)
    C->>PSQ: First(ctx, args, dest)
    PSQ->>PSQ: Scan(ctx, args, dest) [no LIMIT 1]
    PSQ->>DB: SELECT ... (no LIMIT 1, compiled at Prepare time)
    DB-->>PSQ: ALL matching rows
    PSQ-->>C: first row consumed, rest discarded
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
pkg/rain/prepared_select.go:90-94
**`PreparedSelectQuery.First` does not apply LIMIT 1**

`SelectQuery.First` applies `LIMIT 1` at the database level via `q.clone().Limit(1).Scan(...)`, so only one row is ever transferred from the server. `PreparedSelectQuery.First` simply delegates to `p.Scan`, which executes the pre-compiled statement with no `LIMIT 1` in its SQL — the database returns all matching rows, and only the first is consumed by the struct scanner. A prepared query with 100k matching rows will transfer all of them on every `First()` call, silently.

Because the SQL is frozen at `Prepare()` time, the only sound options are: (a) remove `First` from `PreparedSelectQuery` and require callers to build the query with `.Limit(1)` before calling `.Prepare()`, or (b) clearly document that no `LIMIT 1` is applied and that callers must add it themselves when constructing the original `SelectQuery`.

### Issue 2 of 2
pkg/rain/relation_loading.go:298-314
**Duplicated target-column safety check**

The exact same block that checks `config.Columns` and auto-appends `relation.TargetColumn` if missing appears verbatim in both `loadRelatedManyToManyRows` (lines 298–311) and `loadRelatedRows` (lines 393–406). Extracting this into a small helper function (e.g. `ensureTargetColumnSelected(query *SelectQuery, columns []schema.Expression, targetCol *schema.ColumnDef)`) would eliminate the duplication and make future changes (e.g. adding a similar guard for `SourceColumn` on `BelongsTo` relations) a single-line call in each load function.

Reviews (1): Last reviewed commit: "feat(rain): implement First, TxOptions, ..." | Re-trigger Greptile

Comment thread pkg/rain/prepared_select.go Outdated
Comment on lines +90 to +94
// First executes the prepared SELECT query and scans the first row into dest.
// Returns sql.ErrNoRows if no result is found.
func (p *PreparedSelectQuery) First(ctx context.Context, args PreparedArgs, dest any) error {
return p.Scan(ctx, args, dest)
}

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.

P1 PreparedSelectQuery.First does not apply LIMIT 1

SelectQuery.First applies LIMIT 1 at the database level via q.clone().Limit(1).Scan(...), so only one row is ever transferred from the server. PreparedSelectQuery.First simply delegates to p.Scan, which executes the pre-compiled statement with no LIMIT 1 in its SQL — the database returns all matching rows, and only the first is consumed by the struct scanner. A prepared query with 100k matching rows will transfer all of them on every First() call, silently.

Because the SQL is frozen at Prepare() time, the only sound options are: (a) remove First from PreparedSelectQuery and require callers to build the query with .Limit(1) before calling .Prepare(), or (b) clearly document that no LIMIT 1 is applied and that callers must add it themselves when constructing the original SelectQuery.

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/rain/prepared_select.go
Line: 90-94

Comment:
**`PreparedSelectQuery.First` does not apply LIMIT 1**

`SelectQuery.First` applies `LIMIT 1` at the database level via `q.clone().Limit(1).Scan(...)`, so only one row is ever transferred from the server. `PreparedSelectQuery.First` simply delegates to `p.Scan`, which executes the pre-compiled statement with no `LIMIT 1` in its SQL — the database returns all matching rows, and only the first is consumed by the struct scanner. A prepared query with 100k matching rows will transfer all of them on every `First()` call, silently.

Because the SQL is frozen at `Prepare()` time, the only sound options are: (a) remove `First` from `PreparedSelectQuery` and require callers to build the query with `.Limit(1)` before calling `.Prepare()`, or (b) clearly document that no `LIMIT 1` is applied and that callers must add it themselves when constructing the original `SelectQuery`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment thread pkg/rain/relation_loading.go Outdated
Comment on lines 298 to 314
if len(config.Columns) > 0 {
targetQuery.Column(config.Columns...)
// Ensure mapping target column is selected.
found := false
for _, col := range config.Columns {
if cr, ok := col.(schema.ColumnReference); ok && cr.ColumnDef() == relation.TargetColumn {
found = true
break
}
}
if !found {
targetQuery.Column(schema.Ref(relation.TargetColumn))
}
}
if config.Where != nil {
targetQuery.Where(config.Where)
}

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.

P2 Duplicated target-column safety check

The exact same block that checks config.Columns and auto-appends relation.TargetColumn if missing appears verbatim in both loadRelatedManyToManyRows (lines 298–311) and loadRelatedRows (lines 393–406). Extracting this into a small helper function (e.g. ensureTargetColumnSelected(query *SelectQuery, columns []schema.Expression, targetCol *schema.ColumnDef)) would eliminate the duplication and make future changes (e.g. adding a similar guard for SourceColumn on BelongsTo relations) a single-line call in each load function.

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/rain/relation_loading.go
Line: 298-314

Comment:
**Duplicated target-column safety check**

The exact same block that checks `config.Columns` and auto-appends `relation.TargetColumn` if missing appears verbatim in both `loadRelatedManyToManyRows` (lines 298–311) and `loadRelatedRows` (lines 393–406). Extracting this into a small helper function (e.g. `ensureTargetColumnSelected(query *SelectQuery, columns []schema.Expression, targetCol *schema.ColumnDef)`) would eliminate the duplication and make future changes (e.g. adding a similar guard for `SourceColumn` on `BelongsTo` relations) a single-line call in each load function.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

@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/prepared_select.go:90-94
PreparedSelectQuery.First does not apply LIMIT 1

SelectQuery.First applies LIMIT 1 at the database level via q.clone().Limit(1).Scan(...), so only one row is ever transferred from the server. PreparedSelectQuery.First simply delegates to p.Scan, which executes the pre-compiled statement with no LIMIT 1 in its SQL — the database returns all matching rows, and only the first is consumed by the struct scanner. A prepared query with 100k matching rows will transfer all of them on every First() call, silently.

Because the SQL is frozen at Prepare() time, the only sound options are: (a) remove First from PreparedSelectQuery and require callers to build the query with .Limit(1) before calling .Prepare(), or (b) clearly document that no LIMIT 1 is applied and that callers must add it themselves when constructing the original SelectQuery.

Issue 2 of 2

pkg/rain/relation_loading.go:298-314
Duplicated target-column safety check

The exact same block that checks config.Columns and auto-appends relation.TargetColumn if missing appears verbatim in both loadRelatedManyToManyRows (lines 298–311) and loadRelatedRows (lines 393–406). Extracting this into a small helper function (e.g. ensureTargetColumnSelected(query *SelectQuery, columns []schema.Expression, targetCol *schema.ColumnDef)) would eliminate the duplication and make future changes (e.g. adding a similar guard for SourceColumn on BelongsTo relations) a single-line call in each load function.

@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/prepared_select.go:90-94
PreparedSelectQuery.First does not apply LIMIT 1

SelectQuery.First applies LIMIT 1 at the database level via q.clone().Limit(1).Scan(...), so only one row is ever transferred from the server. PreparedSelectQuery.First simply delegates to p.Scan, which executes the pre-compiled statement with no LIMIT 1 in its SQL — the database returns all matching rows, and only the first is consumed by the struct scanner. A prepared query with 100k matching rows will transfer all of them on every First() call, silently.

Because the SQL is frozen at Prepare() time, the only sound options are: (a) remove First from PreparedSelectQuery and require callers to build the query with .Limit(1) before calling .Prepare(), or (b) clearly document that no LIMIT 1 is applied and that callers must add it themselves when constructing the original SelectQuery.

Issue 2 of 2

pkg/rain/relation_loading.go:298-314
Duplicated target-column safety check

The exact same block that checks config.Columns and auto-appends relation.TargetColumn if missing appears verbatim in both loadRelatedManyToManyRows (lines 298–311) and loadRelatedRows (lines 393–406). Extracting this into a small helper function (e.g. ensureTargetColumnSelected(query *SelectQuery, columns []schema.Expression, targetCol *schema.ColumnDef)) would eliminate the duplication and make future changes (e.g. adding a similar guard for SourceColumn on BelongsTo relations) a single-line call in each load function.

Thank you for the feedback! I've made the following changes:

  1. Removed First from PreparedSelectQuery to avoid misleading performance expectations, as it could not correctly apply LIMIT 1 to the pre-compiled SQL. Callers should include .Limit(1) in the query builder before calling .Prepare().
  2. Extracted the duplicated target-column safety check into a new ensureTargetColumnSelected helper function in relation_loading.go and updated both loadRelatedManyToManyRows and loadRelatedRows to use it.

- Add .First() to SelectQuery for ergonomic single-row fetching with implicit LIMIT 1.
- Add BeginTx and RunInTxOpts to DB for sql.TxOptions support.
- Add Columns selection to RelationConfig for restricted relational fetching.
- Refactor relation loading to use ensureTargetColumnSelected helper.

Co-authored-by: cungminh2710 <8063319+cungminh2710@users.noreply.github.com>
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