Skip to content

Easy-wins sprint: unique constraints, $N parameter binding, indexed range scans, truthful EXPLAIN, multi-line REPL, agent-eval harness#85

Merged
zvndev merged 10 commits into
mainfrom
feat/easy-wins-sprint
Jun 10, 2026
Merged

Easy-wins sprint: unique constraints, $N parameter binding, indexed range scans, truthful EXPLAIN, multi-line REPL, agent-eval harness#85
zvndev merged 10 commits into
mainfrom
feat/easy-wins-sprint

Conversation

@zvndev

@zvndev zvndev commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Six features from docs/superpowers/plans/2026-06-09-easy-wins-sprint.md, each landed TDD-first as its own commit. Targets v0.4.7.

Feature What Commit
Truthful EXPLAIN EXPLAIN lowers speculative IndexScan/RangeScan against the catalog → shows the plan that actually runs. Also fixed a latent bug: the newly-reachable fused scan-update path was swallowing errors and bypassing the v0.4.6 oversized-row guard. afc6a8a
Indexed range scans >, >=, <, <=, between traverse the B+tree (new BTree::range_rids for non-unique composite keys) instead of full-scanning — ~7× faster on a selective range over 100K rows; NULLs excluded, exclusive bounds honored. 5e6a196
UNIQUE constraints unique field modifier + alter T add unique .col (scans for existing dups first). Auto-creates a unique index; enforced as a storage-layer pre-check across plain/prepared/upsert paths, before any heap write or WAL append; survives restart. e8aa0b2
$N parameter binding Token-level substitution (never string interpolation) → injection-shaped input is inert. New QueryWithParams (0x04) wire message (pure addition; old clients unaffected) + TS client.query(q, params?). 82a00ff, 4253f6b
Multi-line REPL CLI buffers lines until braces/parens balance outside string literals. de80b8f
Agent-eval harness scripts/agent-eval/ — offline, model-agnostic PowQL eval (10-table schema, 26 tasks) + SQLite baseline. Not in CI. 9ea0e65

⚠️ Breaking change

upsert <T> on .col now requires .col to be unique. This fixes a real bug: upsert ... on .id then a plain insert of the same id used to silently create duplicate rows. Declare the column unique or alter T add unique .col.

Test plan (integration pass, T7)

  • cargo test --workspace41 suites, 0 failures
  • cargo clippy --workspace --all-targets -- -D warnings clean; cargo fmt --all --check clean
  • TS client — 85 green (test 57 / protocol 15 / pool 13), incl. live param-binding integration
  • powdb-compare — no regression (PowDB 1.3–11.7× vs SQLite); baseline/main.json untouched
  • Each feature shipped with its own failing-test-first suite (parser, storage enforcement, restart durability, protocol round-trip, injection-byte-faithfulness, EXPLAIN shape, range correctness)

Notes

  • AGENTS.md updated for every surface (unique cheat-sheet rows + footgun, $N binding replacing the old 'escape it yourself' note, multi-line REPL, range-scan perf); POWQL.md + site/powql.html updated for unique.
  • Built across the sprint with one dev agent per task (T5/T6 in isolated worktrees, merged in); plan + per-task TDD in the plan doc.

🤖 Generated with Claude Code

zvndev and others added 10 commits June 9, 2026 21:55
… outside string literals

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ed tasks, model-agnostic offline runner

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ws the executed plan

EXPLAIN previously printed the planner's speculative IndexScan even when
no index existed and execution fell back to a filtered scan. The lowering
pass (renamed lower_unindexed_scans) now rewrites unindexed IndexScan to
Filter(SeqScan), same as it already did for RangeScan.

Lowering unindexed `.col = lit` updates now routes them through the fused
scan+update path, which was silently swallowing update_hinted errors with
.ok(); propagate them as StorageError so the oversized-row guard fires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…exes

BTree::range_rids walks composite (value,rid) keys between prefix bounds;
the executor fetches rows from the heap by rid and rechecks bounds (which
also preserves null-exclusion semantics). Plan lowering now keeps RangeScan
whenever ANY index exists on the column. Adds range_scan_indexed criterion
workload; docs updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ique, enforced on insert/update/upsert

Declaring unique auto-creates a unique B+tree index; enforcement is a
storage-layer pre-check before any heap write, so plain, prepared, and
upsert paths share one choke point. upsert on .col now requires .col to
be unique (fixes upsert-then-insert duplicate-id bug). alter T add unique
.col scans for existing duplicates first. Constraint survives restart via
persisted IndexedColMeta + WAL replay index rebuild.

Audit: every write that can touch an indexed column funnels through
Table::insert / Table::update_hinted; the byte-patch fast paths
(plan_exec.rs no_indexed guards at 895/967/2590/2651, prepared.rs:225)
and scan_patch_matching_with_hook are unreachable for indexed columns,
so the two pre-checks form a complete choke point. IndexedColMeta.unique
already round-trips through catalog v3 persist/open; rebuild_indexes_from_heap
(catalog.rs:450) restores it after WAL replay. No DropIndex statement
exists, so alter add unique on an already-indexed column is a clean error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n level, QueryWithParams wire message

Params are substituted as literal tokens before parsing (never re-lexed,
never string-interpolated), so untrusted input cannot change query shape.
$N placeholders are 1-based and bound via parser::parse_with_params; the
plain parse() path now rejects a bare placeholder with the standard
unexpected-token error. New MSG_QUERY_PARAMS (0x04) is a pure protocol
addition with a WireParam tag-per-value encoding; existing messages and
old clients are untouched, and dispatch_query_with_params routes through
the same role-enforcement + readonly escalation as dispatch_query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re message

query() gains an optional positional-params array (`$1`..`$N`), disambiguated
from the legacy `query(q, opts)` form via Array.isArray. Params are sent in
the new MSG_QUERY_PARAMS (0x04) frame with a tag-per-value encoding; the
server binds them at the token level so injection-shaped strings are inert.
Numbers bind as int when integral and float otherwise, bigint always int,
null binds PowQL null. Adds protocol round-trip + unknown-tag-rejection
tests and live-server integration tests (byte-faithful injection string,
null/bool/int/float binding, old no-params path). README documents the new
params form and the >= 0.4.7 server requirement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ures

Documents the [Unreleased] feature set: parameter binding ($N), unique
constraints (incl. the breaking upsert-requires-unique change), indexed
range scans, truthful EXPLAIN, multi-line REPL, and the agent-eval harness;
plus the oversized-row-guard regression fix from the EXPLAIN lowering work.

Gate: cargo test --workspace 41 suites green, clippy -D warnings clean,
fmt clean; TS client 85 tests green (test+protocol+pool); powdb-compare
shows no regression (PowDB 1.3-11.7x vs SQLite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zvndev zvndev merged commit 8fc73fb into main Jun 10, 2026
10 checks passed
@zvndev zvndev deleted the feat/easy-wins-sprint branch June 10, 2026 03:56
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