Skip to content

⚡ Bolt: Implement prepared statements for Insert, Update, and Delete#96

Merged
cungminh2710 merged 2 commits into
mainfrom
feat/prepared-iud-statements-5865195085006225899
May 31, 2026
Merged

⚡ Bolt: Implement prepared statements for Insert, Update, and Delete#96
cungminh2710 merged 2 commits into
mainfrom
feat/prepared-iud-statements-5865195085006225899

Conversation

@cungminh2710
Copy link
Copy Markdown
Contributor

This PR implements support for prepared statements in INSERT, UPDATE, and DELETE queries, mirroring the existing support for SELECT queries.

Key Changes:

  • Refactoring: Extracted core SQL generation logic in InsertQuery, UpdateQuery, and DeleteQuery into unexported compile() and writeSQL() methods. This aligns them with the architectural pattern used by SelectQuery.
  • Prepared Queries: Added a Prepare(ctx context.Context) method to each IUD builder.
  • New Types: Introduced PreparedInsertQuery, PreparedUpdateQuery, and PreparedDeleteQuery in a new file pkg/rain/prepared_exec.go.
  • Execution Support: Each prepared type supports Exec(ctx, args) for standard execution and Scan(ctx, args, dest) for queries with a RETURNING clause.
  • Error Handling: Fixed a bug identified during review where errors from rows.Close() were potentially swallowed in Scan methods by adopting named return parameters.
  • Testing: Added comprehensive unit tests for compilation logic and integration tests in the SQLite suite to verify end-to-end execution with named placeholders.

This implementation provides a consistent, type-safe, and performant way to reuse modification queries with different arguments.


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

- Refactored InsertQuery, UpdateQuery, and DeleteQuery builders to separate compilation from execution.
- Added Prepare() method to IUD builders returning specialized prepared query types.
- Implemented PreparedInsertQuery, PreparedUpdateQuery, and PreparedDeleteQuery in pkg/rain/prepared_exec.go.
- Added support for Exec() and Scan() (RETURNING) on prepared IUD queries.
- Added unit tests for compilation and integration tests for execution.

This change brings Rain ORM closer to Drizzle feature parity by providing a consistent API for prepared statements across all core query types.

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
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 31, 2026

Greptile Summary

This PR extends prepared statement support from SelectQuery to InsertQuery, UpdateQuery, and DeleteQuery by extracting SQL generation into internal compile() and writeSQL() methods and adding a Prepare(ctx) method to each builder. Three new types — PreparedInsertQuery, PreparedUpdateQuery, and PreparedDeleteQuery — are introduced in prepared_exec.go, each offering Exec, Scan, and Close methods with sync.Once-guarded teardown.

  • Refactoring (query_insert.go, query_update.go, query_delete.go): ToSQL() now delegates to an internal compile(), keeping observable behaviour identical while enabling Prepare() to reuse the same compilation path.
  • New prepared types (prepared_exec.go): PreparedInsertQuery, PreparedUpdateQuery, and PreparedDeleteQuery mirror the existing PreparedSelectQuery structure; closeErr is written inside sync.Once.Do and read after, which is safe due to the happens-before guarantee provided by sync.Once.
  • Testing (prepared_exec_internal_test.go, prepared_exec_test.go, sqlite_integration_test.go): Compilation unit tests verify SQL output and placeholder detection; an integration test covers the full Exec and Scan lifecycle on SQLite.

Confidence Score: 5/5

Safe to merge; the refactoring preserves all existing validation logic and the new prepared-exec path follows the established SelectQuery pattern faithfully.

The SQL generation refactoring is a straightforward extraction with no observable behaviour changes — all table-nil, view, unbounded, and empty-assignment guards remain in place. The new PreparedXxx types follow the same structure as the existing PreparedSelectQuery. The one structural gap (missing RETURNING guard in Scan) was identified and discussed in a prior review thread.

pkg/rain/prepared_exec.go — Scan methods on the three prepared types; the RETURNING guard present on the non-prepared counterparts is absent here.

Important Files Changed

Filename Overview
pkg/rain/prepared_exec.go New file introducing PreparedInsertQuery, PreparedUpdateQuery, PreparedDeleteQuery with Exec, Scan, and Close methods; Scan methods lack the RETURNING guard present on the non-prepared counterparts (already flagged in previous thread)
pkg/rain/query_insert.go Refactored ToSQL into compile()/writeValuesSQL()/writeSelectSQL(); Prepare() method added; logic is semantically equivalent to the original with no regressions observed
pkg/rain/query_update.go Refactored ToSQL into compile()/writeSQL(); Prepare() method added; equivalent semantics to original code
pkg/rain/query_delete.go Refactored ToSQL into compile()/writeSQL(); Prepare() method added; equivalent semantics to original code
pkg/rain/sqlite_integration_test.go Adds TestSQLiteIntegrationPreparedExecQueries covering end-to-end Exec and Scan paths for Insert, Update, and Delete prepared queries

Sequence Diagram

