Skip to content

feat(bench): add ORM showdown benchmark harness#29

Merged
cungminh2710 merged 3 commits into
mainfrom
codex/linear-mention-qc-236-featbench-add-orm-showdown-benchm
Mar 29, 2026
Merged

feat(bench): add ORM showdown benchmark harness#29
cungminh2710 merged 3 commits into
mainfrom
codex/linear-mention-qc-236-featbench-add-orm-showdown-benchm

Conversation

@cungminh2710
Copy link
Copy Markdown
Contributor

Motivation

  • Provide a dedicated, fair, and benchstat-friendly benchmark harness to compare Rain ORM against Bun, GORM, SQLC-style, Ent-style, and raw database/sql on identical schema, dataset sizes, and workloads.
  • Keep benchmark setup and seeding deterministic and outside timed loops so results are reproducible and directly comparable with benchstat.

Description

  • Add a new benchmark suite at benchmarks/ormshowdown/harness_test.go that defines a canonical schema (users, categories, posts), deterministic seeding, and a standardized set of workloads run for each adapter (insert single, lookup by PK, filtered slice scan, join scan, grouped aggregate, subquery/join report, eager-load equivalent, prepared lookup).
  • Implement adapters for six implementations: raw (database/sql), rain (native), bun, gorm, and placeholders for sqlc/ent implemented as nearest-equivalent typed layers to preserve SQL shape parity.
  • Make outputs benchstat-friendly by using stable Go benchmark names and documenting how to save raw outputs in benchmarks/ormshowdown/README.md (examples such as go test -run '^$' -bench . -benchmem ./benchmarks/ormshowdown/... > before.txt).
  • Add a convenience target bench-ormshowdown to the Makefile and add Bun/GORM (and related) dependencies to go.mod/go.sum so the harness builds and runs.

Testing

  • Ran formatting, linting, and full test suite with make fmt, make lint, and make test, all of which completed successfully.
  • Executed spot benchmark runs to validate harness flow and individual adapters, which succeeded: go test ./benchmarks/ormshowdown -run '^$' -bench BenchmarkORMShowdown/raw/small/lookup_by_pk -benchtime=1x and go test ./benchmarks/ormshowdown -run '^$' -bench BenchmarkORMShowdown/rain/small/prepared_point_lookup -benchtime=1x both passed, and go test ./benchmarks/ormshowdown -run '^$' -bench 'BenchmarkORMShowdown/(bun|gorm|sqlc|ent)/small/lookup_by_pk' -benchtime=1x produced successful benchmark runs for Bun, GORM, SQLC-style and Ent-style adapters.

Codex Task

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 29, 2026

Greptile Summary

This PR introduces a new benchmark harness at benchmarks/ormshowdown/ that compares Rain ORM against Bun, GORM, SQLC-style, Ent-style, and raw database/sql across a canonical SQLite schema and eight deterministic workloads. It also adds the necessary Bun and GORM dependencies to go.mod/go.sum, a bench-ormshowdown Makefile target, and an ADR documenting the fairness design decisions.

Key changes and observations:

  • The two concerns raised in previous rounds (prepared statement prepared per iteration, and Bun/GORM bypassing their ORM query builders) have both been addressed: prepared statements are now stored on the adapter struct and prepared once in open(), and all adapters use their idiomatic high-level APIs for all workloads.
  • One remaining fairness issue: gormAdapter declares result slices with var rows []... (no pre-allocated capacity) for every slice-returning workload (filteredSliceScan, joinScan, groupedAggregate, subqueryReport, eagerLoadNearestEquivalent), while bunAdapter and rainAdapter consistently use make([]..., 0, limit). Since b.ReportAllocs() is active, the extra growth allocations will inflate GORM's reported allocs/op figures relative to the other adapters.
  • sqlcAdapter and entAdapter are thin aliases over rawAdapter (intentional for v1, documented in the ADR and README).
  • Schema setup and seeding occur outside all timed loops; deterministic data and b.TempDir() isolation ensure reproducible, independent per-adapter databases.

Confidence Score: 4/5

Safe to merge after addressing the GORM pre-allocation inconsistency, which currently inflates GORM's allocs/op numbers across five workloads.

The two previously flagged P1 issues (prepared statement per iteration, and ORM builder bypass) are both resolved. One new P1 remains: GORM's missing slice pre-allocation will systematically overstate its allocation cost relative to Bun and Rain in all slice-returning workloads. The fix is a one-line change per affected method.

