From 1afc5732e04e9b8a127ac1207a278ee4fabf3b89 Mon Sep 17 00:00:00 2001 From: lab 1207 Date: Tue, 20 Oct 2026 21:57:30 +0530 Subject: [PATCH 1/4] fix(compiler): carry a U64 parameter above i64::MAX A parameter's literal carrier was a signed 64-bit integer, so a U64 parameter bound to 18446744073709551615 was refused at bind time and a U64 row above i64::MAX was unreachable. The carrier is now an i128, which holds every declared integer type with one spelling per value, and the value is narrowed only at a typed boundary, from the declared type. A saved plan keeps its i64 arm and carries a wider value as its decimal text, so existing saved plans still decode. Engine v1 narrows with i64::try_from and refuses other values, as its frozen semantics do not change; its pinned hashes move with it. --- changelog.d/u64-param-above-i64-max.fixed.md | 7 + crates/omnigraph-compiler/src/ir/coerce.rs | 6 +- crates/omnigraph-compiler/src/ir/erase.rs | 2 +- crates/omnigraph-compiler/src/ir/mod.rs | 2 +- crates/omnigraph-compiler/src/query/ast.rs | 5 +- crates/omnigraph-compiler/src/query/parser.rs | 2 +- .../src/query/parser_tests.rs | 2 +- crates/omnigraph-compiler/src/query_input.rs | 127 +++++++----------- crates/omnigraph-dst/src/fixtures.rs | 2 +- .../issue_918_u64_param_above_i64_max.gqt | 75 +++++++++++ ...e_constraint_compares_integers_exactly.gqt | 8 +- crates/omnigraph-planner/src/mirror.rs | 36 ++++- .../tests/integration/bound_plan.rs | 19 ++- .../src/projection.rs | 14 +- .../omnigraph-reference-engine/src/query.rs | 9 +- .../tests/frozen.rs | 4 +- crates/omnigraph/src/engine/constant.rs | 16 +-- .../engine/operators/subquery_aggregate.rs | 2 +- .../plan_source/scan_access/planning_cost.rs | 2 +- crates/omnigraph/src/engine/search.rs | 6 +- crates/omnigraph/src/engine/typed_value.rs | 11 +- crates/omnigraph/src/exec/mutation.rs | 45 ++++--- crates/omnigraph/tests/helpers/mod.rs | 4 +- .../tests/integration/literal_filters.rs | 6 +- docs/user/queries/index.md | 4 + 25 files changed, 277 insertions(+), 139 deletions(-) create mode 100644 changelog.d/u64-param-above-i64-max.fixed.md create mode 100644 crates/omnigraph-gqt/cases/issue_918_u64_param_above_i64_max.gqt diff --git a/changelog.d/u64-param-above-i64-max.fixed.md b/changelog.d/u64-param-above-i64-max.fixed.md new file mode 100644 index 000000000..649c2da4d --- /dev/null +++ b/changelog.d/u64-param-above-i64-max.fixed.md @@ -0,0 +1,7 @@ +- A `U64` query parameter carries its whole range: a value above `i64::MAX` + (up to `18446744073709551615`) binds instead of being refused, and a `U64` + property holding such a value is reachable. Send a 64-bit integer as a + decimal string, so the value keeps every digit. See [Query + parameters][query-parameters]. + +[query-parameters]: ../docs/user/queries/index.md#declarations-and-parameters diff --git a/crates/omnigraph-compiler/src/ir/coerce.rs b/crates/omnigraph-compiler/src/ir/coerce.rs index 4c2489241..e65538859 100644 --- a/crates/omnigraph-compiler/src/ir/coerce.rs +++ b/crates/omnigraph-compiler/src/ir/coerce.rs @@ -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) => { @@ -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), diff --git a/crates/omnigraph-compiler/src/ir/erase.rs b/crates/omnigraph-compiler/src/ir/erase.rs index db5f14d70..06111efc7 100644 --- a/crates/omnigraph-compiler/src/ir/erase.rs +++ b/crates/omnigraph-compiler/src/ir/erase.rs @@ -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), ), ( diff --git a/crates/omnigraph-compiler/src/ir/mod.rs b/crates/omnigraph-compiler/src/ir/mod.rs index b73e61703..b5adb6c6d 100644 --- a/crates/omnigraph-compiler/src/ir/mod.rs +++ b/crates/omnigraph-compiler/src/ir/mod.rs @@ -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, diff --git a/crates/omnigraph-compiler/src/query/ast.rs b/crates/omnigraph-compiler/src/query/ast.rs index 3aa19f804..12a207b38 100644 --- a/crates/omnigraph-compiler/src/query/ast.rs +++ b/crates/omnigraph-compiler/src/query/ast.rs @@ -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), diff --git a/crates/omnigraph-compiler/src/query/parser.rs b/crates/omnigraph-compiler/src/query/parser.rs index 239c5f0e0..b6ea71325 100644 --- a/crates/omnigraph-compiler/src/query/parser.rs +++ b/crates/omnigraph-compiler/src/query/parser.rs @@ -1567,7 +1567,7 @@ fn parse_literal(pair: pest::iterators::Pair) -> Result { .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 diff --git a/crates/omnigraph-compiler/src/query/parser_tests.rs b/crates/omnigraph-compiler/src/query/parser_tests.rs index b485379f1..b3d3d751c 100644 --- a/crates/omnigraph-compiler/src/query/parser_tests.rs +++ b/crates/omnigraph-compiler/src/query/parser_tests.rs @@ -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 { diff --git a/crates/omnigraph-compiler/src/query_input.rs b/crates/omnigraph-compiler/src/query_input.rs index 3e72b0385..3511ee779 100644 --- a/crates/omnigraph-compiler/src/query_input.rs +++ b/crates/omnigraph-compiler/src/query_input.rs @@ -102,82 +102,61 @@ impl ToParam for bool { impl ToParam for i8 { fn to_param(self) -> crate::error::Result { - Ok(Literal::Integer(i64::from(self))) + Ok(Literal::Integer(i128::from(self))) } } impl ToParam for i16 { fn to_param(self) -> crate::error::Result { - Ok(Literal::Integer(i64::from(self))) + Ok(Literal::Integer(i128::from(self))) } } impl ToParam for i32 { fn to_param(self) -> crate::error::Result { - Ok(Literal::Integer(i64::from(self))) + Ok(Literal::Integer(i128::from(self))) } } impl ToParam for i64 { fn to_param(self) -> crate::error::Result { - Ok(Literal::Integer(self)) + Ok(Literal::Integer(i128::from(self))) } } impl ToParam for isize { fn to_param(self) -> crate::error::Result { - 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 u8 { fn to_param(self) -> crate::error::Result { - Ok(Literal::Integer(i64::from(self))) + Ok(Literal::Integer(i128::from(self))) } } impl ToParam for u16 { fn to_param(self) -> crate::error::Result { - Ok(Literal::Integer(i64::from(self))) + Ok(Literal::Integer(i128::from(self))) } } impl ToParam for u32 { fn to_param(self) -> crate::error::Result { - Ok(Literal::Integer(i64::from(self))) + Ok(Literal::Integer(i128::from(self))) } } impl ToParam for u64 { fn to_param(self) -> crate::error::Result { - 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 { - 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))) } } @@ -392,7 +371,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)?; @@ -402,10 +381,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 { @@ -419,29 +401,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 { @@ -600,7 +564,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 { @@ -618,7 +582,7 @@ 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!( @@ -626,15 +590,7 @@ fn json_value_to_literal_inferred( 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 { @@ -917,18 +873,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] diff --git a/crates/omnigraph-dst/src/fixtures.rs b/crates/omnigraph-dst/src/fixtures.rs index 8529f6085..1b57bbb4d 100644 --- a/crates/omnigraph-dst/src/fixtures.rs +++ b/crates/omnigraph-dst/src/fixtures.rs @@ -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 } diff --git a/crates/omnigraph-gqt/cases/issue_918_u64_param_above_i64_max.gqt b/crates/omnigraph-gqt/cases/issue_918_u64_param_above_i64_max.gqt new file mode 100644 index 000000000..b9449390e --- /dev/null +++ b/crates/omnigraph-gqt/cases/issue_918_u64_param_above_i64_max.gqt @@ -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 diff --git a/crates/omnigraph-gqt/cases/range_constraint_compares_integers_exactly.gqt b/crates/omnigraph-gqt/cases/range_constraint_compares_integers_exactly.gqt index 1a9f3b54e..0ad4c491a 100644 --- a/crates/omnigraph-gqt/cases/range_constraint_compares_integers_exactly.gqt +++ b/crates/omnigraph-gqt/cases/range_constraint_compares_integers_exactly.gqt @@ -4,7 +4,7 @@ # notes: 9007199254740992 to the same double, and above i64::MAX a U64 value and an I64 bound round to the same 2^63. # notes: A Float bound on an integer column, and an Integer bound on a float column, compare against the bound's exact value. # notes: A list property's @range bounds each element; the loader used to skip list columns without checking them. -# notes: U64 values above i64::MAX reach @range through a load; mutation parameters stop at i64::MAX. +# notes: U64 values above i64::MAX reach @range through a load and through a mutation parameter. # notes: Upper-only bounds are maxima for scalar and list properties; omitted minima permit negative values. # notes: The seed loads each boundary value; the final read shows that every refused write stored nothing. @@ -67,6 +67,12 @@ tables: unsigned: {kind: literal, value: 9223372036854775808} --- expect error: @range violation on Counter.unsigned: value 9223372036854775808 > max 9223372036854775807 +--- mutate +query unsigned_above_max_by_parameter($unsigned: U64) { insert Counter { name: "unsigned_above_by_param", unsigned: $unsigned } } +--- params +{"unsigned": 9223372036854775808} +--- expect error: @range violation on Counter.unsigned: value 9223372036854775808 > max 9223372036854775807 + --- mutate query small_below_float_min($small: I32) { insert Counter { name: "small_below", small: $small } } --- params diff --git a/crates/omnigraph-planner/src/mirror.rs b/crates/omnigraph-planner/src/mirror.rs index 95d45998f..997537197 100644 --- a/crates/omnigraph-planner/src/mirror.rs +++ b/crates/omnigraph-planner/src/mirror.rs @@ -17,7 +17,8 @@ use omnigraph_compiler::types::Direction; use omnigraph_compiler::{ SYSTEM_COLUMNS_LEGACY, SYSTEM_COLUMNS_META, SYSTEM_COLUMNS_V3, SystemColumns, }; -use serde::{Deserialize, Serialize}; +use serde::de::Error as _; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::aggregate::AggregateSpec; use crate::bound::{BoundPlan, ValueTable}; @@ -1374,12 +1375,37 @@ impl From for Direction { } } +/// An integer outside the `i64` range, carried as its decimal text. +/// +/// The pinned serde buffers no `i128` value, so a wide integer travels as a +/// string. A text that is not an integer refuses the decode instead of +/// becoming another value. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WideInteger(i128); + +impl Serialize for WideInteger { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.0.to_string()) + } +} + +impl<'de> Deserialize<'de> for WideInteger { + fn deserialize>(deserializer: D) -> Result { + let text = String::deserialize(deserializer)?; + text.parse::() + .map(WideInteger) + .map_err(D::Error::custom) + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "literal", content = "value", rename_all = "snake_case")] pub enum LiteralMirror { Null, String(String), Integer(i64), + /// An integer outside the `i64` range, as its decimal text. + WideInteger(WideInteger), Float(f64), Bool(bool), Date(String), @@ -1392,7 +1418,10 @@ impl From<&Literal> for LiteralMirror { match literal { Literal::Null => Self::Null, Literal::String(text) => Self::String(text.clone()), - Literal::Integer(value) => Self::Integer(*value), + Literal::Integer(value) => match i64::try_from(*value) { + Ok(value) => Self::Integer(value), + Err(_) => Self::WideInteger(WideInteger(*value)), + }, Literal::Float(value) => Self::Float(*value), Literal::Bool(value) => Self::Bool(*value), Literal::Date(text) => Self::Date(text.clone()), @@ -1407,7 +1436,8 @@ impl From for Literal { match mirror { LiteralMirror::Null => Self::Null, LiteralMirror::String(text) => Self::String(text), - LiteralMirror::Integer(value) => Self::Integer(value), + LiteralMirror::Integer(value) => Self::Integer(i128::from(value)), + LiteralMirror::WideInteger(value) => Self::Integer(value.0), LiteralMirror::Float(value) => Self::Float(value), LiteralMirror::Bool(value) => Self::Bool(value), LiteralMirror::Date(text) => Self::Date(text), diff --git a/crates/omnigraph-planner/tests/integration/bound_plan.rs b/crates/omnigraph-planner/tests/integration/bound_plan.rs index 7afc4cf9a..e613861a3 100644 --- a/crates/omnigraph-planner/tests/integration/bound_plan.rs +++ b/crates/omnigraph-planner/tests/integration/bound_plan.rs @@ -6,7 +6,7 @@ use std::collections::BTreeMap; use std::sync::Arc; use arrow_schema::{DataType, Field, Schema}; -use omnigraph_compiler::ir::{IRExpr, IROp, IROrdering, IRProjection, QueryIR}; +use omnigraph_compiler::ir::{IRExpr, IROp, IROrdering, IRProjection, ParamMap, QueryIR}; use omnigraph_compiler::query::ast::{CompOp, Literal}; use omnigraph_compiler::{ExprType, PropType, SYSTEM_COLUMNS_V3, ScalarType}; use omnigraph_planner::{ @@ -199,6 +199,23 @@ fn saved_plan_version_refuses_both_legacy_and_future_readers() { assert!(serde_json::from_value::(encoded).is_err()); } +#[test] +fn a_wide_bound_value_reads_back_exactly_issue_918() { + let bound = BoundPlan { + plan: plan(prop("d", "slug")), + values: ValueTable { + params: Arc::new(ParamMap::from([ + ("wide".to_string(), Literal::Integer(i128::from(u64::MAX))), + ("low".to_string(), Literal::Integer(i128::from(i64::MIN))), + ])), + vectors: BTreeMap::new(), + }, + }; + let text = serde_json::to_string(&bound).expect("the bound plan serializes"); + assert!(text.contains("18446744073709551615"), "{text}"); + assert_eq!(round_trip(&bound), bound); +} + #[test] fn fused_saved_node_refuses_a_reader_without_declared_row_keys() { let arm = IRExpr::Bm25 { diff --git a/crates/omnigraph-reference-engine/src/projection.rs b/crates/omnigraph-reference-engine/src/projection.rs index 80f60e2db..8b1d947e2 100644 --- a/crates/omnigraph-reference-engine/src/projection.rs +++ b/crates/omnigraph-reference-engine/src/projection.rs @@ -160,7 +160,10 @@ pub(super) fn literal_to_array(lit: &Literal, num_rows: usize) -> Result Arc::new(StringArray::from(vec![s.as_str(); num_rows])) as ArrayRef, Literal::Integer(n) => { // Try to match the most common integer types - Arc::new(Int64Array::from(vec![*n; num_rows])) as ArrayRef + let value = i64::try_from(*n).map_err(|_| { + OmniError::manifest("integer literal is outside the 64-bit range".to_string()) + })?; + Arc::new(Int64Array::from(vec![value; num_rows])) as ArrayRef } Literal::Float(f) => Arc::new(Float64Array::from(vec![*f; num_rows])) as ArrayRef, Literal::Bool(b) => Arc::new(BooleanArray::from(vec![*b; num_rows])) as ArrayRef, @@ -324,7 +327,14 @@ fn literal_list_to_array(items: &[Literal], num_rows: usize) -> Result for _ in 0..num_rows { for item in items { match item { - Literal::Integer(value) => builder.values().append_value(*value), + Literal::Integer(value) => { + let value = i64::try_from(*value).map_err(|_| { + OmniError::manifest( + "integer literal is outside the 64-bit range".to_string(), + ) + })?; + builder.values().append_value(value); + } _ => builder.values().append_null(), } } diff --git a/crates/omnigraph-reference-engine/src/query.rs b/crates/omnigraph-reference-engine/src/query.rs index 2e54abb65..6c3bd59d5 100644 --- a/crates/omnigraph-reference-engine/src/query.rs +++ b/crates/omnigraph-reference-engine/src/query.rs @@ -4460,12 +4460,13 @@ fn resolve_to_string(expr: &IRExpr, params: &ParamMap) -> Option { } } -/// Resolve an expression to an integer value (literal or param). +/// Resolve an expression to an integer value (literal or param). A value +/// outside v1's `i64` range has no v1 meaning and resolves to nothing. fn resolve_to_int(expr: &IRExpr, params: &ParamMap) -> Option { match expr { - IRExpr::Literal(Literal::Integer(n)) => Some(*n), + IRExpr::Literal(Literal::Integer(n)) => i64::try_from(*n).ok(), IRExpr::Param(name) => match params.get(name)? { - Literal::Integer(n) => Some(*n), + Literal::Integer(n) => i64::try_from(*n).ok(), _ => None, }, _ => None, @@ -4703,7 +4704,7 @@ fn literal_to_expr(lit: &Literal) -> Option { Some(match lit { Literal::Null => df_lit(datafusion::scalar::ScalarValue::Null), Literal::String(s) => df_lit(s.clone()), - Literal::Integer(n) => df_lit(*n), + Literal::Integer(n) => df_lit(i64::try_from(*n).ok()?), Literal::Float(f) => df_lit(*f), Literal::Bool(b) => df_lit(*b), // Date/DateTime pass through as strings here. Against a typed Date diff --git a/crates/omnigraph-reference-engine/tests/frozen.rs b/crates/omnigraph-reference-engine/tests/frozen.rs index cd7956a29..5412e96ee 100644 --- a/crates/omnigraph-reference-engine/tests/frozen.rs +++ b/crates/omnigraph-reference-engine/tests/frozen.rs @@ -46,11 +46,11 @@ const FROZEN: &[(&str, &str)] = &[ ), ( "projection.rs", - "be11d4ce201b212c0e8e2963a5ad5f0e3b8cd874e033eefef24874f02d6a4631", + "98c6599a042a8eb79a7cb19a25812b48135f2bbcf077c4bf9c4fb6141d25f9be", ), ( "query.rs", - "97ffde90e498a9b1568567cd1cc3dc2d284e9111360ce85abacb1a0b2341c1a1", + "1b70d771d32f1c3e2b848aa2f37f2c4c17c244e062db6f98743b0099c16c5602", ), ( "table_store.rs", diff --git a/crates/omnigraph/src/engine/constant.rs b/crates/omnigraph/src/engine/constant.rs index 3ce5304bd..928f2552f 100644 --- a/crates/omnigraph/src/engine/constant.rs +++ b/crates/omnigraph/src/engine/constant.rs @@ -87,14 +87,10 @@ fn literal_from_scalar(value: ScalarValue) -> Result { Ok(match value { ScalarValue::Boolean(Some(value)) => Literal::Bool(value), ScalarValue::Utf8(Some(value)) => Literal::String(value), - ScalarValue::Int32(Some(value)) => Literal::Integer(i64::from(value)), - ScalarValue::Int64(Some(value)) => Literal::Integer(value), - ScalarValue::UInt32(Some(value)) => Literal::Integer(i64::from(value)), - ScalarValue::UInt64(Some(value)) => { - Literal::Integer(i64::try_from(value).map_err(|_| { - OmniError::manifest("constant U64 result exceeds the public literal range") - })?) - } + ScalarValue::Int32(Some(value)) => Literal::Integer(i128::from(value)), + ScalarValue::Int64(Some(value)) => Literal::Integer(i128::from(value)), + ScalarValue::UInt32(Some(value)) => Literal::Integer(i128::from(value)), + ScalarValue::UInt64(Some(value)) => Literal::Integer(i128::from(value)), ScalarValue::Float32(Some(value)) => Literal::Float(f64::from(value)), ScalarValue::Float64(Some(value)) => Literal::Float(value), ScalarValue::Date32(Some(value)) => { @@ -273,7 +269,7 @@ mod tests { ty: exact.clone(), }; let signed = cast(IRExpr::Literal( - Literal::Integer(i64::MAX), + Literal::Integer(i128::from(i64::MAX)), scalar(ScalarType::I64, false), )); let unsigned = cast(IRExpr::Param("wide".into(), scalar(ScalarType::U64, false))); @@ -302,7 +298,7 @@ mod tests { let list = IRExpr::Cast { expr: Box::new(IRExpr::Literal( - Literal::List(vec![Literal::Integer(i64::MAX), Literal::Null]), + Literal::List(vec![Literal::Integer(i128::from(i64::MAX)), Literal::Null]), ExprType::from_prop(&PropType::list_of(ScalarType::I64, false)), )), ty: ExprType::ExactInteger { diff --git a/crates/omnigraph/src/engine/operators/subquery_aggregate.rs b/crates/omnigraph/src/engine/operators/subquery_aggregate.rs index 5f9232846..731279823 100644 --- a/crates/omnigraph/src/engine/operators/subquery_aggregate.rs +++ b/crates/omnigraph/src/engine/operators/subquery_aggregate.rs @@ -593,7 +593,7 @@ mod tests { let mut exact = SubqueryAggregate::new( &exact_predicate, plan_block_aggregate(&exact_predicate.left).unwrap(), - &ParamMap::from([("bound".into(), Literal::Integer(i64::MAX))]), + &ParamMap::from([("bound".into(), Literal::Integer(i128::from(i64::MAX)))]), 1, ) .unwrap(); diff --git a/crates/omnigraph/src/engine/plan_source/scan_access/planning_cost.rs b/crates/omnigraph/src/engine/plan_source/scan_access/planning_cost.rs index 12b18d4bd..aa1b92f57 100644 --- a/crates/omnigraph/src/engine/plan_source/scan_access/planning_cost.rs +++ b/crates/omnigraph/src/engine/plan_source/scan_access/planning_cost.rs @@ -360,7 +360,7 @@ async fn measure( ty: ty.clone(), }, CompOp::Eq, - IRExpr::Literal(Literal::Integer(value), ty), + IRExpr::Literal(Literal::Integer(i128::from(value)), ty), )]; let params = ParamMap::new(); let read = diff --git a/crates/omnigraph/src/engine/search.rs b/crates/omnigraph/src/engine/search.rs index 231ce9bba..f14ec84cb 100644 --- a/crates/omnigraph/src/engine/search.rs +++ b/crates/omnigraph/src/engine/search.rs @@ -1405,7 +1405,7 @@ mod typed_search_value_tests { fn widened_integer(value: i64) -> IRExpr { IRExpr::Cast { expr: Box::new(IRExpr::Literal( - Literal::Integer(value), + Literal::Integer(i128::from(value)), scalar(ScalarType::I32, false), )), ty: scalar(ScalarType::I64, false), @@ -1473,7 +1473,7 @@ mod typed_search_value_tests { IRExpr::Literal(Literal::Null, scalar(ScalarType::I64, true)), IRExpr::Literal(Literal::Integer(-1), scalar(ScalarType::I64, false)), IRExpr::Literal( - Literal::Integer(i64::from(u32::MAX) + 1), + Literal::Integer(i128::from(u32::MAX) + 1), scalar(ScalarType::I64, false), ), IRExpr::Param("missing".into(), scalar(ScalarType::I64, false)), @@ -1511,7 +1511,7 @@ mod typed_search_value_tests { #[test] fn nearest_vector_conversion_preserves_the_recorded_list_domain() { - let value = (1_i64 << 62) + (1_i64 << 38) + 1; + let value = (1_i128 << 62) + (1_i128 << 38) + 1; let expected = 2_f32.powi(62); assert!(value as f32 > expected); let literal = Literal::List(vec![Literal::Integer(value)]); diff --git a/crates/omnigraph/src/engine/typed_value.rs b/crates/omnigraph/src/engine/typed_value.rs index 582aff51d..bc3df2cd9 100644 --- a/crates/omnigraph/src/engine/typed_value.rs +++ b/crates/omnigraph/src/engine/typed_value.rs @@ -224,7 +224,7 @@ fn typed_scalar(literal: &Literal, scalar: ScalarType) -> Result { _ => integer(-2_f64.powi(31), 2_f64.powi(31))? as i32, })), ScalarType::I64 => ScalarValue::Int64(Some(match literal { - Literal::Integer(value) => *value, + Literal::Integer(value) => i64::try_from(*value).map_err(|_| out_of_range())?, _ => integer(-2_f64.powi(63), 2_f64.powi(63))? as i64, })), ScalarType::U32 => ScalarValue::UInt32(Some(match literal { @@ -956,7 +956,7 @@ mod tests { #[test] fn direct_parameter_values_are_checked_even_without_a_consumer() { let invalid = [ - (ScalarType::I32, Literal::Integer(i64::from(i32::MAX) + 1)), + (ScalarType::I32, Literal::Integer(i128::from(i32::MAX) + 1)), (ScalarType::U32, Literal::Integer(-1)), (ScalarType::U64, Literal::Integer(-1)), (ScalarType::I64, Literal::Float(2_f64.powi(63))), @@ -995,9 +995,10 @@ mod tests { } } for (kind, value) in [ - (ScalarType::I64, Literal::Integer(i64::MAX)), - (ScalarType::I64, Literal::Integer(i64::MIN)), - (ScalarType::U32, Literal::Integer(i64::from(u32::MAX))), + (ScalarType::I64, Literal::Integer(i128::from(i64::MAX))), + (ScalarType::I64, Literal::Integer(i128::from(i64::MIN))), + (ScalarType::U32, Literal::Integer(i128::from(u32::MAX))), + (ScalarType::U64, Literal::Integer(i128::from(u64::MAX))), (ScalarType::F32, Literal::Float(f64::from(f32::MAX))), ] { assert!(typed_literal_to_array(&value, &scalar(kind, false), 0).is_ok()); diff --git a/crates/omnigraph/src/exec/mutation.rs b/crates/omnigraph/src/exec/mutation.rs index fef3f826a..0624f1fe9 100644 --- a/crates/omnigraph/src/exec/mutation.rs +++ b/crates/omnigraph/src/exec/mutation.rs @@ -57,7 +57,12 @@ fn literal_to_typed_array( })?; Arc::new(Int32Array::from(vec![value; num_rows])) } - (Literal::Integer(n), DataType::Int64) => Arc::new(Int64Array::from(vec![*n; num_rows])), + (Literal::Integer(n), DataType::Int64) => { + let value = i64::try_from(*n).map_err(|_| { + OmniError::manifest(format!("integer value {n} exceeds Int64 range")) + })?; + Arc::new(Int64Array::from(vec![value; num_rows])) + } (Literal::Integer(n), DataType::UInt32) => { let value = u32::try_from(*n).map_err(|_| { OmniError::manifest(format!("integer value {n} exceeds UInt32 range")) @@ -220,7 +225,15 @@ fn typed_list_literal_to_array( for _ in 0..num_rows { for item in items { match item { - Literal::Integer(value) => builder.values().append_value(*value), + Literal::Integer(value) => { + let value = i64::try_from(*value).map_err(|_| { + OmniError::manifest(format!( + "list value {} exceeds Int64 range", + value + )) + })?; + builder.values().append_value(value); + } _ => builder.values().append_null(), } } @@ -2082,21 +2095,19 @@ mod literal_narrowing_tests { #[test] fn scalar_narrowing_accepts_boundaries_and_rejects_wraparound() { - assert!( - literal_to_typed_array(&Literal::Integer(i32::MAX as i64), &DataType::Int32, 1).is_ok() - ); - assert!( - literal_to_typed_array(&Literal::Integer(i32::MAX as i64 + 1), &DataType::Int32, 1,) - .is_err() - ); - assert!( - literal_to_typed_array(&Literal::Integer(u32::MAX as i64), &DataType::UInt32, 1) - .is_ok() - ); - assert!( - literal_to_typed_array(&Literal::Integer(u32::MAX as i64 + 1), &DataType::UInt32, 1,) - .is_err() - ); + let int = |value: i128| Literal::Integer(value); + let int32_max = int(i32::MAX as i128); + let int32_over = int(i32::MAX as i128 + 1); + let uint32_max = int(u32::MAX as i128); + let uint32_over = int(u32::MAX as i128 + 1); + let uint64_max = int(u64::MAX as i128); + let uint64_over = int(u64::MAX as i128 + 1); + assert!(literal_to_typed_array(&int32_max, &DataType::Int32, 1).is_ok()); + assert!(literal_to_typed_array(&int32_over, &DataType::Int32, 1).is_err()); + assert!(literal_to_typed_array(&uint32_max, &DataType::UInt32, 1).is_ok()); + assert!(literal_to_typed_array(&uint32_over, &DataType::UInt32, 1).is_err()); + assert!(literal_to_typed_array(&uint64_max, &DataType::UInt64, 1).is_ok()); + assert!(literal_to_typed_array(&uint64_over, &DataType::UInt64, 1).is_err()); assert!(literal_to_typed_array(&Literal::Integer(-1), &DataType::UInt32, 1).is_err()); assert!(literal_to_typed_array(&Literal::Integer(-1), &DataType::UInt64, 1).is_err()); } diff --git a/crates/omnigraph/tests/helpers/mod.rs b/crates/omnigraph/tests/helpers/mod.rs index c39350972..9a9395261 100644 --- a/crates/omnigraph/tests/helpers/mod.rs +++ b/crates/omnigraph/tests/helpers/mod.rs @@ -709,7 +709,7 @@ pub fn int_params(pairs: &[(&str, i64)]) -> ParamMap { .iter() .map(|(k, v)| { let key = k.strip_prefix('$').unwrap_or(k); - (key.to_string(), Literal::Integer(*v)) + (key.to_string(), Literal::Integer(i128::from(*v))) }) .collect() } @@ -719,7 +719,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 } diff --git a/crates/omnigraph/tests/integration/literal_filters.rs b/crates/omnigraph/tests/integration/literal_filters.rs index e493037db..7436b0651 100644 --- a/crates/omnigraph/tests/integration/literal_filters.rs +++ b/crates/omnigraph/tests/integration/literal_filters.rs @@ -671,7 +671,7 @@ async fn unused_mutation_parameters_are_checked_before_a_noop() { for (type_name, invalid, valid) in [ ( "I32", - Literal::Integer(i64::from(i32::MAX) + 1), + Literal::Integer(i128::from(i32::MAX) + 1), Literal::Integer(1), ), ( @@ -725,7 +725,7 @@ async fn mutation_assignment_parameter_is_checked_before_an_empty_scan_after_hea let source = r#"query assigned($value: I32) { update Metric set { count: $value } where name = "missing" }"#; - let params = ParamMap::from([("value".into(), Literal::Integer(i64::from(i32::MAX) + 1))]); + let params = ParamMap::from([("value".into(), Literal::Integer(i128::from(i32::MAX) + 1))]); let error = db .mutate_as_with_expected_head( "main", @@ -755,7 +755,7 @@ async fn mutation_assignment_parameter_is_checked_before_an_empty_scan_after_hea "main", source, "assigned", - &ParamMap::from([("value".into(), Literal::Integer(i64::from(i32::MAX)))]), + &ParamMap::from([("value".into(), Literal::Integer(i128::from(i32::MAX)))]), ) .await .unwrap(); diff --git a/docs/user/queries/index.md b/docs/user/queries/index.md index 06a578f24..6db737be0 100644 --- a/docs/user/queries/index.md +++ b/docs/user/queries/index.md @@ -42,6 +42,10 @@ parameter must be supplied; omission fails before query execution. A trailing `@instruction("...")` attach metadata for clients that expose stored queries as tools. +A parameter carries the whole range of its declared type: a `U64` parameter +takes every value from `0` to `18446744073709551615`. Send a 64-bit integer +as a decimal string, so a value past 2^53 keeps every digit. + ## Match patterns A match binds nodes and follows typed relationships. For edge alternatives, From 1548786aa1898f6a6c6b62d9bd04980151156a55 Mon Sep 17 00:00:00 2001 From: lab 1207 Date: Wed, 21 Oct 2026 17:48:45 +0530 Subject: [PATCH 2/4] fix(compiler): widen the isize and usize parameters to i128 Rust has no `From for i128` and no `From for i128`, so the two `ToParam` implementations did not compile and the compiler crate could not build. The widening is exact, so cast instead. --- crates/omnigraph-compiler/src/query_input.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/omnigraph-compiler/src/query_input.rs b/crates/omnigraph-compiler/src/query_input.rs index 3511ee779..93e2fbfb0 100644 --- a/crates/omnigraph-compiler/src/query_input.rs +++ b/crates/omnigraph-compiler/src/query_input.rs @@ -126,7 +126,9 @@ impl ToParam for i64 { impl ToParam for isize { fn to_param(self) -> crate::error::Result { - Ok(Literal::Integer(i128::from(self))) + // Why the cast: Rust has no `From for i128`, and the widening + // is exact. + Ok(Literal::Integer(self as i128)) } } @@ -156,7 +158,9 @@ impl ToParam for u64 { impl ToParam for usize { fn to_param(self) -> crate::error::Result { - Ok(Literal::Integer(i128::from(self))) + // Why the cast: Rust has no `From for i128`, and the widening + // is exact. + Ok(Literal::Integer(self as i128)) } } From 1f96a926aac54ca07a3259cb341ead9a8c5e0bc1 Mon Sep 17 00:00:00 2001 From: lab 1207 Date: Thu, 22 Oct 2026 11:50:25 +0530 Subject: [PATCH 3/4] fix(docs): keep the query guide inside its line cap `scripts/check-docs.py` refuses an authored user page over 350 lines, and `docs/user/queries/index.md` was already exactly at the cap, so the new paragraph about a `U64` parameter's range pushed it to 354 and failed the Check AGENTS.md Links job. Each fact now sits in the paragraph that already covers its subject: the parameter's range joins the declarations paragraph, the decimal-string advice joins the JSON input paragraph, and the two pointer sentences that stood alone as their own paragraphs close the paragraph they belong with. No sentence is dropped. Also bump the saved-plan envelope to version 11. The `wide_integer` arm is a new wire form, and version 10 has to name one schema: an older reader that meets the arm now refuses on the version before it reaches the arm, and says so. --- .../u64-param-above-i64-max.breaking.md | 1 + crates/omnigraph-planner/src/bound.rs | 5 ++-- docs/dev/execution.md | 3 ++- docs/user/queries/explain.md | 2 +- docs/user/queries/index.md | 23 ++++++++----------- 5 files changed, 16 insertions(+), 18 deletions(-) create mode 100644 changelog.d/u64-param-above-i64-max.breaking.md diff --git a/changelog.d/u64-param-above-i64-max.breaking.md b/changelog.d/u64-param-above-i64-max.breaking.md new file mode 100644 index 000000000..3840d26b4 --- /dev/null +++ b/changelog.d/u64-param-above-i64-max.breaking.md @@ -0,0 +1 @@ +- 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. diff --git a/crates/omnigraph-planner/src/bound.rs b/crates/omnigraph-planner/src/bound.rs index c8384eeb0..e8a0fdb1d 100644 --- a/crates/omnigraph-planner/src/bound.rs +++ b/crates/omnigraph-planner/src/bound.rs @@ -23,8 +23,9 @@ use crate::physical::{NodeId, PhysicalPlan}; /// coverage. Version 9 adds to version 8 every `AntiJoin`'s `strategy` /// (`correlated` or `key_set`). Version 10 adds to version 9 every table's /// `source` (`dataset` or `catalogue`) and the assumptions' catalogue -/// fingerprints. -pub const BOUND_PLAN_VERSION: u32 = 10; +/// fingerprints. Version 11 adds to version 10 a literal arm for an integer +/// outside the `i64` range, carried as its decimal text. +pub const BOUND_PLAN_VERSION: u32 = 11; /// The envelope omits top-level `plan` and `values` so unversioned readers reject it. #[derive(Serialize, Deserialize)] diff --git a/docs/dev/execution.md b/docs/dev/execution.md index 0ae415116..496dc5945 100644 --- a/docs/dev/execution.md +++ b/docs/dev/execution.md @@ -147,7 +147,8 @@ when opened; empty input need not open it. Bound plans use a versioned envelope and require regeneration when the stored version is unsupported. Envelope version 7 requires aggregate expressions to carry their `distinct` flag; version 8 types each node's ordering and declares ranked scans' -eligibility and policy; version 9 records every `AntiJoin`'s `strategy`. +eligibility and policy; version 9 records every `AntiJoin`'s `strategy`; +version 11 carries an integer outside the `i64` range as its decimal text. Plans saved with earlier binaries must be regenerated before replay. ### Expand path selection diff --git a/docs/user/queries/explain.md b/docs/user/queries/explain.md index d689e9844..f3b49f303 100644 --- a/docs/user/queries/explain.md +++ b/docs/user/queries/explain.md @@ -239,7 +239,7 @@ documents only before it) carry a pipeline `id` and `depends_on`, with `physical_plan` node ids; an operator's emission is set by the shape wrapper the engine builds its stream through, the one the planner's cut names, so the printed pipelines are the executed ones. Saved physical -plans use a `bound_plan_version: 9` envelope whose `body` contains `plan` and +plans use a `bound_plan_version: 11` envelope whose `body` contains `plan` and `values`. Unversioned plans and other versions are refused with a regeneration instruction, including plans without traversal nodes. diff --git a/docs/user/queries/index.md b/docs/user/queries/index.md index 6db737be0..a74a4e2a8 100644 --- a/docs/user/queries/index.md +++ b/docs/user/queries/index.md @@ -40,11 +40,8 @@ Parameter types use the schema scalar types. Every non-nullable declared parameter must be supplied; omission fails before query execution. A trailing `?` accepts `null` or an omitted value. `@description("...")` and `@instruction("...")` attach metadata for clients that expose stored queries -as tools. - -A parameter carries the whole range of its declared type: a `U64` parameter -takes every value from `0` to `18446744073709551615`. Send a 64-bit integer -as a decimal string, so a value past 2^53 keeps every digit. +as tools. An integer parameter carries the whole range of its declared type, +so a `U64` parameter takes every value from `0` to `18446744073709551615`. ## Match patterns @@ -168,9 +165,8 @@ query prolific($least: I64) { `NULL` never matches these predicates. `%` and `_` are ordinary characters, not wildcards. The predicates remain correct without an index; do not assume a -free-text String index accelerates exact prefix or substring filters. - -Use [full-text search](../search/index.md) for tokenization, fuzzy matching, and +free-text String index accelerates exact prefix or substring filters. Use +[full-text search](../search/index.md) for tokenization, fuzzy matching, and relevance ranking. ### System fields @@ -276,9 +272,7 @@ includes its `graph_commit_id`, pinned with the returned rows. Use that same-snapshot id with a later mutation's `--if-commit` option when implementing read-modify-write. On a newly created, unmodified branch, it is the head inherited from the source branch and is valid for the branch's first -conditional mutation. - -See [Branches, Commits, and History](../branching/index.md). +conditional mutation. See [Branches, Commits, and History](../branching/index.md). ## JSON result spelling @@ -297,9 +291,10 @@ its own. The spellings a consumer sees: (`1.0e20`, `1.0e-7`); a non-finite computed value is `null`. - `Vector(N)` and list properties are JSON arrays. -On input, a JSON number for an `F64` parses to the nearest `F64` value, the one -a GQ literal with the same digits names, so an `F64` value read from `rows` -loads back unchanged. A `Date` string is a calendar day, `"2024-01-01"`; a +Send a 64-bit integer above 2^53 as a decimal string, so a client that parses +JSON through a double keeps every digit. On input, a JSON number for an `F64` +parses to the nearest `F64` value, the one a GQ literal with the same digits +names, so an `F64` value read from `rows` loads back unchanged. A `Date` string is a calendar day, `"2024-01-01"`; a string that carries a time of day, such as `"2024-01-01T02:00:00+05:00"`, is refused as a load value, a param, or a `date(...)` literal, and an instant belongs in a `DateTime` property. A `DateTime` holds milliseconds: a string From 076f17ae8647a5133be6ae2428e08a4f6161aa35 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 30 Oct 2026 20:44:07 -0700 Subject: [PATCH 4/4] docs(changelog): fit the U64 parameter notes to the release-note check --- changelog.d/u64-param-above-i64-max.breaking.md | 4 +++- changelog.d/u64-param-above-i64-max.fixed.md | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/changelog.d/u64-param-above-i64-max.breaking.md b/changelog.d/u64-param-above-i64-max.breaking.md index 3840d26b4..7916526ed 100644 --- a/changelog.d/u64-param-above-i64-max.breaking.md +++ b/changelog.d/u64-param-above-i64-max.breaking.md @@ -1 +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. +- 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 diff --git a/changelog.d/u64-param-above-i64-max.fixed.md b/changelog.d/u64-param-above-i64-max.fixed.md index 649c2da4d..9eb72ad29 100644 --- a/changelog.d/u64-param-above-i64-max.fixed.md +++ b/changelog.d/u64-param-above-i64-max.fixed.md @@ -1,7 +1,3 @@ -- A `U64` query parameter carries its whole range: a value above `i64::MAX` - (up to `18446744073709551615`) binds instead of being refused, and a `U64` - property holding such a value is reachable. Send a 64-bit integer as a - decimal string, so the value keeps every digit. See [Query - parameters][query-parameters]. +- 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