blob: 6c18ff568bbb6250f19e944b9f8a178530854895 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Query result iteration — the SessionDataSet state machine (protocol
//! spec §4.3): drain the current TsBlock, decode the next cached block,
//! fetch more pages while `moreData`, then auto-close.
use std::collections::VecDeque;
use crate::client::session::{QueryHandle, Session};
use crate::data::tsblock::TsBlock;
use crate::data::value::Value;
use crate::error::{Error, Result};
/// One result row: the timestamp (`None` when the server set
/// `ignoreTimeStamp`, e.g. aggregations and SHOW statements) and one
/// [`Value`] per output column.
#[derive(Debug, Clone, PartialEq)]
pub struct Row {
pub timestamp: Option<i64>,
pub values: Vec<Value>,
}
/// An iterable query result set.
///
/// Borrows the [`Session`] mutably for its whole lifetime: `fetchResultsV2`
/// and `closeOperation` must reach the node that owns the query id, so the
/// connection stays pinned to the result set until it is closed (spec
/// gotcha #13). The borrow also means no other statement can run on the
/// session while a result set is open — close (or drop) it first.
///
/// Not a `std::iter::Iterator` — [`SessionDataSet::next_row`] returns
/// `Result<Option<Row>>` so decode and fetch errors surface cleanly.
/// The query is closed automatically on exhaustion, on [`SessionDataSet::close`],
/// or on drop (best-effort, matching the other SDKs).
pub struct SessionDataSet<'a> {
session: &'a mut Session,
query_id: i64,
statement: String,
columns: Vec<String>,
data_type_list: Vec<String>,
ignore_time_stamp: bool,
/// Output column ordinal → physical TsBlock column index; `-1` = time
/// column; identity mapping when `None` (spec §4.2 field 17).
column_index_map: Option<Vec<i32>>,
/// Cached serialized TsBlocks not yet decoded.
pending_blocks: VecDeque<Vec<u8>>,
/// Currently decoded block and the next row to yield from it.
current: Option<TsBlock>,
row_index: usize,
more_data: bool,
closed: bool,
}
impl<'a> SessionDataSet<'a> {
pub(crate) fn new(session: &'a mut Session, handle: QueryHandle) -> SessionDataSet<'a> {
SessionDataSet {
session,
query_id: handle.query_id,
statement: handle.statement,
columns: handle.columns,
data_type_list: handle.data_type_list,
ignore_time_stamp: handle.ignore_time_stamp,
column_index_map: handle.column_index2_ts_block_column_index_list,
pending_blocks: handle.query_result.into(),
current: None,
row_index: 0,
more_data: handle.more_data,
closed: false,
}
}
/// Output column names.
pub fn columns(&self) -> &[String] {
&self.columns
}
/// Output column type names as reported by the server
/// (`"INT64"`, `"TEXT"`, …), parallel to [`SessionDataSet::columns`].
pub fn column_types(&self) -> &[String] {
&self.data_type_list
}
/// Whether the server flagged this result as having no meaningful
/// timestamps (row timestamps are then `None`).
pub fn ignore_time_stamp(&self) -> bool {
self.ignore_time_stamp
}
/// Advance to the next row, fetching further pages as needed.
/// Returns `Ok(None)` when exhausted (the query is then auto-closed).
pub fn next_row(&mut self) -> Result<Option<Row>> {
if self.closed {
return Ok(None);
}
loop {
// 1. Unread rows in the current block → yield the next one.
if let Some(block) = &self.current {
if self.row_index < block.position_count {
let row = self.assemble_row()?;
self.row_index += 1;
return Ok(Some(row));
}
self.current = None;
}
// 2. More cached blocks → decode the next non-empty one.
if let Some(bytes) = self.pending_blocks.pop_front() {
self.current = Some(TsBlock::decode(&bytes)?);
self.row_index = 0;
continue;
}
// 3. Server has more pages → fetch and refill.
if self.more_data {
let (blocks, more) = self.session.fetch_results(self.query_id, &self.statement)?;
self.pending_blocks = blocks.into();
self.more_data = more;
if !self.pending_blocks.is_empty() {
continue;
}
if self.more_data {
// hasResultSet with an empty page and moreData still
// set — loop and fetch again rather than spin here.
continue;
}
}
// 4. Exhausted → auto-close and report the end.
self.close();
return Ok(None);
}
}
/// Build the output row at `self.row_index` of the current block,
/// mapping output columns through `columnIndex2TsBlockColumnIndexList`
/// (`-1` = time column; identity when absent — spec §5.4).
fn assemble_row(&self) -> Result<Row> {
let block = self.current.as_ref().expect("current block");
let i = self.row_index;
let mut values = Vec::with_capacity(self.columns.len());
for ordinal in 0..self.columns.len() {
let physical = match &self.column_index_map {
Some(map) => *map.get(ordinal).ok_or_else(|| {
Error::Decode(format!(
"column index map has {} entries, need output column {ordinal}",
map.len()
))
})?,
None => ordinal as i32,
};
if physical == -1 {
values.push(Value::Timestamp(block.timestamps[i]));
continue;
}
let column = usize::try_from(physical)
.ok()
.and_then(|p| block.columns.get(p))
.ok_or_else(|| {
Error::Decode(format!(
"column index map points at TsBlock column {physical}, block has {}",
block.columns.len()
))
})?;
values.push(Self::apply_logical_type(
column[i].clone(),
self.data_type_list.get(ordinal).map(String::as_str),
));
}
let timestamp = (!self.ignore_time_stamp).then(|| block.timestamps[i]);
Ok(Row { timestamp, values })
}
/// Re-tag a decoded value with the column's logical type from the
/// response's `dataTypeList`. TsBlock headers carry the *physical* type
/// (DATE arrives as INT32, TIMESTAMP as INT64, STRING as TEXT), so the
/// block decoder alone cannot distinguish them.
fn apply_logical_type(value: Value, logical: Option<&str>) -> Value {
match (logical, value) {
(Some("DATE"), Value::Int32(v)) => Value::Date(v),
(Some("TIMESTAMP"), Value::Int64(v)) => Value::Timestamp(v),
(Some("STRING"), Value::Text(s)) => Value::String(s),
(Some("BLOB"), Value::Text(s)) => Value::Blob(s.into_bytes()),
(_, v) => v,
}
}
/// Close the query (best-effort `closeOperation`, errors swallowed) and
/// release the session borrow's obligations. Idempotent; also invoked
/// automatically on exhaustion and on drop.
pub fn close(&mut self) {
if self.closed {
return;
}
self.closed = true;
self.pending_blocks.clear();
self.current = None;
self.session.close_query(self.query_id);
}
}
impl Drop for SessionDataSet<'_> {
fn drop(&mut self) {
self.close();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::session::SessionConfig;
use crate::data::tsblock::test_util::{header, int32_block, time_column};
use crate::data::tsblock::{ENCODING_BINARY_ARRAY, ENCODING_INT32_ARRAY};
use crate::data::TSDataType;
/// A never-opened session: fetch_results errors, close_query is a no-op —
/// exactly what the offline state-machine tests need.
fn offline_session() -> Session {
Session::new(SessionConfig::default())
}
fn handle(blocks: Vec<Vec<u8>>, more_data: bool) -> QueryHandle {
QueryHandle {
query_id: 1,
statement: "SELECT s1 FROM root.sg.d1".into(),
columns: vec!["root.sg.d1.s1".into()],
data_type_list: vec!["INT32".into()],
ignore_time_stamp: false,
query_result: blocks,
more_data,
column_index2_ts_block_column_index_list: None,
}
}
#[test]
fn logical_type_retagging_from_data_type_list() {
// DATE arrives physically as INT32 in the TsBlock; dataTypeList says DATE.
let mut session = offline_session();
let mut h = handle(vec![int32_block(&[1], &[20260713])], false);
h.data_type_list = vec!["DATE".into()];
let mut ds = SessionDataSet::new(&mut session, h);
let row = ds.next_row().unwrap().unwrap();
assert_eq!(row.values[0], Value::Date(20260713));
assert!(ds.next_row().unwrap().is_none());
}
#[test]
fn apply_logical_type_variants() {
use Value::*;
// Physical → logical re-tags.
assert_eq!(
SessionDataSet::apply_logical_type(Int32(20260713), Some("DATE")),
Date(20260713)
);
assert_eq!(
SessionDataSet::apply_logical_type(Int64(99), Some("TIMESTAMP")),
Timestamp(99)
);
assert_eq!(
SessionDataSet::apply_logical_type(Text("s".into()), Some("STRING")),
String("s".into())
);
assert_eq!(
SessionDataSet::apply_logical_type(Text("b".into()), Some("BLOB")),
Blob(b"b".to_vec())
);
// Pass-throughs: matching physical types and nulls stay untouched.
assert_eq!(
SessionDataSet::apply_logical_type(Int32(5), Some("INT32")),
Int32(5)
);
assert_eq!(SessionDataSet::apply_logical_type(Null, Some("DATE")), Null);
assert_eq!(SessionDataSet::apply_logical_type(Int32(5), None), Int32(5));
}
#[test]
fn drains_rows_across_multiple_cached_blocks() {
let mut session = offline_session();
let blocks = vec![int32_block(&[1, 2], &[10, 20]), int32_block(&[3], &[30])];
let mut ds = SessionDataSet::new(&mut session, handle(blocks, false));
let mut rows = Vec::new();
while let Some(row) = ds.next_row().unwrap() {
rows.push(row);
}
assert_eq!(
rows,
vec![
Row {
timestamp: Some(1),
values: vec![Value::Int32(10)]
},
Row {
timestamp: Some(2),
values: vec![Value::Int32(20)]
},
Row {
timestamp: Some(3),
values: vec![Value::Int32(30)]
},
]
);
// Exhausted → auto-closed; further calls keep returning None.
assert_eq!(ds.next_row().unwrap(), None);
}
#[test]
fn empty_result_set_yields_none_immediately() {
let mut session = offline_session();
let mut ds = SessionDataSet::new(&mut session, handle(vec![], false));
assert_eq!(ds.next_row().unwrap(), None);
}
#[test]
fn empty_blocks_are_skipped() {
let mut session = offline_session();
let blocks = vec![int32_block(&[], &[]), int32_block(&[7], &[70])];
let mut ds = SessionDataSet::new(&mut session, handle(blocks, false));
let row = ds.next_row().unwrap().unwrap();
assert_eq!(row.timestamp, Some(7));
assert_eq!(ds.next_row().unwrap(), None);
}
#[test]
fn more_data_on_dead_session_surfaces_fetch_error() {
// moreData=true forces a fetch_results call, which fails on a
// never-opened session — the error must propagate, not panic.
let mut session = offline_session();
let mut ds = SessionDataSet::new(&mut session, handle(vec![], true));
assert!(ds.next_row().is_err());
}
#[test]
fn ignore_time_stamp_omits_timestamps() {
let mut session = offline_session();
let mut h = handle(vec![int32_block(&[5], &[50])], false);
h.ignore_time_stamp = true;
let mut ds = SessionDataSet::new(&mut session, h);
assert!(ds.ignore_time_stamp());
let row = ds.next_row().unwrap().unwrap();
assert_eq!(row.timestamp, None);
assert_eq!(row.values, vec![Value::Int32(50)]);
}
#[test]
fn column_index_map_with_time_column_and_duplicates() {
// Physical block: one TEXT column. Output: [time, text, text again].
let mut b = header(&[TSDataType::Text], 2, &[ENCODING_BINARY_ARRAY]);
b.extend_from_slice(&time_column(&[100, 200]));
b.push(0); // mayHaveNull
for s in ["a", "b"] {
b.extend_from_slice(&(s.len() as i32).to_be_bytes());
b.extend_from_slice(s.as_bytes());
}
let mut session = offline_session();
let h = QueryHandle {
query_id: 9,
statement: "SELECT time, tag, tag FROM t".into(),
columns: vec!["time".into(), "tag".into(), "tag".into()],
data_type_list: vec!["TIMESTAMP".into(), "TEXT".into(), "TEXT".into()],
ignore_time_stamp: true,
query_result: vec![b],
more_data: false,
column_index2_ts_block_column_index_list: Some(vec![-1, 0, 0]),
};
let mut ds = SessionDataSet::new(&mut session, h);
assert_eq!(ds.columns(), ["time", "tag", "tag"]);
assert_eq!(ds.column_types(), ["TIMESTAMP", "TEXT", "TEXT"]);
let row = ds.next_row().unwrap().unwrap();
assert_eq!(
row.values,
vec![
Value::Timestamp(100),
Value::Text("a".into()),
Value::Text("a".into()),
]
);
let row = ds.next_row().unwrap().unwrap();
assert_eq!(row.values[0], Value::Timestamp(200));
assert_eq!(ds.next_row().unwrap(), None);
}
#[test]
fn identity_mapping_when_map_absent() {
let types = [TSDataType::Int32, TSDataType::Int32];
let mut b = header(&types, 1, &[ENCODING_INT32_ARRAY, ENCODING_INT32_ARRAY]);
b.extend_from_slice(&time_column(&[1]));
for v in [11i32, 22] {
b.push(0);
b.extend_from_slice(&v.to_be_bytes());
}
let mut session = offline_session();
let mut h = handle(vec![b], false);
h.columns = vec!["s1".into(), "s2".into()];
h.data_type_list = vec!["INT32".into(), "INT32".into()];
let mut ds = SessionDataSet::new(&mut session, h);
let row = ds.next_row().unwrap().unwrap();
assert_eq!(row.values, vec![Value::Int32(11), Value::Int32(22)]);
}
#[test]
fn out_of_range_map_entry_is_decode_error() {
let mut session = offline_session();
let mut h = handle(vec![int32_block(&[1], &[10])], false);
h.column_index2_ts_block_column_index_list = Some(vec![5]);
let mut ds = SessionDataSet::new(&mut session, h);
assert!(matches!(ds.next_row(), Err(Error::Decode(_))));
}
#[test]
fn corrupt_block_is_decode_error() {
let mut session = offline_session();
let mut ds = SessionDataSet::new(&mut session, handle(vec![vec![0, 1]], false));
assert!(matches!(ds.next_row(), Err(Error::Decode(_))));
}
#[test]
fn explicit_close_stops_iteration() {
let mut session = offline_session();
let blocks = vec![int32_block(&[1, 2], &[10, 20])];
let mut ds = SessionDataSet::new(&mut session, handle(blocks, false));
assert!(ds.next_row().unwrap().is_some());
ds.close();
ds.close(); // idempotent
assert_eq!(ds.next_row().unwrap(), None);
}
}