Skip to content

fix(sqlite): return the error SQLite raises mid-query instead of ending the rows early - #87

Merged
lukekim merged 1 commit into
spiceai:spiceai-55from
grokspice:fix/sqlite-rows-error
Oct 18, 2026
Merged

lukekim merged 1 commit into
spiceai:spiceai-55from
grokspice:fix/sqlite-rows-error

Conversation

@grokspice

Copy link
Copy Markdown

Summary

rows_to_arrow (the SQLite row reader behind query_arrow and get_schema) read rows with if let Ok(Some(row)) / while let Ok(Some(row)), so an Err from rows.next() was taken as the end of the result. A statement SQLite fails partway through returned the rows produced before the error, or none at all, as a successful answer: an ungrouped SUM over BIGINT values whose total passes i64::MAX came back as zero rows, with no error (spiceai/spiceai#14661).

The error is now propagated as FailedToReadRow, so the query fails with SQLite's own message and never returns a partial or empty result as if it were the answer.

Refs spiceai/spiceai#14661 (closed by the pin bump in spiceai/spiceai that picks this up).

Changes

  • core/src/sql/arrow_sql_gen/sqlite.rs: both rows.next() reads propagate the error (FailedToReadRow { source }, displayed as Failed to read the next row: {source}); the function's # Errors doc says so.
  • Regression tests through the real SQLite reader: an_error_sqlite_raises_on_the_first_row_fails_the_read (the only row of an ungrouped SUM overflows) and an_error_sqlite_raises_after_a_row_fails_the_read (a row is produced before the error fires, so a dropped error would have truncated the answer rather than emptied it).
  • core/src/sqlite/federation.rs: an_error_sqlite_raises_mid_query_fails_the_federated_query runs the overflowing SUM through the federated provider (asserting it is pushed down) and requires the failure.

Reproduction

A probe crate drove the federated SQLiteTable over an in-memory table of three BIGINT 9000000000000000000 values, the way spiced does. On the base of this branch (b4a86350d5):

$ tp-probe overflow
DEFECT: SELECT sum(v) FROM ovf returned rows instead of an error:
++
++
DEFECT: grouped SUM returned a truncated result instead of an error:
++
++
overflow exit=1

On this branch:

$ tp-probe overflow
OK: SELECT sum(v) FROM ovf failed with SQLite's error: execute: Execution error: Failed to execute query.
ConnectionError Error("Failed to convert query result to Arrow: Failed to read the next row: integer overflow")
OK: grouped SUM failed with SQLite's error: ...Failed to read the next row: integer overflow")
overflow exit=0

The three tests, run with the fix reverted (the two rows.next() lines put back) and then with it:

$ cargo test -p datafusion-table-providers --lib --no-default-features --features sqlite-federation,sqlite-bundled -- an_error_sqlite
test sql::arrow_sql_gen::sqlite::tests::an_error_sqlite_raises_on_the_first_row_fails_the_read ... FAILED
test sql::arrow_sql_gen::sqlite::tests::an_error_sqlite_raises_after_a_row_fails_the_read ... FAILED
  the overflow SQLite raises must be returned, not an empty batch: RecordBatch { schema: Schema { fields: [], metadata: {} }, columns: [], row_count: 0 }
  the overflow SQLite raises after the first row must be returned, not a one-row batch: RecordBatch { schema: Schema { fields: [Field { name: "w", data_type: Int64, nullable: true }], ...
test sqlite::federation::tests::an_error_sqlite_raises_mid_query_fails_the_federated_query ... FAILED
  SQLite's integer overflow must fail the query: []

(with the fix)
test sql::arrow_sql_gen::sqlite::tests::an_error_sqlite_raises_after_a_row_fails_the_read ... ok
test sql::arrow_sql_gen::sqlite::tests::an_error_sqlite_raises_on_the_first_row_fails_the_read ... ok
test sqlite::federation::tests::an_error_sqlite_raises_mid_query_fails_the_federated_query ... ok
test result: ok. 3 passed; 0 failed

Test plan

  • cargo test -p datafusion-table-providers --lib --no-default-features --features sqlite-federation,sqlite-bundled -- arrow_sql_gen::sqlite sqlite::federation — 12 passed, 0 failed (output above for the three new tests)
  • cargo clippy -p datafusion-table-providers --no-default-features --features sqlite-federation,sqlite-bundled --tests -- -D warnings — no finding in the two changed files (the pre-existing dead-code findings it reports in core/tests/ and sql_provider_datafusion/mod.rs come from test helpers that are only used under other features; cargo clippy --all-features, which CI runs, does not report them)
  • CI (make test)

Review gate

  • Adversarial review: codex — job review-mv09decb-c994p8 — verdict approve, no findings ("Both row-reading paths now propagate SQLite errors before returning a batch, and the federation path preserves the failure")
  • /code-review: 2 cosmetic findings in the new tests (a lost line continuation in a SQL literal, a raw newline in an assertion message), both fixed; 2 informational notes, no change needed
  • /security-review: no findings (no new input path; the change only propagates an error SQLite already raised)
  • /simplify: not run (the skill diffs the main checkout, not this fork worktree); the diff was read for cleanup by hand

…ng the rows early

`rows_to_arrow` read rows with `if let Ok(Some(row))` and `while let Ok(Some(row))`,
so an `Err` from `rows.next()` was treated as the end of the result. A statement that
SQLite fails partway through, such as `SUM` over `BIGINT` values whose total passes
`i64::MAX` (`integer overflow`), therefore returned the rows produced before the error,
or none at all, as a successful answer: an ungrouped aggregate came back as zero rows
with no error logged (spiceai/spiceai#14661).

Propagate the error as `FailedToReadRow`, so the query fails with SQLite's message and
never returns a partial or empty result as if it were the answer.
Copilot AI balanced review requested due to automatic review settings October 9, 2026 01:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approved

Both row-fetch paths correctly propagate errors with direct and federated regression coverage.

0 open findings

What changed in this PR

Fixes SQLite row reading so mid-query errors propagate instead of returning partial or empty results.

Changes:

  • Adds FailedToReadRow error propagation for both row-fetch paths.
  • Adds direct and federated regression tests for SQLite integer overflow.
File Description
core/​src/​sql/​arrow_sql_gen/​sqlite.rs Propagates row-read errors and adds regression tests.
core/​src/​sqlite/​federation.rs Tests error propagation through federated queries.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@grokspice

Copy link
Copy Markdown
Author

DuckDB Tests (release) is red here, and the failing test is unstable on spiceai-55 independently of this PR. This PR's diff cannot reach it. Recording the triage so a maintainer can act; I have pull only on this repo, so I can neither re-run the job nor merge.

The failure

Job 113627203495 (run 37870489732, head 4038967cd):

thread 'duckdb::upsert_retention_memory::upsert_with_retention_under_query_load_does_not_grow_memory::case_1_single_column'
  panicked at core/tests/duckdb/upsert_retention_memory.rs:694:5:
refresh write failed on cycle 25 of 60 with 262005 rows in the table (key space 300000, memory_limit 256MB).
Engine memory: BASE_TABLE=33.8MiB TRANSACTION=25.5MiB ALLOCATOR=7.7MiB ART_INDEX=0.5MiB file=46.7 MiB
Error: External error: External error: Unable to insert into duckdb table:
       Constraint Error: Duplicate key "id: 263916" violates primary key constraint.

test result: FAILED. 22 passed; 1 failed; 3 ignored

Every other job in that run is green, including Tests, Clippy, and all nine Build (…) jobs.

The same test fails on unrelated branches, with three different engine errors

when branch job error
2026-10-06 sgrebnov/duckdb-1.4.4-pre-1.5-soak 112078300550 (attempt 5 of 5; all five failed) TransactionContext Error: Failed to commit: Invalid bitmask for FixedSizeAllocator — cycle 53
2026-10-07 viktor/sqlite-empty-projection-decode 112989260609 FATAL … IO Error: Corrupt database file: computed checksum 5381 does not match stored checksum 0 → SIGABRT
2026-10-07 lukim/federation-correlated-binding 113041812777 same FATAL … Corrupt database file → SIGABRT
2026-10-09 this PR 113627203495 Constraint Error: Duplicate key

Three distinct DuckDB storage/transaction faults, one test, four branches, none of which share a subject. The job's own retry wrapper already special-cases checksum failed for , so the instability is known at the workflow level.

Why this PR cannot be the cause

Its entire diff is SQLite-side:

core/src/sql/arrow_sql_gen/sqlite.rs   52+  3-
core/src/sqlite/federation.rs          99+  0-

rows_to_arrow in arrow_sql_gen/sqlite.rs takes rusqlite::Rows; sqlite/federation.rs is the SQLite federation provider. Neither is on any path duckdb::upsert_retention_memory executes.

Unverified, offered only as a lead

In the failing cycle the reported ART_INDEX drops from 3.0MiB (cycle 20) to 0.5MiB while the table holds ~262k of a 300k key space. A primary-key violation raised by an upsert, against a key the upsert exists to absorb, is consistent with the ART index losing entries under memory pressure at memory_limit=256MB. I have not reproduced that, and there is no Docker or DuckDB on the host I run from — treat it as a hypothesis, not a diagnosis.

What I am asking for

This PR is MERGEABLE/UNSTABLE — the failing job is not a required context, so it does not hard-block. It carries only a Copilot approval and needs a maintainer's. Either merge over the unrelated red, or re-run DuckDB Tests (release); I can do neither from pull access. The test's instability looks worth its own issue regardless of this PR.

@grokspice

Copy link
Copy Markdown
Author

Correcting my own earlier triage on this thread. I classified the DuckDB Tests (release) red as "unstable on spiceai-55 independently of this PR". The first half of that is wrong: it does not look unstable, it looks deterministic given the absence of #89.

DuckDB Tests (release) job conclusions across the five runs that exist on the three open PRs here: failure on 3/3 runs of branches without #89's change (this PR ×1, #88 ×2), success on 2/2 runs of #89's branch. Same test and case every time — upsert_retention_memory::upsert_with_retention_under_query_load_does_not_grow_memory::case_1_single_column. The base delta between the two groups is one datafusion-federation version pin (Cargo.toml/Cargo.lock, 1 line each), and #89's diff is production-only, so it did not edit the test into passing.

Full table, the confounders I ruled out, and the calibration (n=5, and I have pull only here so I could not re-run anything to extend the sample) are in #89's comment — one place to argue with the data rather than three.

The part of the original triage that stands: this PR's diff still cannot reach that test. It touches core/src/sql/arrow_sql_gen/sqlite.rs and core/src/sqlite/federation.rs only. What changes is the recommended action — not "wait for the flake to settle" but merge #89, then re-run this check.

The standing hold is otherwise unchanged: Python Tests is the repo-wide ClickHouse break, and I still have pull only on this repository, so I can neither re-run a job nor merge.

@grokspice

Copy link
Copy Markdown
Author

Retracting my previous comment (6095046838). I "corrected" the original triage on this thread, and the correction was wrong. The original triage was right: the DuckDB Tests (release) failure is unstable on spiceai-55 independently of this PR.

What refutes my correction is a same-commit pass/fail pair I had not looked for. The same push produced 37917557707 here (success) and 37917555233 on the fork (failure), both at ecf886a0d66e9f8d5a3b7afb042f4c68a8c2850d — the fork run dying with IO Error: Corrupt database file: computed checksum 5381 does not match stored checksum 0. Same code, opposite results.

It is also already tracked: spiceai/spiceai#14183, open since 2026-09-18, "DuckDB: upsert_retention_memory fails with engine state corruption (checksum-0 blocks, upsert PK violation, allocator bitmask) across unrelated branches" — covering both the duplicate-key panic seen here and the checksum-0 abort seen elsewhere.

So ignore my "merge #89, then re-run this check" recommendation. #89 does not fix this, and its own description says so. The standing hold is unchanged and correct: DuckDB Tests (release) is #14183, Python Tests is the repo-wide ClickHouse break, this diff (core/src/sql/arrow_sql_gen/sqlite.rs, core/src/sqlite/federation.rs) reaches neither, and I have pull only here so I can neither re-run a job nor merge.

@lukekim
lukekim merged commit 39c9f36 into spiceai:spiceai-55 Oct 18, 2026
12 of 14 checks passed
tsenart pushed a commit to perfloop-oss/spiceai that referenced this pull request Oct 23, 2026
…error and interval fixes, and read Turso's shifted timestamps (fixes spiceai#14661) (spiceai#15444)

* test(accelerators): DuckDB upsert keeps a repeated key's last copy; SQLite fails a query it aborts; SQLite and Turso interval shifts keep the time of day

Regression tests for spiceai#14643, spiceai#14661 and spiceai#14714, written against trunk before the
datafusion-table-providers pin moves to the fork fixes they guard.

* fix(accelerators): move datafusion-table-providers to the DuckDB upsert, SQLite row-error and SQLite interval fixes, and mark Turso's date operands

Pins datafusion-table-providers at 87c2cba (spiceai/datafusion-table-providers#87,
spiceai#88, spiceai#89). Turso builds its own federated table, so it now installs SQLiteRemoteTable
and mark_federated_date_operands, which the shared interval rewrite reads to shift a
DATE through date() and everything else through datetime().

* test(turso): read the timestamp SQLite's datetime() answers; check interval shifts reach the engine through federated projections

* fix(turso): read the YYYY-MM-DD HH:MM:SS text SQLite's datetime() answers as a UTC timestamp, not NULL

A shifted timestamp reaches Turso as datetime(ts, '-1 hours'), whose answer is not
RFC3339, so the reader returned NULL for every row.

* refactor(tests): share the CSV dataset and federated-SQL helpers, fold the upsert tests onto one helper, and pin rows inside the interval cases

* test(sqlite): require the overflowing call in the SQL sent to SQLite, not anywhere in the plan

* fix(accelerators): pin datafusion-table-providers at the SQLite fixes, before the DuckDB upsert statements that invalidate a DuckDB database

spiceai/datafusion-table-providers#89 splits a DuckDB upsert into several statements in
one transaction, and on DuckDB 1.4.4 a reused table then fails with an internal error that
invalidates the database (604 of 1000 overwrites in a release A/B; 0 of 1000 before it).
Pin spiceai#88's merge commit instead, and drop the spiceai#89 guard tests and ledger row.

---------

Co-authored-by: t <a@b.c>
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.

3 participants