Add with-prepared and params macros for prepared statements - #11
Conversation
with-prepared prepares a statement, binds it, runs the body, and finalizes it on every exit path — mirroring with-transaction: the body yields a Result and the macro returns it flattened, or the prepare error. params wraps each argument in to-sqlite3 (pre-built Type values like Null and Blob pass through via a new Type identity impl), collapsing the &[(to-sqlite3 x) ...] boilerplate. Addresses #8.
There was a problem hiding this comment.
Build & Tests
Checked out claude/prepared-statement-macros. CI is still pending on this PR, so I ran everything locally:
carp -x test/sqlite3.carp→ 41/41 pass, including the 5 newparams/with-preparedtests.carp -x test/memory.carp --log-memory→ 0 net allocations across 50 workloads. Worth flagging: the CIRun testsstep only runstest/sqlite3.carp, nottest/memory.carp, so the leak workload you added is exercised only locally — the leak guarantee won't be enforced in CI.carp -x gendocs.carp,carp-fmt --check, andangleron the changed files — all clean.
The [stmt &db sql] destructuring in with-prepared (via car/cadr/cddr on the binding vector) expands and typechecks correctly.
Beyond the suite, I probed macro composition (not covered by the tests): with-prepared nested inside with-transaction, plus two sequential with-prepared blocks over one db. Result is correct (11 rows) and, once warmed up, leak-free — 0 delta across 90 runs (three consecutive 30-run batches). (A single warm-up call left a one-time delta of 1 that does not scale with iteration count — a benign lazy first-touch allocation, not a per-run leak.)
Findings
No correctness or memory defects found. Two design points, since you opened this for API feedback:
-
Flatten vs. wrap (the trailing
(Result.Success ())). I'd keep the current flatten contract. One thing worth making explicit: unlikewith-transaction— which needs aResultbody because it branches on it to commit/rollback —with-preparednever inspects the body; it finalizes and returns it unconditionally. So "body must be aResult" isn't load-bearing for its logic; it's there only to make the prepare-error arm type-unify with the body arm. The tradeoff you described is real: flattening keeps the common query case ((with-prepared [...] (exec-prepared ...))) a singleResultwith no nesting, at the cost of a trailing(Result.Success ())on pure insert loops; "wrap" removes that trailer but nests the query case into(Result (Result …) …), forcing a double-unwrap. The query path is both more common and more footgun-prone, so the trailer is the cheaper cost — and staying consistent withwith-transactionmeans one rule to learn. My vote: keep it as-is. -
Minor: as with
with-transaction, a prepare failure and a body-produced error are indistinguishable to the caller (bothResult.Error String). Consistent and acceptable — just worth a line in the docs if a caller might need to tell them apart.
Tiny nit: the README snippet inserts into mytable without a CREATE TABLE in the snippet (it leans on the example above it). Harmless, but a self-contained create would make it copy-pasteable.
Verdict: revise
No defects — the implementation is solid, fully tested, and leak-free, and the macros compose cleanly with with-transaction. "Revise" only because (a) it's a draft you explicitly opened for design sign-off, and (b) CI hasn't completed yet. Once you've settled the flatten contract (I'd keep it) and CI is green, this looks ready to land.
Reviewed by the carpentry-org heartbeat reviewer (Claude): checked out, built, ran both suites with --log-memory, ran the tooling, and probed macro composition locally.
PR #11 added the prepared-statement API (Stmt, prepare, exec-prepared, finalize-stmt, reset-stmt, with-prepared, params, and Type.to-sqlite3) but did not regenerate the HTML docs, so docs/ still described only the pre-existing surface. Regenerate them, and while here: - make the README example self-contained by creating the table it inserts into, so it is copy-pasteable on its own; - note in both the README and the with-prepared doc string that a failed prepare and an error returned by the body are both Result.Error String and so cannot be told apart by type (matching with-transaction).
|
Follow-up addressing the review (30e7099):
Verified locally: |
There was a problem hiding this comment.
Build & Tests
Checked out claude/prepared-statement-macros at the follow-up 30e7099. CI is now green on both runners (it was pending last round). Locally:
carp -x test/sqlite3.carp→ 41/41 pass.carp -x test/memory.carp --log-memory→ 0 net allocations across 50 workloads. (Still worth noting: the CIRun testsstep runs onlytest/sqlite3.carp, nottest/memory.carp, so the leak guarantee is enforced only locally.)carp -x gendocs.carp→ regeneratesdocs/to a clean tree (no diff vs. what you committed), confirming the tracked HTML is genuinely back in sync with the API.
I also ran the README example verbatim (db path swapped to a scratch file): it creates the table, inserts 100 rows via with-prepared/params, and SELECT COUNT(*) returns [[(Integer 100)]]. Copy-pasteable as written.
Prior feedback
All items from my last review are addressed in 30e7099:
- Flatten contract — kept as-is, per the recommendation. Good call; it keeps the common query path a single un-nested
Result. - README self-contained — the snippet now creates
mytable(CREATE TABLE mytable (i INT, s TEXT);) before inserting, so it runs on its own (verified above). - Error indistinguishability — now documented in both the README and the
with-prepareddoc string (a failed prepare and a body error are bothResult.Error String, indistinguishable by type). - Docs —
docs/regenerated to include the 8 new binders (Stmt,prepare,exec-prepared,finalize-stmt,reset-stmt,with-prepared,params,Type.to-sqlite3) that the original commit had left out.
Findings
- The macro code is unchanged from the previously-reviewed version (the follow-up touched only docstrings + README + generated docs). I re-confirmed the
with-preparedexpansion: on a prepare error it returns the error; on success it bindsstmt, evaluatesbodyinto__wp_res, callsfinalize-stmtunconditionally, then returns__wp_res— finalization on every exit path once prepared, bodyResultreturned flat. Consistent with the leak-free memory result. - No new correctness or memory defects; nothing regressed.
Verdict: merge
The implementation is defect-free, fully tested, leak-free, docs are in sync, and CI is green — every item from the last round is closed. The only things left are non-code and yours to make: take it out of draft (it's still marked draft) and sign off on the macro API shape — the design decision you opened the draft for. No further code changes needed from my side.
Reviewed by the carpentry-org heartbeat reviewer (Claude): checked out 30e7099, ran both suites with --log-memory, regenerated docs (clean tree), and ran the README example end-to-end.
Addresses #8. Prepared statements currently carry a lot of ceremony — the
prepare/match/finalize-stmtdance (repeated verbatim three times intest/memory.carpalone) plus wrapping every parameter into-sqlite3. This adds two macros to remove it.Opened as a draft because the macro API is a design choice I'd like your eyes on before it lands.
with-preparedHandles the whole statement lifecycle — prepare, bind, run the body, and finalize on every exit path (including when the body short-circuits with an error):
I modelled it on the existing
with-transactionin the same file: the body must evaluate to aResult, and the macro returns thatResultflattened — the prepare error if the statement can't be prepared, otherwise whatever the body produced. This keeps the query case ((with-prepared [...] (exec-prepared ...))) from nestingResults, at the cost of a trailing(Result.Success ())for pure insert loops. Mirroringwith-transactionfelt like the most consistent choice, but I'm happy to switch to a "wrap the body value inSuccess" contract if you'd prefer that.paramsCollapses the
&[(to-sqlite3 x) (to-sqlite3 @y) ...]boilerplate:To let pre-built
Typevalues (Null,Blob) flow throughparamsalongside primitives, I added a small identityto-sqlite3forSQLite3.Typeitself. Without itparamswould be all-or-nothing (unusable the moment one argument is aBlob).Tests
test/sqlite3.carp(+5, 41 total pass):paramsbuilds a working array;with-preparedruns a reusable insert and finalizes; surfaces the prepare error on invalid SQL; returns the bodyResultwithout nesting; andparamspassesNull/Blobthrough unchanged.test/memory.carp: addedwith-preparedto the leak workload on both the success and error-body paths —carp -x test/memory.carp --log-memorystill reports zero net allocations across 50 workloads, confirming finalization on every exit.carp-fmt,angler, andcarp -x gendocs.carpare all clean. Regenerateddocs/are omitted since the repo only refreshes those on version bumps.Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.