sequenceDiagram
    participant User
    participant Builder as InsertQuery / UpdateQuery / DeleteQuery
    participant compile as compile()
    participant Runner as preparingQueryRunner
    participant Prepared as PreparedXxxQuery
    participant DB as database/sql

    User->>Builder: .Set(...).Where(...).Prepare(ctx)
    Builder->>compile: compile()
    compile-->>Builder: compiledQuery (sql, argPlan, hasNames)
    Builder->>Runner: prepareContext(ctx, sql)
    Runner->>DB: db.PrepareContext(ctx, sql)
    DB-->>Runner: "*sql.Stmt"
    Runner-->>Builder: "*sql.Stmt"
    Builder-->>User: "*PreparedXxxQuery"

    User->>Prepared: "Exec(ctx, PreparedArgs{...})"
    Prepared->>Prepared: compiled.bind(args) → []any
    Prepared->>DB: stmt.ExecContext(ctx, bound...)
    DB-->>User: sql.Result, error

    User->>Prepared: "Scan(ctx, PreparedArgs{...}, &dest)"
    Prepared->>Prepared: compiled.bind(args) → []any
    Prepared->>DB: stmt.QueryContext(ctx, bound...)
    DB-->>Prepared: "*sql.Rows"
    Prepared->>Prepared: scanRowsAgainstTable(rows, dest, table)
    Prepared-->>User: error

    User->>Prepared: Close()
    Prepared->>Prepared: closeOnce.Do → stmt.Close()
    Prepared-->>User: error
Loading

Reviews (2): Last reviewed commit: "feat(rain): implement prepared statement..." | Re-trigger Greptile

Comment thread pkg/rain/prepared_exec.go
Comment on lines +31 to +44
func (p *PreparedInsertQuery) Scan(ctx context.Context, args PreparedArgs, dest any) (err error) {
bound, err := p.compiled.bind(args)
if err != nil {
return err
}

rows, err := p.stmt.QueryContext(ctx, bound...)
if err != nil {
return err
}
defer closeRows(rows, &err)

return scanRowsAgainstTable(rows, dest, p.table)
}
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 Missing RETURNING guard in prepared Scan methods

The non-prepared InsertQuery.Scan(), UpdateQuery.Scan(), and DeleteQuery.Scan() all guard against being called without a RETURNING clause by checking len(q.returning) == 0 and returning a clear "rain: insert scan requires RETURNING" error. The three prepared Scan methods have no such check. If a user prepares an INSERT/UPDATE/DELETE without .Returning(...) and then calls Scan(), the statement executes but the database returns no columns; scanRowsAgainstTable then fails with a confusing low-level error (e.g., a scan plan mismatch) rather than the clear sentinel the caller would expect.

Fix: add a hasReturning bool field to each prepared type, set it to len(q.returning) > 0 in the respective Prepare() method, and check it at the top of each Scan(). The same gap exists on lines 76–89 (PreparedUpdateQuery.Scan) and lines 121–134 (PreparedDeleteQuery.Scan).

Prompt To Fix With AI
This is a comment left during a code review.
Path: pkg/rain/prepared_exec.go
Line: 31-44

Comment:
**Missing RETURNING guard in prepared Scan methods**

The non-prepared `InsertQuery.Scan()`, `UpdateQuery.Scan()`, and `DeleteQuery.Scan()` all guard against being called without a `RETURNING` clause by checking `len(q.returning) == 0` and returning a clear `"rain: insert scan requires RETURNING"` error. The three prepared `Scan` methods have no such check. If a user prepares an `INSERT`/`UPDATE`/`DELETE` without `.Returning(...)` and then calls `Scan()`, the statement executes but the database returns no columns; `scanRowsAgainstTable` then fails with a confusing low-level error (e.g., a scan plan mismatch) rather than the clear sentinel the caller would expect.

Fix: add a `hasReturning bool` field to each prepared type, set it to `len(q.returning) > 0` in the respective `Prepare()` method, and check it at the top of each `Scan()`. The same gap exists on lines 76–89 (`PreparedUpdateQuery.Scan`) and lines 121–134 (`PreparedDeleteQuery.Scan`).

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

Fix in Codex

- Refactored InsertQuery, UpdateQuery, and DeleteQuery builders to separate compilation from execution.
- Added Prepare() method to IUD builders returning specialized prepared query types.
- Implemented PreparedInsertQuery, PreparedUpdateQuery, and PreparedDeleteQuery in pkg/rain/prepared_exec.go.
- Added support for Exec() and Scan() (RETURNING) on prepared IUD queries.
- Used named return parameters in Scan() to ensure proper defer error handling.
- Added unit tests for compilation and integration tests for execution.

This change brings Rain ORM closer to Drizzle feature parity by providing a consistent API for prepared statements across all core query types.

Co-authored-by: cungminh2710 <8063319+cungminh2710@users.noreply.github.com>
@cungminh2710 cungminh2710 merged commit 175b065 into main May 31, 2026
5 checks passed
@cungminh2710 cungminh2710 deleted the feat/prepared-iud-statements-5865195085006225899 branch May 31, 2026 23:48
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