benchmarks/ormshowdown/harness_test.go — specifically the five GORM adapter methods that use var rows []... instead of make([]..., 0, limit).

Important Files Changed

Filename Overview
benchmarks/ormshowdown/harness_test.go Main benchmark harness – 749-line file implementing six ORM adapters and workloads; GORM adapter systematically omits pre-allocation of result slices, inflating its reported allocs/op versus Bun and Rain.
Makefile Adds a bench-ormshowdown convenience target; clean and consistent with existing bench targets.
benchmarks/ormshowdown/README.md Documents workloads, run instructions, and fairness notes; accurate and helpful.
docs/adr/2026-03-30-ormshowdown-fairness-design.md ADR explicitly documents the decision to use idiomatic ORM APIs for all adapters; rationale is sound.
go.mod Adds Bun, GORM, and transitive dependencies; versions are pinned appropriately.
go.sum Checksum entries added for all new dependencies; no anomalies.
pkg/rain/sqlite_integration_test.go Trivial whitespace-only change (blank line removal); no functional impact.

Sequence Diagram

sequenceDiagram
    participant F as testing.B (framework)
    participant B as BenchmarkORMShowdown
    participant S as setupCanonicalSchemaAndSeed
    participant A as ormAdapter (open)
    participant W as runWorkloads

    F->>B: run(ds x adapter)
    B->>S: create schema + seed data (outside timer)
    S-->>B: db ready at TempDir path
    B->>A: open(path) registers Cleanup(close)
    A-->>B: connection + preparedPointQuery ready
    B->>W: runWorkloads(b, ctx, ds, adapter)
    W->>F: b.Run(insert_single) [b.N loop]
    W->>F: b.Run(lookup_by_pk) [b.N loop]
    W->>F: b.Run(filtered_slice_scan) [b.N loop]
    W->>F: b.Run(join_scan_posts_users) [b.N loop]
    W->>F: b.Run(grouped_aggregate) [b.N loop]
    W->>F: b.Run(subquery_join_report) [b.N loop]
    W->>F: b.Run(eager_load_nearest_equivalent) [b.N loop]
    W->>F: b.Run(prepared_point_lookup) [b.N loop, reuses pre-prepared stmt]
    F-->>B: Cleanup: close adapter + db
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: benchmarks/ormshowdown/harness_test.go
Line: 756-758

Comment:
**GORM adapter missing pre-allocation inflates allocation counts**

`gormAdapter` consistently declares result slices with `var rows []...` (nil, no pre-allocated capacity) for every slice-returning workload, while `bunAdapter` and `rainAdapter` always use `make([]..., 0, limit)`. Because `b.ReportAllocs()` is active for all workloads, the extra growth allocations incurred when GORM appends into a nil slice will show up in the reported `allocs/op` numbers, systematically overstating GORM's allocation cost relative to Bun.

Affected workloads (all using `var rows []...` in the GORM adapter):
- `filteredSliceScan` (line 757) – `var rows []basicUser` vs `make([]basicUser, 0, limit)` in rain/bun
- `joinScan` (line 762) – `var rows []struct{...}` vs `make([]struct{...}, 0, limit)` in bun
- `groupedAggregate` (line 777) – `var rows []struct{...}` vs `make([]struct{...}, 0, 3)` in bun
- `subqueryReport` (line 789) – `var rows []struct{...}` vs `make([]struct{...}, 0, limit)` in bun
- `eagerLoadNearestEquivalent` (line 808) – `var rows []gormBenchUser` vs `make([]bunBenchUser, 0, limit)` in bun

Pre-allocating here is not an ORM-layer behavior that should be left to the library – it is caller-side setup that belongs outside the timed workload. Using a nil slice penalises GORM's allocation numbers without measuring anything meaningful about GORM itself.

Suggestion (applies to all five workloads; shown for `filteredSliceScan`):

```suggestion
	rows := make([]basicUser, 0, limit)
	return a.db.WithContext(ctx).Table("users").Where("active = ?", true).Order("id ASC").Limit(limit).Find(&rows).Error
```

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

Reviews (2): Last reviewed commit: "." | Re-trigger Greptile

Comment thread benchmarks/ormshowdown/harness_test.go
Comment thread benchmarks/ormshowdown/harness_test.go
@cungminh2710 cungminh2710 merged commit 889128e into main Mar 29, 2026
5 of 6 checks passed
@cungminh2710 cungminh2710 deleted the codex/linear-mention-qc-236-featbench-add-orm-showdown-benchm branch March 29, 2026 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant