You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
handlers.StructuredBatch with pipeline.state.path set fails after its first batch, on every version since v1.1.0. The pipeline exits.
What happens
ERROR error reinitializing handler {"error": "checkpoint after truncate: Invalid Argument: TransactionContext Error: Cannot CHECKPOINT: the current transaction has transaction local changes"}
ERROR failed to consume loop
Reproduced 2026-09-13 on v1.2.0 with a websocket source and the Bluesky demo's StructuredBatch handler, plus state: {path: /tmp/x.duckdb}. The process ran one batch and died about ten seconds after start. The same config without the state block runs for hours. The same config on handlers.InferredMemBatch with the state block runs for hours.
Why
#247 (v1.1.0) made StructuredBatch.Init run TRUNCATE TABLE <table>; CHECKPOINT; before every batch, so an in-memory database reclaims the truncated rows. That fix is correct without durable state.
With a state path, run disables autocommit on the connection (internal/cli/run/root.go, "Every batch from here on is one transaction"). Every statement then runs inside a transaction that closes only at commitState. Init runs after the commit, so its TRUNCATE opens a fresh transaction and deletes the previous batch's rows, and the CHECKPOINT that follows is inside a transaction with local changes. DuckDB refuses that.
The first Init, at startup, passes because the table is empty: a truncate that deletes nothing leaves no local changes.
Nothing in the test suite combines StructuredBatch with WithStateStore or a state path. No shipped example does either: kafka.stateful.window.yml and benchmark.stateful.mem.yml both use InferredMemBatch, and bluesky.postgres.windowed.yml uses StructuredBatch without a state block. The README's durable state section does not say the handler matters.
Options
Checkpoint before the truncate.CHECKPOINT; TRUNCATE TABLE <table>; The checkpoint is then the first statement of a fresh transaction, with no local changes, and it reclaims the rows the previous batch's truncate left. The in-memory reclaim is one batch later than today, still bounded by one batch.
Skip the checkpoint under a state transaction. A file-backed database checkpoints on its own as the WAL grows, so the truncated rows are reclaimed without the per-batch statement. Whether it reclaims them promptly is what the on-disk soak in validate: introspect sink destinations and lint the schema for DuckDB footguns #268 is measuring.
Move the checkpoint into commitState, after the commit and before the handler's Init. That keeps the truncate-then-checkpoint order, but it moves handler knowledge into the pipeline.
Option 1 is the smallest and does not need the pipeline to know which handler it runs.
Done when
A test in internal/handlers runs StructuredBatch on a file-backed DuckDB with autocommit disabled: Init, Write, Invoke, commit, Init, and asserts the second Init succeeds.
The invariant matrix marks handler.structured against state.durability, so the combination cannot go untested again.
The README's durable state section names the handlers it has been verified with.
handlers.StructuredBatchwithpipeline.state.pathset fails after its first batch, on every version since v1.1.0. The pipeline exits.What happens
Reproduced 2026-09-13 on v1.2.0 with a websocket source and the Bluesky demo's
StructuredBatchhandler, plusstate: {path: /tmp/x.duckdb}. The process ran one batch and died about ten seconds after start. The same config without the state block runs for hours. The same config onhandlers.InferredMemBatchwith the state block runs for hours.Why
#247 (v1.1.0) made
StructuredBatch.InitrunTRUNCATE TABLE <table>; CHECKPOINT;before every batch, so an in-memory database reclaims the truncated rows. That fix is correct without durable state.With a state path,
rundisables autocommit on the connection (internal/cli/run/root.go, "Every batch from here on is one transaction"). Every statement then runs inside a transaction that closes only atcommitState.Initruns after the commit, so itsTRUNCATEopens a fresh transaction and deletes the previous batch's rows, and theCHECKPOINTthat follows is inside a transaction with local changes. DuckDB refuses that.The first
Init, at startup, passes because the table is empty: a truncate that deletes nothing leaves no local changes.Nothing in the test suite combines
StructuredBatchwithWithStateStoreor a state path. No shipped example does either:kafka.stateful.window.ymlandbenchmark.stateful.mem.ymlboth useInferredMemBatch, andbluesky.postgres.windowed.ymlusesStructuredBatchwithout a state block. The README's durable state section does not say the handler matters.Options
CHECKPOINT; TRUNCATE TABLE <table>;The checkpoint is then the first statement of a fresh transaction, with no local changes, and it reclaims the rows the previous batch's truncate left. The in-memory reclaim is one batch later than today, still bounded by one batch.commitState, after the commit and before the handler'sInit. That keeps the truncate-then-checkpoint order, but it moves handler knowledge into the pipeline.Option 1 is the smallest and does not need the pipeline to know which handler it runs.
Done when
internal/handlersrunsStructuredBatchon a file-backed DuckDB with autocommit disabled:Init,Write,Invoke, commit,Init, and asserts the secondInitsucceeds.handler.structuredagainststate.durability, so the combination cannot go untested again.Related: #247, #268.