use std::cell::Cell;
use std::sync::Arc;
use super::cell::OwnedRow;
use super::plane::{EngineHandle, Vtable, fault};
use super::{Error, Result, Row, sys};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tier {
Live,
Atlas,
Shelf,
Subprocess,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Stats {
pub foreign: u64,
pub omitted: u64,
pub files_considered: u64,
pub refreshed: u64,
pub rows: u64,
pub elapsed_ns: u64,
pub tier: Option<Tier>,
}
impl Stats {
fn from_wire(raw: &sys::Stats) -> Self {
Self {
foreign: raw.foreign,
omitted: raw.omitted,
files_considered: raw.files_considered,
refreshed: raw.refreshed,
rows: raw.rows,
elapsed_ns: raw.elapsed_ns,
tier: Some(match raw.source {
sys::SOURCE_ATLAS => Tier::Atlas,
sys::SOURCE_SHELF => Tier::Shelf,
_ => Tier::Live,
}),
}
}
}
pub(super) struct Native {
pub(super) ptr: *mut sys::irgx_rows,
pub(super) vt: &'static Vtable,
pub(super) done: Cell<bool>,
pub(super) _engine: Arc<EngineHandle>,
}
impl Drop for Native {
fn drop(&mut self) {
unsafe { (self.vt.close)(self.ptr) };
}
}
enum Source {
Native(Native),
Materialized {
rows: Vec<OwnedRow>,
at: Cell<usize>,
},
}
pub struct Rows {
source: Source,
summary: Option<Stats>,
}
impl Rows {
pub(super) fn native(native: Native) -> Self {
Self {
source: Source::Native(native),
summary: None,
}
}
pub(crate) fn materialized(rows: Vec<OwnedRow>, summary: Option<Stats>) -> Self {
Self {
source: Source::Materialized {
rows,
at: Cell::new(0),
},
summary,
}
}
pub fn next(&self) -> Option<Result<Row<'_>>> {
match &self.source {
Source::Native(n) => {
if n.done.get() {
return None;
}
let mut raw = blank();
match unsafe { (n.vt.next)(n.ptr, &raw mut raw) } {
sys::MATCH => Some(unsafe { Row::from_wire(&raw) }),
sys::OK => {
n.done.set(true);
None
},
other => {
n.done.set(true);
Some(Err(fault(n.vt, other, "row pull")))
},
}
},
Source::Materialized { rows, at } => {
let row = rows.get(at.get())?;
at.set(at.get() + 1);
Some(view(row))
},
}
}
pub fn batch(&self, size: usize) -> Option<Result<Batch<'_>>> {
let size = size.max(1);
match &self.source {
Source::Native(n) => {
if n.done.get() {
return None;
}
let mut buf = vec![blank(); size];
let mut written = 0usize;
let status = unsafe {
(n.vt.next_batch)(n.ptr, buf.as_mut_ptr(), buf.len(), &raw mut written)
};
match status {
sys::MATCH => Some(
buf[..written.min(size)]
.iter()
.map(|raw| unsafe { Row::from_wire(raw) })
.collect::<Result<Vec<_>>>()
.map(|rows| Batch { rows }),
),
sys::OK => {
n.done.set(true);
None
},
other => {
n.done.set(true);
Some(Err(fault(n.vt, other, "row batch")))
},
}
},
Source::Materialized { rows, at } => {
let start = at.get();
if start >= rows.len() {
return None;
}
let end = start.saturating_add(size).min(rows.len());
at.set(end);
Some(
rows[start..end]
.iter()
.map(view)
.collect::<Result<Vec<_>>>()
.map(|rows| Batch { rows }),
)
},
}
}
pub fn batches(&self, size: usize) -> BatchIter<'_> {
BatchIter { rows: self, size }
}
pub fn iter(&self) -> RowIter<'_> {
RowIter { rows: self }
}
#[must_use]
pub fn stats(&self) -> Stats {
if let Some(s) = self.summary {
return s;
}
match &self.source {
Source::Native(n) => {
let mut raw = sys::Stats {
struct_size: super::struct_size::<sys::Stats>(),
..sys::Stats::default()
};
if unsafe { (n.vt.stats)(n.ptr, &raw mut raw) } == sys::OK {
Stats::from_wire(&raw)
} else {
Stats::default()
}
},
Source::Materialized { rows, .. } => Stats {
rows: rows.len() as u64,
tier: Some(Tier::Subprocess),
..Stats::default()
},
}
}
pub fn to_vec(&self) -> Result<Vec<OwnedRow>> {
self.iter().map(|r| r.map(Row::to_owned)).collect()
}
}
fn view(row: &OwnedRow) -> Result<Row<'_>> {
row.view()
.ok_or_else(|| Error::Decode(format!("no schema {} in this build", row.schema_id)))
}
fn blank() -> sys::Row {
sys::Row {
schema_id: 0,
nvalues: 0,
present: 0,
values: std::ptr::null(),
}
}
pub struct Batch<'a> {
rows: Vec<Row<'a>>,
}
impl<'a> Batch<'a> {
#[must_use]
pub fn rows(&self) -> &[Row<'a>] {
&self.rows
}
}
impl<'a> std::ops::Deref for Batch<'a> {
type Target = [Row<'a>];
fn deref(&self) -> &Self::Target {
&self.rows
}
}
impl<'a> IntoIterator for Batch<'a> {
type Item = Row<'a>;
type IntoIter = std::vec::IntoIter<Row<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.rows.into_iter()
}
}
pub struct BatchIter<'a> {
rows: &'a Rows,
size: usize,
}
impl<'a> Iterator for BatchIter<'a> {
type Item = Result<Batch<'a>>;
fn next(&mut self) -> Option<Self::Item> {
self.rows.batch(self.size)
}
}
pub struct RowIter<'a> {
rows: &'a Rows,
}
impl<'a> Iterator for RowIter<'a> {
type Item = Result<Row<'a>>;
fn next(&mut self) -> Option<Self::Item> {
self.rows.next()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contract::schema::SCHEMAS;
use crate::runtime::cell::OwnedValue;
fn similar(path: &str, distance: f64) -> OwnedRow {
let id = SCHEMAS
.iter()
.find(|s| s.name == "similar")
.map_or(0, |s| s.id);
let mut row = OwnedRow::new(id);
row.set("path", OwnedValue::Text(path.to_owned()));
row.set("distance", OwnedValue::Real(distance));
row
}
fn cursor(n: usize) -> Rows {
Rows::materialized(
(0..n).map(|i| similar(&format!("f{i}.rs"), 0.1)).collect(),
None,
)
}
#[test]
fn batches_partition_the_answer_exactly_once() {
let rows = cursor(7);
let seen: Vec<String> = rows
.batches(3)
.filter_map(std::result::Result::ok)
.flatten()
.filter_map(|r| r.text("path").map(str::to_owned))
.collect();
assert_eq!(seen.len(), 7);
assert_eq!(seen.first().map(String::as_str), Some("f0.rs"));
assert_eq!(seen.last().map(String::as_str), Some("f6.rs"));
}
#[test]
fn a_zero_batch_size_still_makes_progress() {
let rows = cursor(2);
assert_eq!(rows.batches(0).count(), 2);
}
#[test]
fn several_batches_from_one_cursor_coexist() {
let rows = cursor(4);
let first = rows
.batch(2)
.and_then(std::result::Result::ok)
.expect("first batch");
let second = rows
.batch(2)
.and_then(std::result::Result::ok)
.expect("second batch");
assert_eq!((first.rows().len(), second.rows().len()), (2, 2));
assert_eq!(first.first().and_then(|r| r.text("path")), Some("f0.rs"));
assert_eq!(second.first().and_then(|r| r.text("path")), Some("f2.rs"));
assert!(rows.batch(2).is_none(), "the cursor is drained");
}
#[test]
fn a_drained_cursor_reports_its_row_count_and_tier() {
let rows = cursor(3);
assert_eq!(rows.iter().count(), 3);
let stats = rows.stats();
assert_eq!(stats.rows, 3);
assert_eq!(stats.tier, Some(Tier::Subprocess));
}
#[test]
fn a_summary_line_wins_over_the_row_count() {
let summary = Stats {
foreign: 12,
omitted: 5,
tier: Some(Tier::Subprocess),
..Stats::default()
};
let rows = Rows::materialized(vec![similar("a.rs", 0.1)], Some(summary));
assert_eq!((rows.stats().foreign, rows.stats().omitted), (12, 5));
}
#[test]
fn owning_a_row_outlives_the_cursor_it_came_from() {
let owned = {
let rows = cursor(1);
rows.to_vec().expect("materialized rows decode")
};
assert_eq!(
owned
.first()
.and_then(OwnedRow::view)
.and_then(|r| r.text("path")),
Some("f0.rs")
);
}
#[test]
fn a_row_whose_schema_this_build_lacks_is_refused_not_skipped() {
let beyond = u32::try_from(SCHEMAS.len()).unwrap_or(u32::MAX) + 1;
let rows = Rows::materialized(vec![OwnedRow::new(beyond)], None);
let err = rows
.to_vec()
.expect_err("an unknown schema must not decode");
assert!(err.to_string().contains(&beyond.to_string()), "{err}");
}
}