From a1057f75601a18a692b4ca38c61287a7cf027540 Mon Sep 17 00:00:00 2001 From: discord9 <55937128+discord9@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:40:01 +0800 Subject: [PATCH 1/4] refactor: discard unused bounds for empty range intersections Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- .../src/extension_plan/range_manipulate.rs | 86 ++++++++++++++++--- 1 file changed, 75 insertions(+), 11 deletions(-) diff --git a/src/promql/src/extension_plan/range_manipulate.rs b/src/promql/src/extension_plan/range_manipulate.rs index 99d39e6584..2e4a1d00e3 100644 --- a/src/promql/src/extension_plan/range_manipulate.rs +++ b/src/promql/src/extension_plan/range_manipulate.rs @@ -761,16 +761,7 @@ impl RangeManipulateStream { let start = query_start.max(first_ts_aligned); let end = query_end.min(last_ts_aligned); if start > end { - let bounds = if start >= i64::MIN as i128 - && start <= i64::MAX as i128 - && end >= i64::MIN as i128 - && end <= i64::MAX as i128 - { - (start as i64, end as i64) - } else { - (self.start, self.end) - }; - return Ok((vec![], bounds)); + return Ok((vec![], (self.start, self.end))); } // The intersection is within the declared i64 query bounds. let start = start as i64; @@ -1508,6 +1499,68 @@ mod test { } } + #[tokio::test] + async fn no_intersection_batch_is_skipped_and_stream_continues() { + let schema = Arc::new(Schema::new(vec![ + Field::new( + TIME_INDEX_COLUMN, + TimestampMillisecondType::DATA_TYPE, + false, + ), + Field::new("value", DataType::Float64, false), + ])); + let input = LogicalPlan::EmptyRelation(EmptyRelation { + produce_one_row: false, + schema: schema.clone().to_dfschema_ref().unwrap(), + }); + let plan = RangeManipulate::new( + 0, + 50, + 10, + 1, + TIME_INDEX_COLUMN.to_string(), + vec!["value".to_string()], + input, + ) + .unwrap(); + let no_intersection = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(TimestampMillisecondArray::from(vec![100])), + Arc::new(Float64Array::from(vec![1.0])), + ], + ) + .unwrap(); + let intersection = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(TimestampMillisecondArray::from(vec![20])), + Arc::new(Float64Array::from(vec![2.0])), + ], + ) + .unwrap(); + let input = Arc::new(DataSourceExec::new(Arc::new( + MemorySourceConfig::try_new(&[vec![no_intersection, intersection]], schema, None) + .unwrap(), + ))); + + let batches = datafusion::physical_plan::collect( + plan.to_execution_plan(input), + SessionContext::default().task_ctx(), + ) + .await + .unwrap(); + + assert_eq!(batches.len(), 1); + assert_eq!(batches[0].num_rows(), 1); + let timestamps = batches[0] + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(timestamps.value(0), 20); + } + fn calculate_range_for_test( query_start: i64, query_end: i64, @@ -1634,7 +1687,7 @@ mod test { 10, 0, vec![100], - (100, 50), + (0, 50), ), ]; @@ -1772,6 +1825,17 @@ mod test { let (actual, (start, end)) = calculate_range_for_test(query_start, query_end, interval, range, ×tamps); let expected = calculate_range_oracle(×tamps, start, end, interval, range); + let expected = if actual.is_empty() && !expected.is_empty() { + assert!( + expected.iter().all(|(_, len)| *len == 0), + "case={case}, timestamps={timestamps:?}, query=({query_start}, {query_end}), \ + interval={interval}, range={range}, bounds=({start}, {end}): \ + no-intersection output must have no selected samples" + ); + vec![] + } else { + expected + }; assert_eq!( actual, expected, "case={case}, timestamps={timestamps:?}, query=({query_start}, {query_end}), \ From 0b49cf4f97f5414cab6dee405b850579fea0806c Mon Sep 17 00:00:00 2001 From: discord9 <55937128+discord9@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:50:00 +0800 Subject: [PATCH 2/4] feat: allow native precision instant LastRow selection Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- src/query/src/optimizer/scan_hint.rs | 56 +++++++++++++++---- .../common/promql/native_time_selection.sql | 54 ++++++++++++++++-- 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/src/query/src/optimizer/scan_hint.rs b/src/query/src/optimizer/scan_hint.rs index ce1009f881..75cf0a520a 100644 --- a/src/query/src/optimizer/scan_hint.rs +++ b/src/query/src/optimizer/scan_hint.rs @@ -19,7 +19,6 @@ use arrow_schema::SortOptions; use common_function::aggrs::aggr_wrapper::aggr_state_func_name; use common_recordbatch::OrderOption; use common_recordbatch::filter::SimpleFilterEvaluator; -use common_time::timestamp::TimeUnit; use datafusion::datasource::DefaultTableSource; use datafusion_common::tree_node::{Transformed, TreeNodeRewriter}; use datafusion_common::{Column, Result}; @@ -139,8 +138,7 @@ impl ScanHintRule { /// predicate later rejects that row. Only recognized tag/time predicates are /// allowed: tags select whole series, and supported time predicates constrain /// the scan window before row selection. Field or unrecognized predicates are - /// conservatively rejected. Finer-than-millisecond timestamps are also excluded - /// because instant evaluation can conflate distinct samples at that precision. + /// conservatively rejected. /// /// This checks only attached predicates; the path allowlist separately rejects /// residual Filter nodes between InstantManipulate and the scan. @@ -149,14 +147,6 @@ impl ScanHintRule { provider: &DummyTableProvider, ) -> bool { let metadata = provider.region_metadata(); - // Instant evaluation is millisecond-based, so finer time units can - // conflate timestamps and must not use the LastRow hint. - if !matches!( - metadata.time_index_type().unit(), - TimeUnit::Second | TimeUnit::Millisecond - ) { - return false; - } for filter in &table_scan.filters { let Some(filter) = SimpleFilterEvaluator::try_new(filter) else { return false; @@ -1023,7 +1013,49 @@ mod test { } #[test] - fn single_evaluation_rejects_microsecond_and_nanosecond_time_index_casts() { + fn single_evaluation_uses_last_row_for_microsecond_and_nanosecond_time_indexes() { + for timestamp_type in [ + ConcreteDataType::timestamp_microsecond_datatype(), + ConcreteDataType::timestamp_nanosecond_datatype(), + ] { + let direct_provider = Arc::new(mock_table_provider_with_timestamp( + RegionId::new(1, 1), + timestamp_type.clone(), + )); + let direct = ScanHintRule + .rewrite( + single_evaluation(scan_plan(direct_provider, "direct")), + &OptimizerContext::default(), + ) + .unwrap() + .data; + assert_eq!( + scan_requests(&direct)[0].series_row_selector, + Some(TimeSeriesRowSelector::LastRow { after_merge: true }) + ); + + let projection_provider = Arc::new(mock_table_provider_with_timestamp( + RegionId::new(1, 1), + timestamp_type, + )); + let projection = LogicalPlanBuilder::from(scan_plan(projection_provider, "projection")) + .project(vec![col("ts")]) + .unwrap() + .build() + .unwrap(); + let projected = ScanHintRule + .rewrite(single_evaluation(projection), &OptimizerContext::default()) + .unwrap() + .data; + assert_eq!( + scan_requests(&projected)[0].series_row_selector, + Some(TimeSeriesRowSelector::LastRow { after_merge: true }) + ); + } + } + + #[test] + fn single_evaluation_rejects_lossy_microsecond_and_nanosecond_time_index_casts() { for timestamp_type in [ ConcreteDataType::timestamp_microsecond_datatype(), ConcreteDataType::timestamp_nanosecond_datatype(), diff --git a/tests/cases/standalone/common/promql/native_time_selection.sql b/tests/cases/standalone/common/promql/native_time_selection.sql index d6ec7d3d13..a206cd8dc6 100644 --- a/tests/cases/standalone/common/promql/native_time_selection.sql +++ b/tests/cases/standalone/common/promql/native_time_selection.sql @@ -27,13 +27,36 @@ INSERT INTO native_time_us VALUES (1000000, 'window', 3), (1000001, 'window', 4); +-- The native projection and exact zero-lookback bounds must reach the scan; +-- the 1s+tick row must not displace 201. +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (RepartitionExec:.*) RepartitionExec: REDACTED +-- SQLNESS REPLACE native_time_us.__table_id\s*=\s*UInt32\(\d+\) native_time_us.__table_id=UInt32(REDACTED) +TQL EXPLAIN (1, 1, '1s', '0s') native_time_us{series="exact"}; + +-- The actual memtable scan must use LastRow { after_merge: true } with native +-- zero-lookback bounds; it must select exact 1s rather than the future tick. +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (-+) - +-- SQLNESS REPLACE (\s\s+) _ +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED +-- SQLNESS REPLACE (flat_format.*) REDACTED +TQL ANALYZE VERBOSE (1, 1, '1s', '0s') native_time_us{series="exact"}; + +-- The same-series future tick is in the memtable, while exact 1s remains selected. +TQL EVAL (1, 1, '1s', '0s') native_time_us{series="exact"}; + -- Future-only selection is empty before flushing, exercising the memtable path. TQL EVAL (1, 1, '1s', '300s') native_time_us{series="future"}; ADMIN FLUSH_TABLE('native_time_us'); --- At 1s, selection keeps an exact native timestamp. -TQL EVAL (1, 1, '1s', '300s') native_time_us{series="exact"}; +-- The exact native sample remains selected from the flushed SST. +TQL EVAL (1, 1, '1s', '0s') native_time_us{series="exact"}; TQL EVAL (1, 1, '1s', '300s') timestamp(native_time_us{series="future"}); TQL EVAL (1, 1, '1s', '300s') timestamp(native_time_us{series="exact"}); @@ -89,13 +112,36 @@ INSERT INTO native_time_ns VALUES (1000000000, 'window', 3), (1000000001, 'window', 4); +-- The native projection and exact zero-lookback bounds must reach the scan; +-- the 1s+tick row must not displace 201. +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (RepartitionExec:.*) RepartitionExec: REDACTED +-- SQLNESS REPLACE native_time_ns.__table_id\s*=\s*UInt32\(\d+\) native_time_ns.__table_id=UInt32(REDACTED) +TQL EXPLAIN (1, 1, '1s', '0s') native_time_ns{series="exact"}; + +-- The actual memtable scan must use LastRow { after_merge: true } with native +-- zero-lookback bounds; it must select exact 1s rather than the future tick. +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (-+) - +-- SQLNESS REPLACE (\s\s+) _ +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED +-- SQLNESS REPLACE (flat_format.*) REDACTED +TQL ANALYZE VERBOSE (1, 1, '1s', '0s') native_time_ns{series="exact"}; + +-- The same-series future tick is in the memtable, while exact 1s remains selected. +TQL EVAL (1, 1, '1s', '0s') native_time_ns{series="exact"}; + -- Future-only selection is empty before flushing, exercising the memtable path. TQL EVAL (1, 1, '1s', '300s') native_time_ns{series="future"}; ADMIN FLUSH_TABLE('native_time_ns'); --- At 1s, selection keeps an exact native timestamp. -TQL EVAL (1, 1, '1s', '300s') native_time_ns{series="exact"}; +-- The exact native sample remains selected from the flushed SST. +TQL EVAL (1, 1, '1s', '0s') native_time_ns{series="exact"}; TQL EVAL (1, 1, '1s', '300s') timestamp(native_time_ns{series="future"}); TQL EVAL (1, 1, '1s', '300s') timestamp(native_time_ns{series="exact"}); From 1495a6acc7156b908a0b317d6db0c7164ad9ab61 Mon Sep 17 00:00:00 2001 From: discord9 <55937128+discord9@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:08:17 +0800 Subject: [PATCH 3/4] fix: preserve native time bounds independently for LastRow Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- src/query/src/optimizer/scan_hint.rs | 99 +++++++++---- src/query/src/promql/planner.rs | 135 +++++++++++++----- .../common/promql/native_time_selection.sql | 30 ++++ 3 files changed, 198 insertions(+), 66 deletions(-) diff --git a/src/query/src/optimizer/scan_hint.rs b/src/query/src/optimizer/scan_hint.rs index 75cf0a520a..3c3d0cc920 100644 --- a/src/query/src/optimizer/scan_hint.rs +++ b/src/query/src/optimizer/scan_hint.rs @@ -672,26 +672,49 @@ mod test { } fn last_value_aggregate(input: LogicalPlan) -> LogicalPlan { - LogicalPlanBuilder::from(input) + let aggregate = LogicalPlanBuilder::from(input) .aggregate( vec![col("k0")], - vec![Expr::AggregateFunction(AggregateFunction { - func: last_value_udaf(), - params: AggregateFunctionParams { - args: vec![col("v0")], - distinct: false, - filter: None, - order_by: vec![Sort { - expr: col("ts"), - asc: true, - nulls_first: true, - }], - null_treatment: None, - }, - })], + vec![ + Expr::AggregateFunction(AggregateFunction { + func: last_value_udaf(), + params: AggregateFunctionParams { + args: vec![col("v0")], + distinct: false, + filter: None, + order_by: vec![Sort { + expr: col("ts"), + asc: true, + nulls_first: true, + }], + null_treatment: None, + }, + }), + Expr::AggregateFunction(AggregateFunction { + func: last_value_udaf(), + params: AggregateFunctionParams { + args: vec![col("ts")], + distinct: false, + filter: None, + order_by: vec![Sort { + expr: col("ts"), + asc: true, + nulls_first: true, + }], + null_treatment: None, + }, + }), + ], ) .unwrap() .build() + .unwrap(); + let timestamp = aggregate.schema().field(2).name().clone(); + + LogicalPlanBuilder::from(aggregate) + .project(vec![col("k0"), col(timestamp).alias("ts")]) + .unwrap() + .build() .unwrap() } @@ -918,21 +941,39 @@ mod test { None, ) .unwrap() + .project(vec![ + Expr::Column(Column::new(Some("left"), "ts")), + Expr::Column(Column::new(Some("left"), "v0")), + ]) + .unwrap() .build() .unwrap(); let nonlast_aggregate = LogicalPlanBuilder::from(scan_plan(provider(), "aggregate")) .aggregate( vec![col("k0")], - vec![Expr::AggregateFunction(AggregateFunction { - func: max_udaf(), - params: AggregateFunctionParams { - args: vec![col("v0")], - distinct: false, - filter: None, - order_by: vec![], - null_treatment: None, - }, - })], + vec![ + Expr::AggregateFunction(AggregateFunction { + func: max_udaf(), + params: AggregateFunctionParams { + args: vec![col("v0")], + distinct: false, + filter: None, + order_by: vec![], + null_treatment: None, + }, + }), + Expr::AggregateFunction(AggregateFunction { + func: max_udaf(), + params: AggregateFunctionParams { + args: vec![col("ts")], + distinct: false, + filter: None, + order_by: vec![], + null_treatment: None, + }, + }) + .alias("ts"), + ], ) .unwrap() .build() @@ -1113,7 +1154,7 @@ mod test { #[test] fn single_evaluation_rejects_projection_expressions_that_change_rows() { let invalid_projections = [ - vec![col("ts").alias("renamed")], + vec![col("ts").alias("renamed"), col("ts")], vec![ Expr::BinaryExpr(datafusion_expr::expr::BinaryExpr::new( Box::new(col("v0")), @@ -1121,6 +1162,7 @@ mod test { Box::new(lit(1.0_f64)), )) .alias("v0"), + col("ts"), ], vec![ Expr::Cast(Cast::new( @@ -1129,7 +1171,10 @@ mod test { )) .alias("ts"), ], - vec![Expr::Cast(Cast::new(Box::new(col("v0")), DataType::Int64)).alias("v0")], + vec![ + Expr::Cast(Cast::new(Box::new(col("v0")), DataType::Int64)).alias("v0"), + col("ts"), + ], vec![ Expr::Cast(Cast::new( Box::new(col("ts")), diff --git a/src/query/src/promql/planner.rs b/src/query/src/promql/planner.rs index 34d0e20840..fd52e67a4a 100644 --- a/src/query/src/promql/planner.rs +++ b/src/query/src/promql/planner.rs @@ -2786,13 +2786,14 @@ impl PromPlanner { _ => None, }) .unwrap_or(ArrowTimeUnit::Millisecond); - let scalar = |milliseconds: i64| -> Option { - let value = match unit { - ArrowTimeUnit::Second => milliseconds.div_euclid(1_000), - ArrowTimeUnit::Millisecond => milliseconds, - ArrowTimeUnit::Microsecond => milliseconds.checked_mul(1_000)?, - ArrowTimeUnit::Nanosecond => milliseconds.checked_mul(1_000_000)?, - }; + let native_value = |milliseconds: i128| match unit { + ArrowTimeUnit::Second => milliseconds.div_euclid(1_000), + ArrowTimeUnit::Millisecond => milliseconds, + ArrowTimeUnit::Microsecond => milliseconds * 1_000, + ArrowTimeUnit::Nanosecond => milliseconds * 1_000_000, + }; + let scalar = |milliseconds: i128| -> Option { + let value = i64::try_from(native_value(milliseconds)).ok()?; Some(match unit { ArrowTimeUnit::Second => ScalarValue::TimestampSecond(Some(value), None), ArrowTimeUnit::Millisecond => ScalarValue::TimestampMillisecond(Some(value), None), @@ -2801,45 +2802,57 @@ impl PromPlanner { }) }; let window = self.ctx.range.unwrap_or(self.ctx.lookback_delta); - let filter = |lower_ms: i64, upper_ms: i64| -> Option { - let lower = DfExpr::Literal(scalar(lower_ms)?, None); - let lower_filter = if window == 0 { - time_index_expr.clone().gt_eq(lower) - } else if unit == ArrowTimeUnit::Millisecond - && let Some(inclusive_lower) = lower_ms.checked_add(1) - { - time_index_expr - .clone() - .gt_eq(DfExpr::Literal(scalar(inclusive_lower)?, None)) - } else { - time_index_expr.clone().gt(lower) - }; - Some( - lower_filter.and( + let filter = |lower_ms: i128, upper_ms: i128| { + let lower_value = native_value(lower_ms); + let upper_value = native_value(upper_ms); + if lower_value > i128::from(i64::MAX) || upper_value < i128::from(i64::MIN) { + return Some(lit(false)); + } + let lower_filter = (lower_value >= i128::from(i64::MIN)).then(|| { + let lower = DfExpr::Literal(scalar(lower_ms).unwrap(), None); + if window == 0 { + time_index_expr.clone().gt_eq(lower) + } else if unit == ArrowTimeUnit::Millisecond + && let Some(inclusive_lower) = lower_ms + .checked_add(1) + .and_then(|lower| i64::try_from(lower).ok()) + .and_then(|lower| scalar(i128::from(lower))) + { time_index_expr .clone() - .lt_eq(DfExpr::Literal(scalar(upper_ms)?, None)), - ), - ) + .gt_eq(DfExpr::Literal(inclusive_lower, None)) + } else { + time_index_expr.clone().gt(lower) + } + }); + let upper_filter = (upper_value <= i128::from(i64::MAX)).then(|| { + time_index_expr + .clone() + .lt_eq(DfExpr::Literal(scalar(upper_ms).unwrap(), None)) + }); + + match (lower_filter, upper_filter) { + (Some(lower), Some(upper)) => Some(lower.and(upper)), + (Some(filter), None) | (None, Some(filter)) => Some(filter), + (None, None) => None, + } }; let bounds = |timestamp: i64| { - timestamp - .checked_sub(offset_duration) - .and_then(|upper| upper.checked_sub(window).map(|lower| (lower, upper))) + let upper = i128::from(timestamp) - i128::from(offset_duration); + (upper - i128::from(window), upper) }; let num_points = (end as i128 - start as i128) / self.ctx.interval as i128; if num_points > MAX_SCATTER_POINTS as i128 || self.ctx.interval <= INTERVAL_1H { - return Ok(bounds(start) - .zip(bounds(end)) - .and_then(|((lower, _), (_, upper))| filter(lower, upper))); + let (lower, _) = bounds(start); + let (_, upper) = bounds(end); + return Ok(filter(lower, upper)); } let mut filters = Vec::new(); for timestamp in (start..=end).step_by(self.ctx.interval as usize) { - let Some((lower, upper)) = bounds(timestamp) else { - // An unrepresentable envelope must not discard samples. - return Ok(None); - }; + let (lower, upper) = bounds(timestamp); let Some(filter) = filter(lower, upper) else { + // A point whose native bounds cannot be represented may cover the whole native + // time domain, so its disjunct cannot be omitted. return Ok(None); }; filters.push(filter); @@ -12254,11 +12267,55 @@ mod test { ); } planner.ctx.end = i64::MAX; + let filter = planner + .build_time_index_filter(0, &schema) + .unwrap() + .unwrap() + .to_string(); assert!( - planner - .build_time_index_filter(0, &schema) - .unwrap() - .is_none() + filter.contains("timestamp >= TimestampNanosecond(1000000000, None)"), + "{filter}" + ); + + // A lookback subtraction can underflow milliseconds while the upper bound remains + // representable. Keep that upper bound so LastRow cannot select a future sample. + let ms_schema = Arc::new( + DFSchema::try_from(ArrowSchema::new(vec![Field::new( + "timestamp", + ArrowDataType::Timestamp(ArrowTimeUnit::Millisecond, None), + false, + )])) + .unwrap(), + ); + planner.ctx.start = i64::MIN + 100; + planner.ctx.end = planner.ctx.start; + planner.ctx.lookback_delta = 200; + let filter = planner + .build_time_index_filter(0, &ms_schema) + .unwrap() + .unwrap() + .to_string(); + assert_eq!( + filter, + format!( + "timestamp <= TimestampMillisecond({}, None)", + i64::MIN + 100 + ) + ); + + // The lower bound can also overflow while converting milliseconds to native nanoseconds. + // Its representable upper bound still has to reach the scan. + planner.ctx.start = 0; + planner.ctx.end = 0; + planner.ctx.lookback_delta = 300_000; + let filter = planner + .build_time_index_filter(9_223_372_036_854, &schema) + .unwrap() + .unwrap() + .to_string(); + assert_eq!( + filter, + "timestamp <= TimestampNanosecond(-9223372036854000000, None)" ); } diff --git a/tests/cases/standalone/common/promql/native_time_selection.sql b/tests/cases/standalone/common/promql/native_time_selection.sql index a206cd8dc6..9a1a1d9c68 100644 --- a/tests/cases/standalone/common/promql/native_time_selection.sql +++ b/tests/cases/standalone/common/promql/native_time_selection.sql @@ -171,6 +171,36 @@ TQL EVAL (1, 1, '1s') last_over_time((native_time_ns{series="exact"})[1s:1s]); DROP TABLE native_time_ns; +-- An unrepresentable native lower bound must not discard its representable upper bound. +-- The upper filter must reach LastRow so the 1ms-future row cannot hide the eligible row. +CREATE TABLE native_time_ns_lower_overflow ( + ts TIMESTAMP(9) TIME INDEX, + series STRING PRIMARY KEY, + val DOUBLE, +); +INSERT INTO native_time_ns_lower_overflow VALUES + (-9223372036854000000, 'exact', 1), + (-9223372036853000000, 'exact', 2); + +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE native_time_ns_lower_overflow.__table_id\s*=\s*UInt32\(\d+\) native_time_ns_lower_overflow.__table_id=UInt32(REDACTED) +TQL EXPLAIN (0, 0, '1s', '300s') native_time_ns_lower_overflow{series="exact"} offset 9223372036854ms; + +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (-+) - +-- SQLNESS REPLACE (\s\s+) _ +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED +-- SQLNESS REPLACE (flat_format.*) REDACTED +TQL ANALYZE VERBOSE (0, 0, '1s', '300s') native_time_ns_lower_overflow{series="exact"} offset 9223372036854ms; + +-- The representable upper bound selects only the exact row. +TQL EVAL (0, 0, '1s', '300s') native_time_ns_lower_overflow{series="exact"} offset 9223372036854ms; +DROP TABLE native_time_ns_lower_overflow; + -- Second precision is promoted before applying fractional-second offsets. CREATE TABLE native_time_sec (ts TIMESTAMP(0) TIME INDEX, val DOUBLE); INSERT INTO native_time_sec VALUES (0, 10), (1, 11), (2, 12); From 10eda8d580eb61edefa323ecfa75ba004d5e57cf Mon Sep 17 00:00:00 2001 From: discord9 <55937128+discord9@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:48:46 +0800 Subject: [PATCH 4/4] test: verify native LastRow predicates and overflow through SQL Signed-off-by: discord9 <55937128+discord9@users.noreply.github.com> --- .../promql/native_time_selection.result | 224 +++++++++++++++++- .../common/promql/native_time_selection.sql | 39 +-- 2 files changed, 242 insertions(+), 21 deletions(-) diff --git a/tests/cases/standalone/common/promql/native_time_selection.result b/tests/cases/standalone/common/promql/native_time_selection.result index 1715356f81..33590312e2 100644 --- a/tests/cases/standalone/common/promql/native_time_selection.result +++ b/tests/cases/standalone/common/promql/native_time_selection.result @@ -30,6 +30,67 @@ INSERT INTO native_time_us VALUES Affected Rows: 18 +-- The native projection and exact 1ms-lookback bounds must reach the scan; +-- the 1s+tick row must not displace 201. +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (RepartitionExec:.*) RepartitionExec: REDACTED +-- SQLNESS REPLACE native_time_us.__table_id\s*=\s*UInt32\(\d+\) native_time_us.__table_id=UInt32(REDACTED) +TQL EXPLAIN (1, 1, '1s', '1ms') native_time_us{series="exact"}; + ++---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| plan_type | plan | ++---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| logical_plan | MergeScan [is_placeholder=false, remote_input=[ | +| | PromInstantManipulate: range=[1000..1000], lookback=[1], interval=[1000], time index=[ts] | +| | PromSeriesDivide: tags=["series"] | +| | Sort: native_time_us.series ASC NULLS FIRST, native_time_us.ts ASC NULLS FIRST | +| | Projection: native_time_us.val, native_time_us.series, native_time_us.ts | +| | Filter: native_time_us.series = Utf8("exact") AND native_time_us.ts > TimestampMicrosecond(999000, None) AND native_time_us.ts <= TimestampMicrosecond(1000000, None) | +| | TableScan: native_time_us, partial_filters=[native_time_us.series = Utf8("exact"), native_time_us.ts > TimestampMicrosecond(999000, None), native_time_us.ts <= TimestampMicrosecond(1000000, None)] | +| | ]] | +| physical_plan | CooperativeExec | +| | MergeScanExec: REDACTED +| | | ++---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +-- The actual memtable scan must use LastRow { after_merge: true } with native +-- 1ms-lookback bounds; it must select exact 1s rather than the future tick. +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (-+) - +-- SQLNESS REPLACE (\s\s+) _ +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED +-- SQLNESS REPLACE (flat_format.*) REDACTED +-- SQLNESS REPLACE (elapsed_compute.*) REDACTED +TQL ANALYZE VERBOSE (1, 1, '1s', '1ms') native_time_us{series="exact"}; + ++-+-+-+ +| stage | node | plan_| ++-+-+-+ +| 0_| 0_|_CooperativeExec metrics=[]_| +|_|_|_MergeScanExec: REDACTED +|_|_|_| +| 1_| 0_|_PromInstantManipulateExec: range=[1000..1000], lookback=[1], interval=[1000], time index=[ts] metrics=[output_rows: 1, REDACTED +|_|_|_PromSeriesDivideExec: tags=["series"] metrics=[output_rows: 1, REDACTED +|_|_|_ProjectionExec: expr=[val@2 as val, series@1 as series, ts@0 as ts] metrics=[output_rows: 1, REDACTED +|_|_|_CooperativeExec metrics=[]_| +|_|_|_SeriesScan: region=REDACTED, {"partition_count":{"count":1, "mem_ranges":1, "files":0, "file_ranges":0}, "selector":"LastRow { after_merge: true }", "distribution":"PerSeries", "projection": ["ts", "series", "val"], "filters": ["series = Dictionary(UInt32, Utf8(\"exact\"))", "ts > TimestampMicrosecond(999000, None)", "ts <= TimestampMicrosecond(1000000, None)"], "REDACTED +|_|_|_| +|_|_| Total rows: 1_| ++-+-+-+ + +-- The same-series future tick is in the memtable, while exact 1s remains selected. +TQL EVAL (1, 1, '1s', '1ms') native_time_us{series="exact"}; + ++-------+--------+---------------------+ +| val | series | ts | ++-------+--------+---------------------+ +| 201.0 | exact | 1970-01-01T00:00:01 | ++-------+--------+---------------------+ + -- Future-only selection is empty before flushing, exercising the memtable path. TQL EVAL (1, 1, '1s', '300s') native_time_us{series="future"}; @@ -44,8 +105,8 @@ ADMIN FLUSH_TABLE('native_time_us'); | 0 | +-------------------------------------+ --- At 1s, selection keeps an exact native timestamp. -TQL EVAL (1, 1, '1s', '300s') native_time_us{series="exact"}; +-- The exact native sample remains selected from the flushed SST. +TQL EVAL (1, 1, '1s', '1ms') native_time_us{series="exact"}; +-------+--------+---------------------+ | val | series | ts | @@ -195,6 +256,67 @@ INSERT INTO native_time_ns VALUES Affected Rows: 18 +-- The native projection and exact 1ms-lookback bounds must reach the scan; +-- the 1s+tick row must not displace 201. +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (RepartitionExec:.*) RepartitionExec: REDACTED +-- SQLNESS REPLACE native_time_ns.__table_id\s*=\s*UInt32\(\d+\) native_time_ns.__table_id=UInt32(REDACTED) +TQL EXPLAIN (1, 1, '1s', '1ms') native_time_ns{series="exact"}; + ++---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| plan_type | plan | ++---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| logical_plan | MergeScan [is_placeholder=false, remote_input=[ | +| | PromInstantManipulate: range=[1000..1000], lookback=[1], interval=[1000], time index=[ts] | +| | PromSeriesDivide: tags=["series"] | +| | Sort: native_time_ns.series ASC NULLS FIRST, native_time_ns.ts ASC NULLS FIRST | +| | Projection: native_time_ns.val, native_time_ns.series, native_time_ns.ts | +| | Filter: native_time_ns.series = Utf8("exact") AND native_time_ns.ts > TimestampNanosecond(999000000, None) AND native_time_ns.ts <= TimestampNanosecond(1000000000, None) | +| | TableScan: native_time_ns, partial_filters=[native_time_ns.series = Utf8("exact"), native_time_ns.ts > TimestampNanosecond(999000000, None), native_time_ns.ts <= TimestampNanosecond(1000000000, None)] | +| | ]] | +| physical_plan | CooperativeExec | +| | MergeScanExec: REDACTED +| | | ++---------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +-- The actual memtable scan must use LastRow { after_merge: true } with native +-- 1ms-lookback bounds; it must select exact 1s rather than the future tick. +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (-+) - +-- SQLNESS REPLACE (\s\s+) _ +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED +-- SQLNESS REPLACE (flat_format.*) REDACTED +-- SQLNESS REPLACE (elapsed_compute.*) REDACTED +TQL ANALYZE VERBOSE (1, 1, '1s', '1ms') native_time_ns{series="exact"}; + ++-+-+-+ +| stage | node | plan_| ++-+-+-+ +| 0_| 0_|_CooperativeExec metrics=[]_| +|_|_|_MergeScanExec: REDACTED +|_|_|_| +| 1_| 0_|_PromInstantManipulateExec: range=[1000..1000], lookback=[1], interval=[1000], time index=[ts] metrics=[output_rows: 1, REDACTED +|_|_|_PromSeriesDivideExec: tags=["series"] metrics=[output_rows: 1, REDACTED +|_|_|_ProjectionExec: expr=[val@2 as val, series@1 as series, ts@0 as ts] metrics=[output_rows: 1, REDACTED +|_|_|_CooperativeExec metrics=[]_| +|_|_|_SeriesScan: region=REDACTED, {"partition_count":{"count":1, "mem_ranges":1, "files":0, "file_ranges":0}, "selector":"LastRow { after_merge: true }", "distribution":"PerSeries", "projection": ["ts", "series", "val"], "filters": ["series = Dictionary(UInt32, Utf8(\"exact\"))", "ts > TimestampNanosecond(999000000, None)", "ts <= TimestampNanosecond(1000000000, None)"], "REDACTED +|_|_|_| +|_|_| Total rows: 1_| ++-+-+-+ + +-- The same-series future tick is in the memtable, while exact 1s remains selected. +TQL EVAL (1, 1, '1s', '1ms') native_time_ns{series="exact"}; + ++-------+--------+---------------------+ +| val | series | ts | ++-------+--------+---------------------+ +| 201.0 | exact | 1970-01-01T00:00:01 | ++-------+--------+---------------------+ + -- Future-only selection is empty before flushing, exercising the memtable path. TQL EVAL (1, 1, '1s', '300s') native_time_ns{series="future"}; @@ -209,8 +331,8 @@ ADMIN FLUSH_TABLE('native_time_ns'); | 0 | +-------------------------------------+ --- At 1s, selection keeps an exact native timestamp. -TQL EVAL (1, 1, '1s', '300s') native_time_ns{series="exact"}; +-- The exact native sample remains selected from the flushed SST. +TQL EVAL (1, 1, '1s', '1ms') native_time_ns{series="exact"}; +-------+--------+---------------------+ | val | series | ts | @@ -330,6 +452,100 @@ DROP TABLE native_time_ns; Affected Rows: 0 +-- An unrepresentable native lower bound must not discard its representable upper bound. +-- The upper filter must reach LastRow so the 1ms-future row cannot hide the eligible row. +CREATE TABLE native_time_ns_lower_overflow ( + ts TIMESTAMP(9) TIME INDEX, + series STRING PRIMARY KEY, + val DOUBLE, +); + +Affected Rows: 0 + +INSERT INTO native_time_ns_lower_overflow VALUES + (-9223200000000000000, 'exact', 1), + (-9223199999999000000, 'exact', 2); + +Affected Rows: 2 + +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE native_time_ns_lower_overflow.__table_id\s*=\s*UInt32\(\d+\) native_time_ns_lower_overflow.__table_id=UInt32(REDACTED) +TQL EXPLAIN (0, 0, '1s', '2d') native_time_ns_lower_overflow{series="exact"} offset 106750d; + ++---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| plan_type | plan | ++---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| logical_plan | MergeScan [is_placeholder=false, remote_input=[ | +| | PromInstantManipulate: range=[0..0], lookback=[172800000], interval=[1000], time index=[ts] | +| | PromSeriesNormalize: offset=[9223200000000], time index=[ts], filter NaN: [false] | +| | PromSeriesDivide: tags=["series"] | +| | Sort: native_time_ns_lower_overflow.series ASC NULLS FIRST, native_time_ns_lower_overflow.ts ASC NULLS FIRST | +| | Projection: native_time_ns_lower_overflow.val, native_time_ns_lower_overflow.series, native_time_ns_lower_overflow.ts | +| | Filter: native_time_ns_lower_overflow.series = Utf8("exact") AND native_time_ns_lower_overflow.ts <= TimestampNanosecond(-9223200000000000000, None) | +| | TableScan: native_time_ns_lower_overflow, partial_filters=[native_time_ns_lower_overflow.series = Utf8("exact"), native_time_ns_lower_overflow.ts <= TimestampNanosecond(-9223200000000000000, None)] | +| | ]] | +| physical_plan | CooperativeExec | +| | MergeScanExec: REDACTED +| | | ++---------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + +-- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED +-- SQLNESS REPLACE (Hash.*) REDACTED +-- SQLNESS REPLACE (-+) - +-- SQLNESS REPLACE (\s\s+) _ +-- SQLNESS REPLACE (peers.*) REDACTED +-- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED +-- SQLNESS REPLACE (flat_format.*) REDACTED +-- SQLNESS REPLACE (elapsed_compute.*) REDACTED +TQL ANALYZE VERBOSE (0, 0, '1s', '2d') native_time_ns_lower_overflow{series="exact"} offset 106750d; + ++-+-+-+ +| stage | node | plan_| ++-+-+-+ +| 0_| 0_|_CooperativeExec metrics=[]_| +|_|_|_MergeScanExec: REDACTED +|_|_|_| +| 1_| 0_|_PromInstantManipulateExec: range=[0..0], lookback=[172800000], interval=[1000], time index=[ts] metrics=[output_rows: 1, REDACTED +|_|_|_PromSeriesNormalizeExec: offset=[9223200000000], time index=[ts], filter NaN: [false] metrics=[output_rows: 1, REDACTED +|_|_|_PromSeriesDivideExec: tags=["series"] metrics=[output_rows: 1, REDACTED +|_|_|_ProjectionExec: expr=[val@2 as val, series@1 as series, ts@0 as ts] metrics=[output_rows: 1, REDACTED +|_|_|_CooperativeExec metrics=[]_| +|_|_|_SeriesScan: region=REDACTED, {"partition_count":{"count":1, "mem_ranges":1, "files":0, "file_ranges":0}, "selector":"LastRow { after_merge: true }", "distribution":"PerSeries", "projection": ["ts", "series", "val"], "filters": ["series = Dictionary(UInt32, Utf8(\"exact\"))", "ts <= TimestampNanosecond(-9223200000000000000, None)"], "REDACTED +|_|_|_| +|_|_| Total rows: 1_| ++-+-+-+ + +-- The representable upper bound selects only the exact row. +TQL EVAL (0, 0, '1s', '2d') native_time_ns_lower_overflow{series="exact"} offset 106750d; + ++-----+--------+---------------------+ +| val | series | ts | ++-----+--------+---------------------+ +| 1.0 | exact | 1970-01-01T00:00:00 | ++-----+--------+---------------------+ + +ADMIN FLUSH_TABLE('native_time_ns_lower_overflow'); + ++----------------------------------------------------+ +| ADMIN FLUSH_TABLE('native_time_ns_lower_overflow') | ++----------------------------------------------------+ +| 0 | ++----------------------------------------------------+ + +TQL EVAL (0, 0, '1s', '2d') native_time_ns_lower_overflow{series="exact"} offset 106750d; + ++-----+--------+---------------------+ +| val | series | ts | ++-----+--------+---------------------+ +| 1.0 | exact | 1970-01-01T00:00:00 | ++-----+--------+---------------------+ + +DROP TABLE native_time_ns_lower_overflow; + +Affected Rows: 0 + -- Second precision is promoted before applying fractional-second offsets. CREATE TABLE native_time_sec (ts TIMESTAMP(0) TIME INDEX, val DOUBLE); diff --git a/tests/cases/standalone/common/promql/native_time_selection.sql b/tests/cases/standalone/common/promql/native_time_selection.sql index 9a1a1d9c68..7f0c72f507 100644 --- a/tests/cases/standalone/common/promql/native_time_selection.sql +++ b/tests/cases/standalone/common/promql/native_time_selection.sql @@ -27,17 +27,17 @@ INSERT INTO native_time_us VALUES (1000000, 'window', 3), (1000001, 'window', 4); --- The native projection and exact zero-lookback bounds must reach the scan; +-- The native projection and exact 1ms-lookback bounds must reach the scan; -- the 1s+tick row must not displace 201. -- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED -- SQLNESS REPLACE (peers.*) REDACTED -- SQLNESS REPLACE (Hash.*) REDACTED -- SQLNESS REPLACE (RepartitionExec:.*) RepartitionExec: REDACTED -- SQLNESS REPLACE native_time_us.__table_id\s*=\s*UInt32\(\d+\) native_time_us.__table_id=UInt32(REDACTED) -TQL EXPLAIN (1, 1, '1s', '0s') native_time_us{series="exact"}; +TQL EXPLAIN (1, 1, '1s', '1ms') native_time_us{series="exact"}; -- The actual memtable scan must use LastRow { after_merge: true } with native --- zero-lookback bounds; it must select exact 1s rather than the future tick. +-- 1ms-lookback bounds; it must select exact 1s rather than the future tick. -- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED -- SQLNESS REPLACE (Hash.*) REDACTED -- SQLNESS REPLACE (-+) - @@ -45,10 +45,11 @@ TQL EXPLAIN (1, 1, '1s', '0s') native_time_us{series="exact"}; -- SQLNESS REPLACE (peers.*) REDACTED -- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED -- SQLNESS REPLACE (flat_format.*) REDACTED -TQL ANALYZE VERBOSE (1, 1, '1s', '0s') native_time_us{series="exact"}; +-- SQLNESS REPLACE (elapsed_compute.*) REDACTED +TQL ANALYZE VERBOSE (1, 1, '1s', '1ms') native_time_us{series="exact"}; -- The same-series future tick is in the memtable, while exact 1s remains selected. -TQL EVAL (1, 1, '1s', '0s') native_time_us{series="exact"}; +TQL EVAL (1, 1, '1s', '1ms') native_time_us{series="exact"}; -- Future-only selection is empty before flushing, exercising the memtable path. TQL EVAL (1, 1, '1s', '300s') native_time_us{series="future"}; @@ -56,7 +57,7 @@ TQL EVAL (1, 1, '1s', '300s') native_time_us{series="future"}; ADMIN FLUSH_TABLE('native_time_us'); -- The exact native sample remains selected from the flushed SST. -TQL EVAL (1, 1, '1s', '0s') native_time_us{series="exact"}; +TQL EVAL (1, 1, '1s', '1ms') native_time_us{series="exact"}; TQL EVAL (1, 1, '1s', '300s') timestamp(native_time_us{series="future"}); TQL EVAL (1, 1, '1s', '300s') timestamp(native_time_us{series="exact"}); @@ -112,17 +113,17 @@ INSERT INTO native_time_ns VALUES (1000000000, 'window', 3), (1000000001, 'window', 4); --- The native projection and exact zero-lookback bounds must reach the scan; +-- The native projection and exact 1ms-lookback bounds must reach the scan; -- the 1s+tick row must not displace 201. -- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED -- SQLNESS REPLACE (peers.*) REDACTED -- SQLNESS REPLACE (Hash.*) REDACTED -- SQLNESS REPLACE (RepartitionExec:.*) RepartitionExec: REDACTED -- SQLNESS REPLACE native_time_ns.__table_id\s*=\s*UInt32\(\d+\) native_time_ns.__table_id=UInt32(REDACTED) -TQL EXPLAIN (1, 1, '1s', '0s') native_time_ns{series="exact"}; +TQL EXPLAIN (1, 1, '1s', '1ms') native_time_ns{series="exact"}; -- The actual memtable scan must use LastRow { after_merge: true } with native --- zero-lookback bounds; it must select exact 1s rather than the future tick. +-- 1ms-lookback bounds; it must select exact 1s rather than the future tick. -- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED -- SQLNESS REPLACE (Hash.*) REDACTED -- SQLNESS REPLACE (-+) - @@ -130,10 +131,11 @@ TQL EXPLAIN (1, 1, '1s', '0s') native_time_ns{series="exact"}; -- SQLNESS REPLACE (peers.*) REDACTED -- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED -- SQLNESS REPLACE (flat_format.*) REDACTED -TQL ANALYZE VERBOSE (1, 1, '1s', '0s') native_time_ns{series="exact"}; +-- SQLNESS REPLACE (elapsed_compute.*) REDACTED +TQL ANALYZE VERBOSE (1, 1, '1s', '1ms') native_time_ns{series="exact"}; -- The same-series future tick is in the memtable, while exact 1s remains selected. -TQL EVAL (1, 1, '1s', '0s') native_time_ns{series="exact"}; +TQL EVAL (1, 1, '1s', '1ms') native_time_ns{series="exact"}; -- Future-only selection is empty before flushing, exercising the memtable path. TQL EVAL (1, 1, '1s', '300s') native_time_ns{series="future"}; @@ -141,7 +143,7 @@ TQL EVAL (1, 1, '1s', '300s') native_time_ns{series="future"}; ADMIN FLUSH_TABLE('native_time_ns'); -- The exact native sample remains selected from the flushed SST. -TQL EVAL (1, 1, '1s', '0s') native_time_ns{series="exact"}; +TQL EVAL (1, 1, '1s', '1ms') native_time_ns{series="exact"}; TQL EVAL (1, 1, '1s', '300s') timestamp(native_time_ns{series="future"}); TQL EVAL (1, 1, '1s', '300s') timestamp(native_time_ns{series="exact"}); @@ -179,14 +181,14 @@ CREATE TABLE native_time_ns_lower_overflow ( val DOUBLE, ); INSERT INTO native_time_ns_lower_overflow VALUES - (-9223372036854000000, 'exact', 1), - (-9223372036853000000, 'exact', 2); + (-9223200000000000000, 'exact', 1), + (-9223199999999000000, 'exact', 2); -- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED -- SQLNESS REPLACE (peers.*) REDACTED -- SQLNESS REPLACE (Hash.*) REDACTED -- SQLNESS REPLACE native_time_ns_lower_overflow.__table_id\s*=\s*UInt32\(\d+\) native_time_ns_lower_overflow.__table_id=UInt32(REDACTED) -TQL EXPLAIN (0, 0, '1s', '300s') native_time_ns_lower_overflow{series="exact"} offset 9223372036854ms; +TQL EXPLAIN (0, 0, '1s', '2d') native_time_ns_lower_overflow{series="exact"} offset 106750d; -- SQLNESS REPLACE (RoundRobinBatch.*) REDACTED -- SQLNESS REPLACE (Hash.*) REDACTED @@ -195,10 +197,13 @@ TQL EXPLAIN (0, 0, '1s', '300s') native_time_ns_lower_overflow{series="exact"} o -- SQLNESS REPLACE (peers.*) REDACTED -- SQLNESS REPLACE region=\d+\(\d+,\s+\d+\) region=REDACTED -- SQLNESS REPLACE (flat_format.*) REDACTED -TQL ANALYZE VERBOSE (0, 0, '1s', '300s') native_time_ns_lower_overflow{series="exact"} offset 9223372036854ms; +-- SQLNESS REPLACE (elapsed_compute.*) REDACTED +TQL ANALYZE VERBOSE (0, 0, '1s', '2d') native_time_ns_lower_overflow{series="exact"} offset 106750d; -- The representable upper bound selects only the exact row. -TQL EVAL (0, 0, '1s', '300s') native_time_ns_lower_overflow{series="exact"} offset 9223372036854ms; +TQL EVAL (0, 0, '1s', '2d') native_time_ns_lower_overflow{series="exact"} offset 106750d; +ADMIN FLUSH_TABLE('native_time_ns_lower_overflow'); +TQL EVAL (0, 0, '1s', '2d') native_time_ns_lower_overflow{series="exact"} offset 106750d; DROP TABLE native_time_ns_lower_overflow; -- Second precision is promoted before applying fractional-second offsets.