Skip to content

spike: 014 embedding-API kernel — Send+Sync handle over a streaming VDBE #682

Description

@dpsiderius

Context

Spec 013 (Embedding API, PR #678, Status: Proposed) specifies a public
Connection/Statement/Transaction facade. Two of its seven
requirements are architectural rather than ergonomic, and a wrong choice
in either forces a rewrite of the facade built on top:

  • Req 4 — a Send + Sync handle over a connection-owned worker
    thread, because Rc<dyn PageSource>/Rc<RefCell<Pager>> are !Send
    (23 sites in src/).
  • Req 7 — incremental row access, because every entry point today
    (execute_with_db, execute_with_db_and_params,
    execute_transaction_step) returns a fully materialized
    Vec<Vec<Value>>.

These interact: streaming across a thread boundary means the statement
handle holds cursor state on the worker while the caller holds a Send
handle. That interaction is the actual risk, and neither requirement's
cost is known.

A static read of the tree already narrows it. Vm is public
(Vm::with_db, bind_params, rows), but the step loop is not:

  • src/vdbe/exec.rs:704fn dispatch(...) is private
  • src/vdbe/exec.rsfn run(...) is private
  • src/vdbe/exec.rs:619ResultRow calls emit_row, which pushes
    into a Vec inside the Vm; Vm::rows() hands back the
    accumulated slice

So Req 7 is not implementable as an external layer over today's
public API, and Req 4 probably is. This spike measures both instead of
assuming either.

Hypothesis

A Send + Sync connection handle over a worker-thread-owned Rc engine
graph can serve incremental per-row results and a rows-affected
count, without modifying the engine's Rc/RefCell ownership or the
VDBE opcode-dispatch loop.

The static read above predicts the hypothesis fails on its second
clause
— streaming will require an additive engine change. The spike's
job is to find the minimal such change, prove it is additive (no
behaviour change for existing callers), and measure what streaming costs
or saves.

Design

Three prototypes over one fixture, mirroring spike 013's
three-implementation shape:

  1. batch — control. Today's public API
    (execute_with_db_and_params) on the caller's thread. No engine
    change.
  2. worker_batch — connection owns a thread; commands cross a
    channel; the whole result set returns per statement. Isolates Req 4.
    Expected to need no engine change.
  3. worker_stream — same worker, rows yielded one at a time, cursor
    state living on the worker. Exercises Reqs 4 and 7 together. Requires
    the minimal engine change, which is part of the deliverable.

Correctness gates timing: all three prototypes must return byte-identical
rows for the same query before any measurement is trusted (spike 013's
all_three_comparators_agree_on_every_pair precedent).

Success Criteria

  1. Send + Sync on the handle proven at compile time (static assertion),
    not asserted in prose.
  2. All three prototypes agree on every row of every fixture query.
  3. Per-row latency and total throughput measured for all three.
  4. Peak memory measured for a large result set — Req 7's "without
    materializing" is a claim that needs a number.
  5. Drop-mid-iteration releases the worker's cursors (Req 7's second
    scenario) and a dead engine surfaces as an error, not a hang.
  6. The minimal src/ diff for streaming is stated as a diff, with a
    verdict on whether it is additive for existing callers.
  7. A go/no-go on spec 013's Req 4 + Req 7 as written, including any
    correction the spec needs before it merges.

Spike Output

  • tests/spike/014_embedding_api/ — standalone crate, own [workspace],
    path dep on the parent, Makefile (test/bench), spike-014 target
    in the root Makefile.
  • README.md — Hypothesis / Setup / Design / Results / Finding /
    Conclusion, with an explicit ADR-proposed-or-not verdict.
  • Any src/ patch stays on the spike branch, never merged; the
    shippable change lands later via its own ticket.

Complexity

Estimate: medium
Reasoning: Three prototypes, but only one is novel — batch is a
copy of examples/query.rs, and worker_batch is a standard
channel-plus-thread wrapper. The cost sits in worker_stream: finding
the minimal step/yield primitive in src/vdbe/exec.rs without
disturbing the dispatch loop, plus a criterion harness and a memory
measurement. Bounded by being throwaway code that ships nothing.

Context

Feeds PR #678's review — the spec is still Status: Proposed, so a
correction found here is cheaper than one found after merge. Does not
block, and is not blocked by, the spec-010/Req-8 autoindex corruption
bug, which is independent.

Refs: 013/Req-4, 013/Req-7, 010/Req-8 (independent), #678, #621

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    spikeExploratory/experimental spike work

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions