Repository navigation
fix(array): convert a date scalar into a timestamp instead of re-labelling it - #93
Conversation
…lling it `Scalar::cast` sent every cast whose *target* is an extension type through that target's storage type and then re-labelled the result. For a source that is itself an extension type that discards what the source means: `vortex.date` and `vortex.timestamp` both count from the epoch but in different units, so the number is not transferable between them. Two ways that surfaced, both through the same expression. A scan falsifies `cast(col as timestamp) > lit` into `cast(max(col) as timestamp) <= lit`, binds `max(col)` to the file's statistic and casts that scalar to decide whether to read the file at all: - `date[days]` stores `i32` and `timestamp[ns]` stores `i64`, so the storage cast was refused and the scan failed with `No CastReduce to cast constant array from vortex.date[days](i32?) to vortex.timestamp[ns](i64?)`. - `date[ms]` stores `i64`, the same as `timestamp[ns]`, so the storage cast succeeded and handed back the millisecond count as a nanosecond one — an instant 10^6 too small. The file was then pruned as unable to match, and rows that did match were silently dropped. Let an extension source convert itself: `ExtScalar::cast` now performs the `vortex.date` to `vortex.timestamp` rescale, and refuses any other pair of extension types rather than re-labelling one as the other. The rescale is the one `Extension`'s `CastKernel` already applied to arrays, moved into `extension::datetime::cast` so both callers share it — a file is pruned on the scalar conversion and its surviving rows are filtered on the array one, so the two disagreeing is what drops rows. Reported as spiceai/spiceai#13624.
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes date-scalar-to-timestamp conversion so file-statistics pruning matches row-level array casting.
Changes:
- Routes extension scalar casts through extension-aware conversion.
- Shares checked date-to-timestamp scaling between scalar and array paths.
- Adds regression tests and patch tracking.
Checks: Static review only; tests were not run.
File summaries
| File | Description |
|---|---|
vortex-array/src/scalar/cast.rs |
Dispatches extension sources correctly. |
vortex-array/src/scalar/typed_view/extension/mod.rs |
Implements scalar temporal conversion. |
vortex-array/src/scalar/typed_view/extension/tests.rs |
Adds scalar and array agreement tests. |
vortex-array/src/extension/datetime/mod.rs |
Exposes shared conversion helpers. |
vortex-array/src/extension/datetime/cast.rs |
Centralizes checked temporal scaling. |
vortex-array/src/arrays/extension/compute/cast.rs |
Reuses shared conversion logic. |
SPICE_PATCHES.md |
Records verification commands. |
Review details
Suppressed comments (2)
vortex-array/src/scalar/typed_view/extension/tests.rs:384
- Move this test's imports into the module-level import block at
tests.rs:4-15, as done inarrays/extension/compute/cast.rs:167-186. Function-scoped imports are inconsistent with the repository's established import placement.
use crate::IntoArray;
use crate::arrays::ExtensionArray;
use crate::arrays::PrimitiveArray;
use crate::builtins::ArrayBuiltins;
use crate::executor::VortexSessionExecute;
vortex-array/src/scalar/typed_view/extension/tests.rs:462
- Move these imports into the module-level import block at
tests.rs:4-15, consistent witharrays/extension/compute/cast.rs:167-186; imports in this test module should not be scoped to individual test functions.
use crate::extension::datetime::TimeUnit;
use crate::extension::datetime::Timestamp;
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
One consequence worth flagging, since it is a deliberate choice rather than an oversight. Refusing every extension pair other than Two reasons to leave it refused rather than widen the conversion:
The cost is bounded and is not correctness. In |
…oth paths The conversion checked only that the rescaled value fits `i64`, and the `i64` range is wider than the instants a timestamp can hold. So `date[days]` at `i32::MAX` — an ordinary `Date32` value — converts to `timestamp[s]` in the array path, while the scalar path builds a scalar from the result and `Timestamp`'s validation rejects it. That is the disagreement this patch set exists to remove, at the other end of the range: a scan would fail casting a file's statistic while the rows it gates convert happily. Fold the bound into the conversion, which is now a `DateToTimestamp` built once per cast and carrying the target's representable range alongside the scale, so neither caller can apply the scale without the check. The scalar path did not merely error there, either. `Timestamp::unpack_native` built its Jiff span with the unchecked constructors, which panic outside Jiff's *span* range — wider than its timestamp range — so a value between the two limits aborted instead of failing the `checked_add` below it. Build the span through `TimeUnit::to_jiff_span`, which is checked. That path is reachable from any read of such a value, not only from a cast. Reported by review on spiceai/spiceai#13745.
Review on this PR found the previous pin still let the scalar and array casts disagree, at the far end of the range rather than the near one: the shared conversion checked only that the rescaled value fits `i64`, which is wider than the instants a timestamp can hold. `date[days]` at `i32::MAX` — an ordinary `Date32` value — converted in the array path and was then rejected by `Timestamp`'s validation in the scalar path, so a scan could still fail casting a file's statistic while the rows it gates converted fine. Worse, that rejection was a panic rather than an error for part of the range, because `Timestamp::unpack_native` built its Jiff span with the unchecked constructors. spiceai/vortex#93 now folds the target's representable range into the conversion both paths share, and makes that validation report instead of aborting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed The conversion checked only that the rescaled value fits
The Two changes:
The conformance test now casts one value per array rather than a batch: an array cast fails as a whole, so a batch containing one unconvertible value says nothing about the ones beside it. It covers Both guards were checked by mutation. Removing the range check fails the conformance test and the three |
|
For anyone reading the red checks: On On
I'll re-run the coverage job once the in-progress workflow finishes and this becomes possible. |
`Timestamp::unpack_native` validated a storage value by building a Jiff span from it and adding that to the epoch, and a span's limits are not a timestamp's at either end. They stop one short of `i64::MIN` nanoseconds, which is 1677-09-21T00:12:43.145224192Z — an instant a `vortex.timestamp[ns]` array holds and that `DateToTimestamp` converts a date into, so the scalar refused a value the array path produced. That is the disagreement this change set exists to remove: a scan prunes a file on the scalar conversion and filters the rows it kept on the array one. They also run past the last instant, and outside them the span constructors abort rather than report. Both ends now come from `Timestamp::storage_range`, which is the range `DateToTimestamp` already converts into, so a scalar accepts exactly the values the array kernel produces and there is one definition of the range rather than two that have to agree. Reported by review on spiceai/spiceai#13745. Claude-Session: https://claude.ai/code/session_01KfJw4MeXt3YzD26SgKVdyy
`Display` built its instant with `Span::new().nanoseconds(v)` and `Timestamp + Span`, both of which abort rather than report. A `Display` impl has no way to report a failure, so it cannot be built on them — and admitting the whole `i64` nanosecond range put a value that aborts here within reach of `format!` on an ordinary scalar. Build the instant with the reporting constructors: `Timestamp::from_nanosecond` for nanoseconds, whose range covers every `i64`, and the checked span plus `checked_add` for the other units, whose own limits are the timestamp's. A count that denotes no instant renders as itself rather than taking the process down, alongside the existing fallback for a timezone that does not resolve. Four of the five cases guarded here aborted before this change, not one: the seconds and milliseconds extrema and the first instant past the range were all reachable through a `TimestampValue` built directly. Reported by review on #93. Claude-Session: https://claude.ai/code/session_01KfJw4MeXt3YzD26SgKVdyy
There was a problem hiding this comment.
🟢 Approval recommended
Review details
Suppressed comments (4)
Previously missed (2) — in code that hasn't changed since the last review.
vortex-array/src/extension/datetime/cast.rs:172
- This boundary date is mislabeled: 2,932,896 days after 1970-01-01 is 9999-12-31, not 10000-01-01. It is rejected because Jiff's timestamp ceiling falls before that day's midnight, so the current explanation gives the wrong reason for the boundary.
This issue also appears on line 187 of the same file.
// Jiff stops at the end of year 9999; 2_932_896 days is 10000-01-01.
vortex-array/src/scalar/typed_view/extension/tests.rs:392
- The named boundary is off by one day: the rejected day count in this test is 9999-12-31, not 10000-01-01. Please describe the actual first date whose midnight exceeds Jiff's timestamp range.
This issue also appears on line 407 of the same file.
// Ordinary dates, then the boundaries: the largest `Date32` value, the first day no
// timestamp can represent (10000-01-01) and the last one that can. The boundaries are
vortex-array/src/extension/datetime/cast.rs:187
last_dayis 2,932,895 days after the epoch, which is 9999-12-30, not 9999-12-31. The latter day's midnight is already beyond Jiff's timestamp range, so the comment currently contradicts the boundary asserted above.
// 9999-12-31 fits in seconds through microseconds; nanoseconds run out in 2262.
vortex-array/src/scalar/typed_view/extension/tests.rs:408
- These Date64 cases do not test the boundary described above: they are 10000-01-01 and 9999-12-31T23:59:59, so both are already outside Jiff's timestamp range. Use the same adjacent date boundary as the Date32 cases—9999-12-31T00:00 (rejected) and 9999-12-30T00:00 (accepted)—to verify scalar/array agreement on both sides for millisecond storage.
253_402_300_800_000,
253_402_300_799_000,
- Files reviewed: 8/8 changed files
- Comments generated: 1
- Review effort level: Balanced
Two overclaims in the patch row and the test that carries it. It said a timestamp value never goes through a Jiff span. Rendering still builds one for seconds through microseconds, whose limits enclose the timestamp's — through the checked constructors and `checked_add`. Only nanoseconds bypass a span, because a span's nanosecond floor is one above `i64::MIN`. It also said row 15's conversion produces `i64::MIN` nanoseconds. It cannot: a `vortex.date` is days or milliseconds, and neither divides that value (`i64::MIN % 1_000_000 == 224192`, `% 86_400_000_000_000 == 763145224192`), and days are stored in `i32` besides. What reaches it is a `vortex.timestamp[ns]` column that holds the value directly, whose statistic the scan then binds as a scalar — so the range is what has to be right, not the conversion. Reported by review on #93. Claude-Session: https://claude.ai/code/session_01KfJw4MeXt3YzD26SgKVdyy
There was a problem hiding this comment.
🟢 Approval recommended
Review details
Suppressed comments (3)
Previously missed (2) — in code that hasn't changed since the last review.
vortex-array/src/extension/datetime/cast.rs:172
2_932_896days after 1970-01-01 is 9999-12-31, not 10000-01-01. This value is rejected because Jiff's timestamp range ends earlier on 9999-12-30, so the current explanation misidentifies both the date and the boundary being tested.
This issue also appears on line 187 of the same file.
// Jiff stops at the end of year 9999; 2_932_896 days is 10000-01-01.
vortex-array/src/scalar/typed_view/extension/tests.rs:394
- This description does not match both value sets: the day pair straddles Jiff's whole-day boundary, but the millisecond pair (
253_402_300_799_000and253_402_300_800_000) is entirely beyond Jiff's maximum. Describe these as accepted/rejected and out-of-range cases rather than claiming each source unit contains the first unrepresentable day and the last representable one.
// Ordinary dates, then the boundaries: the largest `Date32` value, the first day no
// timestamp can represent (10000-01-01) and the last one that can. The boundaries are
// where a range check applied to one path and not the other shows up — the array holds
// the converted value and the scalar's validation rejects it.
vortex-array/src/extension/datetime/cast.rs:187
last_dayis2_932_895, which denotes 9999-12-30, not 9999-12-31. The latter is already outside Jiff's timestamp range, as the preceding test asserts.
// 9999-12-31 fits in seconds through microseconds; nanoseconds run out in 2262.
- Files reviewed: 8/8 changed files
- Comments generated: 0 new
- Review effort level: Balanced
|
Differential check of the validation change, since replacing Both validations reproduced against jiff 0.2.31 and run over the same 816,068 values per the four stored units — every boundary of All four nanosecond disagreements are the same value — Seconds, milliseconds and microseconds are bit-for-bit unchanged, which is the part worth knowing — Harness: |
spiceai/vortex#93 squash-merged as 3c5246867, so the commit this branch pinned is no longer reachable from `spiceai-54` — the ledger already recorded it as being on that branch, which it was not. The squash carries all of it: the scalar cast, `Timestamp::storage_range` and the validation that uses it, and the rendering path that never aborts. Claude-Session: https://claude.ai/code/session_01KfJw4MeXt3YzD26SgKVdyy
… on it (fixes #13624) (#13745) * fix(vortex): convert a date statistic into a timestamp before pruning on it (fixes #13624) A pushed-down `CAST(date_col AS TIMESTAMP)` failed the scan of a Vortex file: No CastReduce to cast constant array from vortex.date[days](i32?) to vortex.timestamp[ns](i64?) The scan uses the filter twice. It falsifies `cast(col as timestamp) > lit` into `cast(max(col) as timestamp) <= lit` and casts the file's `max` statistic to decide whether to read the file at all, then casts the column's rows to filter the ones it read. The fork's `vortex.date` cast (fork PR #28) is registered on `ExtensionArray` and covers the second; the first is a scalar, and `Scalar::cast` routed an extension source through the *target's* storage type and re-labelled the result instead of converting it. That failed loudly for `Date32` and silently for `Date64`. `vortex.date[ms]` stores `i64`, the same as `vortex.timestamp[ns]`, so the re-label succeeded and returned the millisecond count as a nanosecond one — `2024-03-01` came back as `1970-01-01T00:28:29.2512Z`. The falsifier then concluded the file could hold no matching row, the scan skipped it, and the query returned no rows and no error. Fixed on the fork (spiceai/vortex#93): an extension scalar now converts itself, sharing the rescale with the array kernel so the statistic and the rows it gates cannot disagree, and refusing any extension pair the kernel refuses rather than re-labelling one as the other. Guards both halves in `crates/vortex`, and records the patch in `docs/dev/fork_patches.md` — the ledger's "patch that is present but incomplete" section was this defect, so it goes away with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(vortex): assert the date cast really pushes into the scan in both guards Both guards assert a row count, and DataFusion returns the same count when it evaluates the filter itself above the scan — so a pushdown that stopped happening would leave them green and guarding nothing. Share the plan assertion the Date32 guard already made, and give the Date64 one the same check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(vortex): repin past the date-cast range check the fork was missing Review on this PR found the previous pin still let the scalar and array casts disagree, at the far end of the range rather than the near one: the shared conversion checked only that the rescaled value fits `i64`, which is wider than the instants a timestamp can hold. `date[days]` at `i32::MAX` — an ordinary `Date32` value — converted in the array path and was then rejected by `Timestamp`'s validation in the scalar path, so a scan could still fail casting a file's statistic while the rows it gates converted fine. Worse, that rejection was a panic rather than an error for part of the range, because `Timestamp::unpack_native` built its Jiff span with the unchecked constructors. spiceai/vortex#93 now folds the target's representable range into the conversion both paths share, and makes that validation report instead of aborting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(vortex): move the pin past the timestamp-range half of the fork fix The scalar cast lands a converted date inside the range a timestamp scalar accepts. The fork validated that range by building a Jiff span, whose nanosecond floor is one above `i64::MIN` — so `timestamp[ns]` refused 1677-09-21T00:12:43.145224192Z, an instant the column holds and the conversion produces, while the array path admitted it. A scan prunes a file on the scalar conversion and filters the rows it kept on the array one, so the two ranges disagreeing is a file dropped by a comparison the row filter would never have made. The fork now validates against the timestamp's own range, which is the range the conversion targets. `a_nanosecond_timestamp_scalar_spans_the_whole_i64_range` guards it. The two cast guards pass on either pin, so without it half of the fork patch could go missing silently — which is what the ledger row now says. Claude-Session: https://claude.ai/code/session_01KfJw4MeXt3YzD26SgKVdyy * fix(vortex): move the pin past the timestamp rendering fix, and split its ledger row The fork also keeps a timestamp value off Jiff spans when rendering it, not only when validating it. `Display` has no way to report a failure, so building it on the unchecked span constructors turned a count no timestamp can hold into an abort — and admitting the whole `i64` nanosecond range put such a value within reach of formatting an ordinary scalar. `a_nanosecond_timestamp_scalar_spans_the_whole_i64_range` now renders the scalar it builds, so it covers both paths, and the ledger carries the span removal as its own row: it shares a fork PR with the date-to-timestamp scalar cast but is separately losable on a re-cut, and the two cast guards pass either way. Claude-Session: https://claude.ai/code/session_01KfJw4MeXt3YzD26SgKVdyy * docs: say what the timestamp patch does, not more than it does Two overclaims in the ledger row. The title said a timestamp value never goes through a Jiff span. Rendering still builds one for seconds through microseconds, whose limits enclose the timestamp's — through the checked constructors and `checked_add`. Only nanoseconds bypass a span, because a span's nanosecond floor is one above `i64::MIN`. The body said the date-to-timestamp conversion produces `i64::MIN` nanoseconds. It cannot: a `vortex.date` is days or milliseconds, and neither divides that value (`i64::MIN % 1_000_000 == 224192`, `% 86_400_000_000_000 == 763145224192`), and days are stored in `i32` besides. What reaches it is a `vortex.timestamp[ns]` column holding the value directly, as read from Arrow, whose statistic the scan then binds as a scalar — so the range is what was wrong, not the conversion. Matches the wording of the fork's own row for the same patch. Claude-Session: https://claude.ai/code/session_01KfJw4MeXt3YzD26SgKVdyy * test(vortex): guard the rendering fallback for the units that keep a span The nanosecond guard covers the half of the fork patch where the whole `i64` is admitted. The other three units keep a Jiff span, so what the patch changes for them is that the span is built with the checked constructors and added with `checked_add` — and only a count outside the range exercises that. A re-cut that restored either unchecked form would abort on those counts while the nanosecond guard stayed green, which is the gap the ledger row was claiming to cover. `a_timestamp_count_that_is_not_an_instant_renders_instead_of_aborting` pins both abort paths: `i64::MAX` seconds and milliseconds and `i64::MAX` microseconds are past the span range, and 253_402_300_800 seconds is inside it but past the last instant, so it reaches `Timestamp + Span`. Both fail on the pin before the rendering fix with the aborts they name, and an ordinary instant still renders as one rather than falling back. Claude-Session: https://claude.ai/code/session_01KfJw4MeXt3YzD26SgKVdyy * fix(vortex): move the pin onto the merged spiceai-54 tip spiceai/vortex#93 squash-merged as 3c5246867, so the commit this branch pinned is no longer reachable from `spiceai-54` — the ledger already recorded it as being on that branch, which it was not. The squash carries all of it: the scalar cast, `Timestamp::storage_range` and the validation that uses it, and the rendering path that never aborts. Claude-Session: https://claude.ai/code/session_01KfJw4MeXt3YzD26SgKVdyy --------- Co-authored-by: code <Code@Lukes-Mac-mini.localdomain> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> (cherry picked from commit ff94ca0)
…ys to timestamps instead of re-labelling (#75, #78, #93) Re-ports two related fork patch families from spiceai-54 to 0.85.0 as fresh implementations rather than cherry-picks, since neither applies: both touch functions upstream restructured, and the second depends on the first already being in place (resolve_timezone) to even compile against. Fixed-offset timezone resolution (#75, #78): Arrow allows a timestamp's timezone to be an IANA name or a fixed offset (+00:00), but jiff only resolves IANA names, so any offset-form timezone aborted the process via `.vortex_expect("unknown timezone")` -- in scalar validation, Display rendering, and TemporalMetadata::to_jiff. Ports the fork's resolve_timezone (new file, 38 passing tests carried over verbatim) and threads it through all three sites, plus Scalar::try_extension_ref (a fallible counterpart to the panicking extension_ref) through the three call sites that built extension scalars infallibly on a read path (min/max aggregation, scalar_at, the constant-folding reduce rule) and DateTimeParts::scalar_at. Date -> timestamp conversion (#93, spiceai/spiceai#13624): Scalar::cast sent any cast targeting an extension type through the target's storage type and re-labelled the result, discarding what the source meant -- vortex.date and vortex.timestamp both count from the epoch but in different units. A scan falsifies `cast(col as timestamp) > lit` into `cast(max(col) as timestamp) <= lit` and casts a file's statistic scalar to decide whether to read it at all, so this silently pruned files whose rows would have matched. Ports DateToTimestamp (new file, shared by both the array CastKernel and the scalar cast so the two cannot disagree about which files/rows to keep) and Timestamp::storage_range, replacing Jiff Span-based validation whose limits are not a timestamp's at either end (span floor is one nanosecond above i64::MIN, which a real timestamp[ns] column holds). This supersedes a98a78c's array-only port on this branch: that port used legacy_session() as a workaround for CastReduce's missing ExecutionCtx. Discovered from an untracked later Spice commit that this fork's own answer was to split into CastReduce (buffer-free restructuring) and a proper CastKernel (which does get a real ctx) instead -- the same split this commit uses, requiring an explicit `register_execute_parent_kernel(Cast.id(), Extension, ...)` that has no upstream equivalent (Extension had no Cast kernel at 0.85.0 at all, only the reduce path). Verified: cargo test -p vortex-array -p vortex-datetime-parts --lib: 3428 + 72 passed, 0 failed. cargo check across vortex-array, vortex-arrow, vortex-layout, vortex-file, vortex-io, vortex-utils, vortex-duckdb: clean, zero warnings. Not ported: the DuckDB TIMESTAMP_TZ dtype/scalar conversion's is_utc_timezone gate (vortex-duckdb/src/convert/{dtype,scalar}.rs) -- vortex-duckdb is not a crate Spice's Cargo.toml consumes, directly or transitively, so this is fork hygiene rather than something Spice's re-pin needs. Left for a follow-up.
What was wrong
Scalar::castsent every cast whose target is an extension type through that target's storage type and then re-labelled the result:That is sound for a source that carries no interpretation of its own. It is not sound for a source that is itself an extension type:
vortex.dateandvortex.timestampboth count from the epoch but in different units, so the number is not transferable between them.The path that reaches it is file-statistics pruning. A scan falsifies
cast(col as timestamp) > litintocast(max(col) as timestamp) <= lit, bindsmax(col)to the file's statistic and casts that scalar to decide whether to read the file at all —vortex-file/src/pruning.rs::can_prune_file_stats.Extension'sCastKernelconverts the array, so the rows are handled; the statistic is not.Both storage layouts went wrong, in opposite ways:
vortex.date[days]i32No CastReduce to cast constant array from vortex.date[days](i32?) to vortex.timestamp[ns](i64?)vortex.date[ms]i64, same astimestamp[ns]The second is the serious one.
2024-03-01came back as1970-01-01T00:28:29.2512Z, the falsifier concluded the file could hold no matching row, and the file was pruned. The query returned zero rows with no error.What this does
Lets an extension source convert itself.
Scalar::castno longer re-labels through the target's storage type when the source is an extension type;ExtScalar::castperforms thevortex.date→vortex.timestamprescale, and refuses any other pair of extension types rather than re-labelling one as the other.vortex.timestamp[ms]→vortex.timestamp[ns]now errors, matching what the array kernel already does (cast_different_ext_dtype), instead of silently returning a wrong instant.The rescale is the one
Extension'sCastKernelalready applied to arrays, moved intoextension::datetime::castso both callers share it. That sharing is the point: a file is pruned on the scalar conversion and its surviving rows are filtered on the array one, so the two disagreeing is what drops rows.Evidence
Before, on a four-row Vortex file with
event_date DATE(date[days]), throughspiceai/spiceai's DataFusion adapter:The same file with a
date[ms]column and the row2024-03-01, which matches:After, both queries are right —
count(*) = 1, and:And the scalar cast directly:
Tests
vortex-array/src/scalar/typed_view/extension/tests.rs:test_ext_scalar_cast_date_to_timestamp— both storage layouts land on the right instant.test_ext_scalar_cast_date_to_timestamp_matches_the_array_kernel— every date/timestamp unit pair, asserting the scalar cast and the array kernel agree value for value, and that the scalar cast refuses whatever the kernel refuses. This is the property the pruning path depends on.test_ext_scalar_cast_between_timestamp_units_is_refused— the re-label that used to succeed.All three fail on this branch's parent and pass on it.
cargo test -p vortex-arrayis green (3001 passed);vortex-file,vortex-scanandvortex-arroware unchanged, andvortex-layout's 7layouts::table::testsfailures reproduce on the parent commit (they need an installedvortex-ioruntime).SPICE_PATCHES.mdgets a row, and row 7 — the array-side half of this cast — gets theVerifycommand it was missing.Reported as spiceai/spiceai#13624.