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
{{ message }}
Repository navigation
bug: a U64 parameter above the I64 range is refused #918
A query comparing a U64 property with a U64 parameter bound to 18446744073709551615 fails with params rejected: param 'v': value 18446744073709551615 exceeds current engine range for U64 (max 9223372036854775807).
Expected result
The parameter binds and the query returns the one row whose size is 18446744073709551615.
A query parameter declared U64 cannot hold a value above i64::MAX (9223372036854775807). The engine refuses the value when it binds the parameters, before planning. The storage, the comparison typing and the execution path all support the full U64 range. Only the parameter carrier is too narrow. The carrier is Literal::Integer(i64), which all integer parameters share.
Verdict: reproduced
Main:0b5e17fa23d36483178240c28f39c5d3448394a1 (latest main, checked again before this comment). I used a clean git worktree at that commit.
Environment: Linux x86_64 cloud VM, rustc 1.97.1, --locked, debug test profile. The target is omnigraph-engine on local-filesystem, as the case declares.
Differences from the report: the OS and CPU are different (Linux x86_64, not macOS arm64). The main commit is newer (0b5e17fa, not dfc73f46). The case text is the same. I had to change one thing to run it, described in the next paragraph.
The corpus runner refuses the case as invalid_case if the file name is issue_918_*.gqt and the header is # issue: none. That refusal does not prove anything. So I ran the reporter's text byte for byte under a neutral name, and a copy with # issue: 918 under the issue name:
$ cargo test -p omnigraph-gqt --locked --test gq_logic_tests u64_param_above_i64
---- case::issue_918_u64_param_above_i64.gqt ----
"step 1 (query): params rejected: param 'v': value 18446744073709551615 exceeds current engine range for U64 (max 9223372036854775807); ..."
---- case::reporter_u64_param_above_i64.gqt ----
"step 1 (query): params rejected: param 'v': value 18446744073709551615 exceeds current engine range for U64 (max 9223372036854775807); ..."
test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 376 filtered out
The seed row with size = 18446744073709551615 loads with no error. The refusal comes at step 1, when the parameters bind. This is the failing test on latest main. A fix can use the same case as its regression test (with # issue: 918).
To find the edges of the problem, I ran these probe cases on the same schema (node Item { key: String @key size: U64 }):
Probe
Result on 0b5e17fa
Parameter 9223372036854775807 (i64::MAX), control
passes
Parameter 9223372036854775808 (i64::MAX + 1)
params rejected: ... exceeds current engine range for U64
Parameter as the decimal string "18446744073709551615"
invalid_case: ... parse error: invalid integer: number too large to fit in target type
The embedded Rust API has the same limit. impl ToParam for u64 refuses values above i64::MAX, and a unit test pins that refusal. query_input::tests::to_param_rejects_unsigned_values_outside_engine_range passes on main, so a fix must reverse that test.
The decimal-string result is important. The stored-query catalog tells clients to send 64-bit integers as JSON strings (ParamKind::BigInt, omnigraph-api-types/src/lib.rs L1538-L1543), so that it does not lose precision past 2^53. The string form gets the same refusal, so a client has no way to send the upper half of the U64 range.
Root cause
The literal carrier is Literal::Integer(i64) (query/ast.rs L991-L1001). Every integer value goes through it: parameters, query literals and mutation values.
The U64 binding branch parses a u64 correctly, then narrows it to i64 with i64::try_from (query_input.rs L424-L445). The narrowing gives the reported message. ToParam for u64 and usize narrow in the same way (L158-L182).
The code after the binding already handles the full range. typed_scalar builds ScalarValue::UInt64 from the declared type with u64::try_from (engine/typed_value.rs L234-L237). Scan pushdown uses the same function (engine/scan.rs L1158-L1165). The comparison typing reasons in i128 and has an exact domain for mixed signed/U64 comparisons (ir/coerce.rs L94-L107, L312-L346). The only defect is the width of the payload.
The user docs say "Parameters use their declared type, regardless of the supplied value" (docs/user/queries/index.md L66-L73). The i64 limit on U64 parameters is not documented. The message "current engine range" shows that the limit was deliberate, but the declared type does not show it.
Recommended fix: one integer carrier for every declared integer domain
Make the carrier wide enough for every declared integer type. Narrow the value only at a typed boundary, and only from the declared type (invariant 9).
Change Literal::Integer(i64) to Literal::Integer(i128). This holds I32, I64, U32 and U64 with one spelling per value, so the by-value Eq/Hash in ast.rs stays sound. It also matches the compiler's existing exact domain (literal_round_trips uses i128, and exact_integer is Decimal128(38, 0)).
In the U64 branch of json_value_to_literal_typed, return Literal::Integer(i128::from(value)). Remove the "current engine range" errors. Make ToParam for u64, usize and isize lossless.
Narrow only in typed_scalar (engine) and literal_to_typed_array (mutations), with iN::try_from/uN::try_from from the declared type. These places already exist and already refuse out-of-range values.
Keep the saved bound-plan format readable. LiteralMirror::Integer(i64) is serialized into replay envelopes (omnigraph-planner/src/mirror.rs L1297-L1308). Do not put a bare i128 in this adjacently tagged enum. The pinned serde 1.0.228 buffered Content has no I128 arm, so decoding can fail when serde buffers the content. Keep the integer arm for values in the i64 range, so existing saved plans still decode. Add an arm that holds wider values as a decimal string, or increase the envelope version. Add a round-trip test at u64::MAX and i64::MIN in omnigraph-planner/tests/integration/bound_plan.rs.
The frozen v1 engine matches Literal exhaustively (reference-engine/src/projection.rs L157-L177, query.rs L4478-L4489, L4702-L4718). Every carrier change in this section needs a small v1 edit, an update to the pin in tests/frozen.rs, and a reviewer. In v1, narrow with i64::try_from and refuse other values. Do not add v1 behavior.
Query-text literals (the last probe) use the same carrier, but they also need a language decision: what type does 18446744073709551615 get? A rule that agrees with the current narrowing is "I64 if the value fits, else U64; refuse values outside [i64::MIN, u64::MAX]". You can do this in the same change with its own case, or track it separately. It is not part of this issue as reported.
Regression evidence for the fix:
Add crates/omnigraph-gqt/cases/issue_918_u64_param_above_i64.gqt with # issue: 918. Use the reporter's step, and add steps for these items:
2^63 (the first value that fails now)
the decimal-string form
insert, and update ... where, with a U64 parameter
a [U64] list parameter with in/contains
a U64 parameter compared with an I64 property (the exact_integer cast)
an indexed U64 property (scan pushdown)
refusal of 18446744073709551616 and -1
Reverse to_param_rejects_unsigned_values_outside_engine_range. Its name must contain issue_918 for the Fix Regression Gate.
Add a changelog.d/<slug>.fixed.md note. Add one sentence to the parameter section of the query guide.
The change does not touch storage, so the DST cost golden does not change. ParamKind::BigInt is already a string on the wire, so openapi.json does not change either.
Alternatives and tradeoffs
Option
Correctness
Ongoing liability
A. Widen the one carrier to i128 (recommended)
Exact for all declared integer types. Narrowing happens in one place per typed boundary.
About 37 files match Literal::Integer(, and most need only a type change. v1 needs an edit and a pin update. The mirror needs a compatible wire form. Literal can grow from 16-byte alignment, which does not matter because there are few literals per query.
B. Add Literal::Unsigned(u64)
Exact.
One value gets two spellings (Integer(5) and Unsigned(5)). By-value Eq/Hash and plan identity then need a canonical-form rule that every producer must obey. Every consumer must handle two arms. The liability grows with each new integer type.
C. Bind parameters as typed Arrow ScalarValues from the declared type
The strongest typing.
The largest change: the public params!/ToParam API, the replay envelope, v1 and GQT. Use this as a long-term direction if more numeric types come (for example Decimal or I128), not as a bug fix.
D. Carry large U64 values as Float or String
Wrong. Float rounds 2^64 - 1 to 2^64. String carries query meaning in strings, which the invariants' deny-list forbids.
Do not use.
Option A gives a small amount of efficiency (enum size) for clarity and robustness. That is the correct priority here, because the cost is per literal, not per row.
Lance upstream assessment
This is not a Lance defect, and no Lance change will fix it or affect it.
Lance stores UInt64 natively (Lance data types guide). The probes show this: a u64::MAX row loads and reads back exactly.
Lance's SQL-string filter planner once refused UInt64 literals above i64::MAX (lance#7910). lance#8178 fixed it on 2026-08-03. The pinned Lance 11.0.0 contains that fix: GitHub compare shows v11.0.0 307 commits ahead of the fix commit and 0 behind, and lance-datafusion-11.0.0/src/planner.rs L491-L493 parses i64 and then u64. Also, the OmniGraph v2 query pushdown builds typed DataFusion expressions (filter_expr), not SQL strings.
lance#8807 (open) is about integers above i64::MAX in Lance JSON-typed columns. OmniGraph U64 properties are Arrow UInt64 columns, so that issue does not apply.
The release notes for v12.0.0, v13.0.0 (latest stable) and v14.0.0-beta.19 contain no related item. A search of open Lance PRs for unsigned/UInt64 filter or literal work found nothing related. A Lance version bump does not change the recommendation.
typed_comparison_domains.gqt already seeds u64::MAX and compares U64 properties with I64 parameters. It never binds a U64 parameter above i64::MAX, which is why this gap was not found.
No open PR touches this. I found no issue that supersedes this one, so I am not closing it.
Limitations
I ran on Linux x86_64, not on the reporter's macOS arm64. Nothing in the failing path depends on the platform.
I ran only the omnigraph-engine local-filesystem target. I did not run the HTTP server or the CLI end to end. Both call the same Standard-mode function (the links are in the root-cause section).
I did not write a prototype of the fix. No after-fix evidence exists yet.
I have taken this one — a pull request is open: #1102
It implements option A from the triage note: the literal carrier is widened to i128 and the value is narrowed at a typed boundary, from the declared type. The bind path no longer refuses an unsigned value above i64::MAX; the engine still refuses it where the target type cannot hold it, naming the value and the range.
Two parts of the change to look at closely:
the saved-plan wire form gains one arm (WideInteger, the decimal text), because the pinned serde buffers carry no i128. The integer arm keeps its form and its meaning, so existing saved plans still decode;
the frozen v1 engine is touched in two places — narrow and refuse, no new v1 behaviour — and its pinned hashes are updated in the same pull request, as the note asks.
I did not bump BOUND_PLAN_VERSION; if you prefer the bump it is a one-line change plus a note about regenerating saved plans. Query-text integer literals are unchanged, since that is a language decision rather than part of this issue.
Coverage: a compiler unit test at the bind, a planner test that a saved plan carrying u64::MAX round-trips byte-exactly, and a GQT case in the repository's own format that reads and writes the value 18446744073709551615 end to end.
Activity
ragnorc commentedon Oct 17, 2026
Reproduction and fix assessment
Summary
A query parameter declared
U64cannot hold a value abovei64::MAX(9223372036854775807). The engine refuses the value when it binds the parameters, before planning. The storage, the comparison typing and the execution path all support the fullU64range. Only the parameter carrier is too narrow. The carrier isLiteral::Integer(i64), which all integer parameters share.Verdict: reproduced
0b5e17fa23d36483178240c28f39c5d3448394a1(latest main, checked again before this comment). I used a cleangit worktreeat that commit.--locked, debug test profile. The target isomnigraph-engineonlocal-filesystem, as the case declares.0b5e17fa, notdfc73f46). The case text is the same. I had to change one thing to run it, described in the next paragraph.The corpus runner refuses the case as
invalid_caseif the file name isissue_918_*.gqtand the header is# issue: none. That refusal does not prove anything. So I ran the reporter's text byte for byte under a neutral name, and a copy with# issue: 918under the issue name:The seed row with
size = 18446744073709551615loads with no error. The refusal comes at step 1, when the parameters bind. This is the failing test on latest main. A fix can use the same case as its regression test (with# issue: 918).To find the edges of the problem, I ran these probe cases on the same schema (
node Item { key: String @key size: U64 }):0b5e17fa9223372036854775807(i64::MAX), control9223372036854775808(i64::MAX + 1)params rejected: ... exceeds current engine range for U64"18446744073709551615"u64::MAX, read it back with no parameterinsert Item { key: "e", size: $v }with$v = u64::MAXstep 1 (mutate): params rejected: ...$i.size = 18446744073709551615invalid_case: ... parse error: invalid integer: number too large to fit in target typeThe embedded Rust API has the same limit.
impl ToParam for u64refuses values abovei64::MAX, and a unit test pins that refusal.query_input::tests::to_param_rejects_unsigned_values_outside_engine_rangepasses on main, so a fix must reverse that test.The decimal-string result is important. The stored-query catalog tells clients to send 64-bit integers as JSON strings (
ParamKind::BigInt,omnigraph-api-types/src/lib.rsL1538-L1543), so that it does not lose precision past 2^53. The string form gets the same refusal, so a client has no way to send the upper half of theU64range.Root cause
Literal::Integer(i64)(query/ast.rsL991-L1001). Every integer value goes through it: parameters, query literals and mutation values.U64binding branch parses au64correctly, then narrows it toi64withi64::try_from(query_input.rsL424-L445). The narrowing gives the reported message.ToParam for u64andusizenarrow in the same way (L158-L182).json_params_to_param_mapwithJsonParamMode::Standard(omnigraph-server/src/handlers.rsL3403,omnigraph-cli/src/helpers.rsL970,omnigraph-gqt-core/src/lib.rsL3012). So all transports have this defect. No production caller usesJsonParamMode::JavaScript.typed_scalarbuildsScalarValue::UInt64from the declared type withu64::try_from(engine/typed_value.rsL234-L237). Scan pushdown uses the same function (engine/scan.rsL1158-L1165). The comparison typing reasons ini128and has an exact domain for mixed signed/U64comparisons (ir/coerce.rsL94-L107, L312-L346). The only defect is the width of the payload.Literal::IntegertoUInt64(exec/mutation.rsL67-L72, L252-L262). The parser reads integer literals asi64(query/parser.rsL1538-L1544).The user docs say "Parameters use their declared type, regardless of the supplied value" (
docs/user/queries/index.mdL66-L73). Thei64limit onU64parameters is not documented. The message "current engine range" shows that the limit was deliberate, but the declared type does not show it.Recommended fix: one integer carrier for every declared integer domain
Make the carrier wide enough for every declared integer type. Narrow the value only at a typed boundary, and only from the declared type (invariant 9).
Literal::Integer(i64)toLiteral::Integer(i128). This holdsI32,I64,U32andU64with one spelling per value, so the by-valueEq/Hashinast.rsstays sound. It also matches the compiler's existing exact domain (literal_round_tripsusesi128, andexact_integerisDecimal128(38, 0)).U64branch ofjson_value_to_literal_typed, returnLiteral::Integer(i128::from(value)). Remove the "current engine range" errors. MakeToParamforu64,usizeandisizelossless.typed_scalar(engine) andliteral_to_typed_array(mutations), withiN::try_from/uN::try_fromfrom the declared type. These places already exist and already refuse out-of-range values.LiteralMirror::Integer(i64)is serialized into replay envelopes (omnigraph-planner/src/mirror.rsL1297-L1308). Do not put a barei128in this adjacently tagged enum. The pinned serde 1.0.228 bufferedContenthas noI128arm, so decoding can fail when serde buffers the content. Keep theintegerarm for values in thei64range, so existing saved plans still decode. Add an arm that holds wider values as a decimal string, or increase the envelope version. Add a round-trip test atu64::MAXandi64::MINinomnigraph-planner/tests/integration/bound_plan.rs.Literalexhaustively (reference-engine/src/projection.rsL157-L177,query.rsL4478-L4489, L4702-L4718). Every carrier change in this section needs a small v1 edit, an update to the pin intests/frozen.rs, and a reviewer. In v1, narrow withi64::try_fromand refuse other values. Do not add v1 behavior.18446744073709551615get? A rule that agrees with the current narrowing is "I64if the value fits, elseU64; refuse values outside[i64::MIN, u64::MAX]". You can do this in the same change with its own case, or track it separately. It is not part of this issue as reported.Regression evidence for the fix:
crates/omnigraph-gqt/cases/issue_918_u64_param_above_i64.gqtwith# issue: 918. Use the reporter's step, and add steps for these items:2^63(the first value that fails now)insert, andupdate ... where, with aU64parameter[U64]list parameter within/containsU64parameter compared with anI64property (theexact_integercast)U64property (scan pushdown)18446744073709551616and-1to_param_rejects_unsigned_values_outside_engine_range. Its name must containissue_918for the Fix Regression Gate.changelog.d/<slug>.fixed.mdnote. Add one sentence to the parameter section of the query guide.ParamKind::BigIntis already a string on the wire, soopenapi.jsondoes not change either.Alternatives and tradeoffs
i128(recommended)Literal::Integer(, and most need only a type change. v1 needs an edit and a pin update. The mirror needs a compatible wire form.Literalcan grow from 16-byte alignment, which does not matter because there are few literals per query.Literal::Unsigned(u64)Integer(5)andUnsigned(5)). By-valueEq/Hashand plan identity then need a canonical-form rule that every producer must obey. Every consumer must handle two arms. The liability grows with each new integer type.ScalarValues from the declared typeparams!/ToParamAPI, the replay envelope, v1 and GQT. Use this as a long-term direction if more numeric types come (for exampleDecimalorI128), not as a bug fix.U64values asFloatorStringFloatrounds2^64 - 1to2^64.Stringcarries query meaning in strings, which the invariants' deny-list forbids.Option A gives a small amount of efficiency (enum size) for clarity and robustness. That is the correct priority here, because the cost is per literal, not per row.
Lance upstream assessment
This is not a Lance defect, and no Lance change will fix it or affect it.
UInt64natively (Lance data types guide). The probes show this: au64::MAXrow loads and reads back exactly.UInt64literals abovei64::MAX(lance#7910). lance#8178 fixed it on 2026-08-03. The pinned Lance 11.0.0 contains that fix: GitHub compare showsv11.0.0307 commits ahead of the fix commit and 0 behind, andlance-datafusion-11.0.0/src/planner.rsL491-L493 parsesi64and thenu64. Also, the OmniGraph v2 query pushdown builds typed DataFusion expressions (filter_expr), not SQL strings.i64::MAXin Lance JSON-typed columns. OmniGraphU64properties are ArrowUInt64columns, so that issue does not apply.UInt64filter or literal work found nothing related. A Lance version bump does not change the recommendation.Related work
[String]. It is a different literal-typing defect in the sameLiterallayer. It is not the same defect, and it is not a superseding issue.sum. It added theexact_integerdomain that option A aligns with.issue_858_integer_sum_unsigned.gqtalready seedsu64::MAX.typed_comparison_domains.gqtalready seedsu64::MAXand comparesU64properties withI64parameters. It never binds aU64parameter abovei64::MAX, which is why this gap was not found.Limitations
omnigraph-enginelocal-filesystem target. I did not run the HTTP server or the CLI end to end. Both call the sameStandard-mode function (the links are in the root-cause section).lab1207 commentedon Oct 20, 2026
I have taken this one — a pull request is open: #1102
It implements option A from the triage note: the literal carrier is widened to
i128and the value is narrowed at a typed boundary, from the declared type. The bind path no longer refuses an unsigned value abovei64::MAX; the engine still refuses it where the target type cannot hold it, naming the value and the range.Two parts of the change to look at closely:
WideInteger, the decimal text), because the pinned serde buffers carry noi128. Theintegerarm keeps its form and its meaning, so existing saved plans still decode;I did not bump
BOUND_PLAN_VERSION; if you prefer the bump it is a one-line change plus a note about regenerating saved plans. Query-text integer literals are unchanged, since that is a language decision rather than part of this issue.Coverage: a compiler unit test at the bind, a planner test that a saved plan carrying
u64::MAXround-trips byte-exactly, and a GQT case in the repository's own format that reads and writes the value18446744073709551615end to end.