Skip to content
3 changes: 3 additions & 0 deletions changelog.d/u64-param-above-i64-max.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Saved query plans now use envelope version 11, which carries a `U64` parameter above `i64::MAX` as its decimal text. Regenerate plans saved with earlier binaries; older binaries reject version 11 plans. See [saved plans][u64-param-above-i64-max-plans].

[u64-param-above-i64-max-plans]: ../docs/user/queries/explain.md#profile
3 changes: 3 additions & 0 deletions changelog.d/u64-param-above-i64-max.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- A `U64` query parameter above `i64::MAX` now binds instead of being refused; send it as a decimal string to keep every digit. See [query parameters][query-parameters].

[query-parameters]: ../docs/user/queries/index.md#declarations-and-parameters
6 changes: 3 additions & 3 deletions crates/omnigraph-compiler/src/ir/coerce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ fn literal_round_trips(value: &Literal, source: Domain, target: Domain) -> bool
};
float_round_trips(value, target)
} else {
integer_round_trips(i128::from(*value), target)
integer_round_trips(*value, target)
}
}
Literal::Float(value) => {
Expand Down Expand Up @@ -460,8 +460,8 @@ mod tests {
fn literal_narrowing_is_exact_and_keeps_a_cast() {
use ScalarType::*;
for (owner, literal, source, expected) in [
(I32, Literal::Integer(i64::from(i32::MAX)), I64, I32),
(I32, Literal::Integer(i64::from(i32::MAX) + 1), I64, I64),
(I32, Literal::Integer(i128::from(i32::MAX)), I64, I32),
(I32, Literal::Integer(i128::from(i32::MAX) + 1), I64, I64),
(U32, Literal::Integer(-1), I64, I64),
(I64, Literal::Float(2_f64.powi(63)), F64, F64),
(I64, Literal::Float(-2_f64.powi(63)), F64, I64),
Expand Down
2 changes: 1 addition & 1 deletion crates/omnigraph-compiler/src/ir/erase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ mod tests {
PropType::scalar(ScalarType::String, false),
),
(
Literal::Integer(i64::MIN),
Literal::Integer(i128::from(i64::MIN)),
PropType::scalar(ScalarType::I64, false),
),
(
Expand Down
2 changes: 1 addition & 1 deletion crates/omnigraph-compiler/src/ir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,7 +875,7 @@ mod render_tests {

fn int(value: i64) -> IRExpr {
IRExpr::Literal(
Literal::Integer(value),
Literal::Integer(i128::from(value)),
crate::types::ExprType::from_prop(&crate::types::PropType::scalar(
crate::types::ScalarType::I64,
false,
Expand Down
5 changes: 4 additions & 1 deletion crates/omnigraph-compiler/src/query/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,10 @@ impl std::fmt::Display for AggFunc {
pub enum Literal {
Null,
String(String),
Integer(i64),
/// An integer value, exact for every declared integer type: `I32`, `I64`,
/// `U32` and `U64` share the one spelling, and a value is narrowed only at
/// a typed boundary, from the declared type.
Integer(i128),
Float(f64),
Bool(bool),
Date(String),
Expand Down
2 changes: 1 addition & 1 deletion crates/omnigraph-compiler/src/query/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1567,7 +1567,7 @@ fn parse_literal(pair: pest::iterators::Pair<Rule>) -> Result<Literal> {
.as_str()
.parse()
.map_err(|e| query_error_at(format!("invalid integer: {}", e), &inner))?;
Ok(Literal::Integer(n))
Ok(Literal::Integer(i128::from(n)))
}
Rule::float_lit => {
let f: f64 = inner
Expand Down
2 changes: 1 addition & 1 deletion crates/omnigraph-compiler/src/query/parser_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ fn prop(variable: &str, property: &str) -> Expr {
}

fn int(value: i64) -> Expr {
Expr::Literal(Literal::Integer(value))
Expr::Literal(Literal::Integer(i128::from(value)))
}

fn and(left: Expr, right: Expr) -> Expr {
Expand Down
131 changes: 56 additions & 75 deletions crates/omnigraph-compiler/src/query_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,82 +102,65 @@ impl ToParam for bool {

impl ToParam for i8 {
fn to_param(self) -> crate::error::Result<Literal> {
Ok(Literal::Integer(i64::from(self)))
Ok(Literal::Integer(i128::from(self)))
}
}

impl ToParam for i16 {
fn to_param(self) -> crate::error::Result<Literal> {
Ok(Literal::Integer(i64::from(self)))
Ok(Literal::Integer(i128::from(self)))
}
}

impl ToParam for i32 {
fn to_param(self) -> crate::error::Result<Literal> {
Ok(Literal::Integer(i64::from(self)))
Ok(Literal::Integer(i128::from(self)))
}
}

impl ToParam for i64 {
fn to_param(self) -> crate::error::Result<Literal> {
Ok(Literal::Integer(self))
Ok(Literal::Integer(i128::from(self)))
}
}

impl ToParam for isize {
fn to_param(self) -> crate::error::Result<Literal> {
let value = i64::try_from(self).map_err(|_| {
CompilerError::Execution(format!(
"param value {} exceeds current engine range for numeric literals (max {})",
self,
i64::MAX
))
})?;
Ok(Literal::Integer(value))
// Why the cast: Rust has no `From<isize> for i128`, and the widening
// is exact.
Ok(Literal::Integer(self as i128))
}
}

impl ToParam for u8 {
fn to_param(self) -> crate::error::Result<Literal> {
Ok(Literal::Integer(i64::from(self)))
Ok(Literal::Integer(i128::from(self)))
}
}

impl ToParam for u16 {
fn to_param(self) -> crate::error::Result<Literal> {
Ok(Literal::Integer(i64::from(self)))
Ok(Literal::Integer(i128::from(self)))
}
}

impl ToParam for u32 {
fn to_param(self) -> crate::error::Result<Literal> {
Ok(Literal::Integer(i64::from(self)))
Ok(Literal::Integer(i128::from(self)))
}
}

impl ToParam for u64 {
fn to_param(self) -> crate::error::Result<Literal> {
let value = i64::try_from(self).map_err(|_| {
CompilerError::Execution(format!(
"param value {} exceeds current engine range for numeric literals (max {})",
self,
i64::MAX
))
})?;
Ok(Literal::Integer(value))
Ok(Literal::Integer(i128::from(self)))
}
}

impl ToParam for usize {
fn to_param(self) -> crate::error::Result<Literal> {
let value = i64::try_from(self).map_err(|_| {
CompilerError::Execution(format!(
"param value {} exceeds current engine range for numeric literals (max {})",
self,
i64::MAX
))
})?;
Ok(Literal::Integer(value))
// Why the cast: Rust has no `From<usize> for i128`, and the widening
// is exact.
Ok(Literal::Integer(self as i128))
}
}

Expand Down Expand Up @@ -392,7 +375,7 @@ fn json_value_to_literal_typed(
let value = i32::try_from(value).map_err(|_| {
RunInputError::message(format!("param '{}': value {} exceeds I32", key, value))
})?;
Ok(Literal::Integer(i64::from(value)))
Ok(Literal::Integer(i128::from(value)))
}
JsonParamMode::JavaScript => {
let value = parse_i64_param(key, value, mode)?;
Expand All @@ -402,10 +385,13 @@ fn json_value_to_literal_typed(
key, value
))
})?;
Ok(Literal::Integer(i64::from(value)))
Ok(Literal::Integer(i128::from(value)))
}
},
"I64" => Ok(Literal::Integer(parse_i64_param(key, value, mode)?)),
"I64" => {
let value = parse_i64_param(key, value, mode)?;
Ok(Literal::Integer(i128::from(value)))
}
"U32" => {
let value = parse_u64_param(key, value, mode)?;
let value = match mode {
Expand All @@ -419,29 +405,11 @@ fn json_value_to_literal_typed(
))
})?,
};
Ok(Literal::Integer(i64::from(value)))
Ok(Literal::Integer(i128::from(value)))
}
"U64" => {
let value = parse_u64_param(key, value, mode)?;
let value = match mode {
JsonParamMode::Standard => i64::try_from(value).map_err(|_| {
RunInputError::message(format!(
"param '{}': value {} exceeds current engine range for U64 (max {})",
key,
value,
i64::MAX
))
})?,
JsonParamMode::JavaScript => i64::try_from(value).map_err(|_| {
RunInputError::message(format!(
"param '{}': value {} exceeds current engine range for U64 parameters (max {})",
key,
value,
i64::MAX
))
})?,
};
Ok(Literal::Integer(value))
Ok(Literal::Integer(i128::from(value)))
}
"F32" | "F64" => {
let value = value.as_f64().ok_or_else(|| match mode {
Expand Down Expand Up @@ -600,7 +568,7 @@ fn json_value_to_literal_inferred(
Value::Number(number) => match mode {
JsonParamMode::Standard => {
if let Some(value) = number.as_i64() {
Ok(Literal::Integer(value))
Ok(Literal::Integer(i128::from(value)))
} else if let Some(value) = number.as_f64() {
Ok(Literal::Float(value))
} else {
Expand All @@ -618,23 +586,15 @@ fn json_value_to_literal_inferred(
key, value
)));
}
Ok(Literal::Integer(value))
Ok(Literal::Integer(i128::from(value)))
} else if let Some(value) = number.as_u64() {
if value > JS_MAX_SAFE_INTEGER_U64 {
return Err(RunInputError::message(format!(
"param '{}': integer {} exceeds JS safe integer range; use a decimal string and a typed query parameter for exact values",
key, value
)));
}
let value = i64::try_from(value).map_err(|_| {
RunInputError::message(format!(
"param '{}': integer {} exceeds supported range (max {})",
key,
value,
i64::MAX
))
})?;
Ok(Literal::Integer(value))
Ok(Literal::Integer(i128::from(value)))
} else if let Some(value) = number.as_f64() {
Ok(Literal::Float(value))
} else {
Expand Down Expand Up @@ -917,18 +877,39 @@ mod tests {
assert!(matches!(date, Literal::Date(ref value) if value == "2026-03-06"));
}

/// Issue 918: a `U64` parameter carries its whole range; the value is
/// narrowed only at a typed boundary, from the declared type.
#[test]
fn to_param_rejects_unsigned_values_outside_engine_range() {
let error = u64::MAX.to_param().expect_err("oversized u64 should fail");
fn to_param_keeps_unsigned_values_above_the_signed_range_issue_918() {
let literal = u64::MAX.to_param().expect("u64 param");

assert_eq!(
error.to_string(),
format!(
"execution error: param value {} exceeds current engine range for numeric literals (max {})",
u64::MAX,
i64::MAX
)
);
assert!(matches!(
literal,
Literal::Integer(value) if value == i128::from(u64::MAX)
));
}

/// Issue 918: a `U64` parameter bound above `i64::MAX` binds, in the
/// decimal string form the wire uses for a 64-bit integer.
#[test]
fn a_u64_param_above_the_signed_range_binds_issue_918() {
let query = find_named_query(
"query find($id: U64) { match { $u: User } return { $u } }",
"find",
)
.expect("query should parse");

let params = json_params_to_param_map(
Some(&json!({ "id": "18446744073709551615" })),
&query.params,
JsonParamMode::Standard,
)
.expect("params should bind");

assert!(matches!(
params.get("id"),
Some(Literal::Integer(value)) if *value == i128::from(u64::MAX)
));
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/omnigraph-dst/src/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ pub fn mixed_params(str_pairs: &[(&str, &str)], int_pairs: &[(&str, i64)]) -> Pa
let mut map = params(str_pairs);
for (k, v) in int_pairs {
let key = k.strip_prefix('$').unwrap_or(k);
map.insert(key.to_string(), Literal::Integer(*v));
map.insert(key.to_string(), Literal::Integer(i128::from(*v)));
}
map
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# issue: 918
# red_on: main @ 4ba904d3; before the fix a `U64` parameter bound to 18446744073709551615 is refused with `param 'v': value 18446744073709551615 exceeds current engine range for U64 (max 9223372036854775807)`.
# notes: A `U64` parameter carries its whole range; the value is narrowed only at a typed boundary, from the declared type.

--- runner
timeout_ms: 10000
environments:
- target: omnigraph-engine
storage: local-filesystem

--- schema
node Item {
key: String @key
size: U64
}

--- seed
{"type":"Item","data":{"key":"a","size":18446744073709551615}}
{"type":"Item","data":{"key":"b","size":0}}

--- query
query item_above_the_signed_range($v: U64) {
match { $i: Item $i.size = $v }
return { $i.key }
}

--- params
{"v": 18446744073709551615}

--- expect unordered
{"i.key": "a"}

--- expect shape
i.key: String

--- query
query item_above_the_signed_range_as_text($v: U64) {
match { $i: Item $i.size = $v }
return { $i.key }
}

--- params
{"v": "18446744073709551615"}

--- expect unordered
{"i.key": "a"}

--- expect shape
i.key: String

--- mutate
query insert_item_above_the_signed_range($v: U64) {
insert Item { key: "c", size: $v }
}

--- params
{"v": 18446744073709551615}

--- expect affected: nodes=1 edges=0

--- query
query inserted_item_keeps_the_whole_value($v: U64) {
match { $i: Item $i.size = $v }
return { $i.key }
}

--- params
{"v": 18446744073709551615}

--- expect unordered
{"i.key": "a"}
{"i.key": "c"}

--- expect shape
i.key: String
Loading
Loading