use std::cell::Cell;
use crate::value::{ElementProperty, PropertyMap};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct RenderOptions {
pub columns: Option<Vec<String>>,
pub max_width: Option<usize>,
pub truncate: Option<usize>,
pub row_numbers: bool,
pub max_rows: Option<usize>,
pub max_items: Option<usize>,
#[cfg_attr(feature = "serde", serde(default))]
pub spelling: crate::traversal::Spelling,
}
impl RenderOptions {
pub fn with_spelling(mut self, spelling: crate::traversal::Spelling) -> Self {
self.spelling = spelling;
self
}
pub fn new() -> Self {
Self::default()
}
pub fn unlimited() -> Self {
Self::default()
}
pub fn with_columns<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.columns = Some(columns.into_iter().map(Into::into).collect());
self
}
pub fn with_max_width(mut self, max_width: usize) -> Self {
self.max_width = Some(max_width);
self
}
pub fn with_truncate(mut self, truncate: usize) -> Self {
self.truncate = Some(truncate);
self
}
pub fn with_row_numbers(mut self, row_numbers: bool) -> Self {
self.row_numbers = row_numbers;
self
}
pub fn with_max_rows(mut self, max_rows: usize) -> Self {
self.max_rows = Some(max_rows.max(1));
self
}
pub fn with_max_items(mut self, max_items: usize) -> Self {
self.max_items = Some(max_items.max(1));
self
}
pub fn with_no_limit(mut self) -> Self {
self.max_rows = None;
self.max_items = None;
self
}
pub fn is_bounded(&self) -> bool {
self.max_rows.is_some() || self.max_items.is_some()
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RenderedOutput {
pub text: String,
pub truncation: Option<Truncation>,
}
impl RenderedOutput {
pub fn complete(text: impl Into<String>) -> Self {
Self {
text: text.into(),
truncation: None,
}
}
pub fn new(text: impl Into<String>, truncation: Option<Truncation>) -> Self {
Self {
text: text.into(),
truncation,
}
}
pub fn notice(&self) -> Option<String> {
self.truncation.as_ref().map(Truncation::notice)
}
pub fn into_text(self) -> String {
self.text
}
}
impl std::fmt::Display for RenderedOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.text)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Truncation {
pub rows_shown: usize,
pub more_rows: bool,
pub items_cut: bool,
pub max_items: Option<usize>,
}
impl Truncation {
pub fn of(
rows_shown: usize,
more_rows: bool,
items_cut: bool,
max_items: Option<usize>,
) -> Option<Self> {
(more_rows || items_cut).then_some(Self {
rows_shown,
more_rows,
items_cut,
max_items: if items_cut { max_items } else { None },
})
}
pub fn notice(&self) -> String {
let mut lines = Vec::with_capacity(2);
if self.more_rows {
lines.push(format!(
"… showing the first {} {}; more exist. Use .limit(n), #{{no_limit: true}}, \
/set no-limit (--no-limit), or to_list() for the data.",
self.rows_shown,
if self.rows_shown == 1 { "row" } else { "rows" }
));
}
if self.items_cut {
let items = match self.max_items {
Some(n) => format!("{n} {}", if n == 1 { "item" } else { "items" }),
None => "the item limit".to_string(),
};
lines.push(format!(
"… nested collections were cut to {items}. Use #{{max_items: n}}, \
/set max-items <n> (--max-items <n>), or /set no-limit (--no-limit)."
));
}
lines.join("\n")
}
}
pub(crate) const DISPLAY_MARK: &str = " [display]";
pub(crate) const CUT_MARKER: &str = "…";
pub(crate) struct ItemLimit {
max: Option<usize>,
cut: Cell<bool>,
}
impl ItemLimit {
pub(crate) fn new(max: Option<usize>) -> Self {
Self {
max,
cut: Cell::new(false),
}
}
pub(crate) fn unlimited() -> Self {
Self::new(None)
}
pub(crate) fn keep(&self, len: usize) -> usize {
match self.max {
Some(max) if len > max => {
self.cut.set(true);
max
}
_ => len,
}
}
pub(crate) fn is_limited(&self) -> bool {
self.max.is_some()
}
pub(crate) fn was_cut(&self) -> bool {
self.cut.get()
}
}
pub(crate) fn cut_nested(prop: &ElementProperty, limit: &ItemLimit) -> ElementProperty {
match prop {
ElementProperty::Null => ElementProperty::Null,
ElementProperty::String(s) => ElementProperty::String(s.clone()),
ElementProperty::Compressed(text) => ElementProperty::String(text.text()),
ElementProperty::Number(n) => ElementProperty::Number(n.clone()),
ElementProperty::Boolean(b) => ElementProperty::Boolean(*b),
ElementProperty::Uuid(u) => ElementProperty::Uuid(*u),
ElementProperty::Array(items) => {
let keep = limit.keep(items.len());
ElementProperty::Array(
items
.iter()
.take(keep)
.map(|item| cut_nested(item, limit))
.collect(),
)
}
ElementProperty::Object(obj) => {
ElementProperty::Object(Box::new(cut_entries(obj, limit, true)))
}
ElementProperty::Map(map) => {
let keep = limit.keep(map.len());
ElementProperty::Map(Box::new(
map.iter()
.take(keep)
.map(|(key, value)| (key.clone(), cut_nested(value, limit)))
.collect(),
))
}
ElementProperty::Vertex(obj) => {
ElementProperty::Vertex(Box::new(cut_entries(obj, limit, false)))
}
ElementProperty::Edge(obj) => {
ElementProperty::Edge(Box::new(cut_entries(obj, limit, false)))
}
}
}
fn cut_entries(obj: &PropertyMap, limit: &ItemLimit, cut_keys: bool) -> PropertyMap {
let keep = if cut_keys {
limit.keep(obj.len())
} else {
obj.len()
};
obj.iter()
.take(keep)
.map(|(key, value)| (key.clone(), cut_nested(value, limit)))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn notice_names_rows_and_items() {
let truncation = Truncation::of(100, true, true, Some(100)).unwrap();
let notice = truncation.notice();
assert!(
notice.starts_with("… showing the first 100 rows; more exist."),
"{notice}"
);
assert!(notice.contains("#{no_limit: true}"), "{notice}");
assert!(
notice.contains("… nested collections were cut to 100 items."),
"{notice}"
);
assert_eq!(notice.lines().count(), 2);
}
#[test]
fn nothing_cut_is_no_truncation() {
assert_eq!(Truncation::of(5, false, false, Some(3)), None);
}
#[test]
fn cut_nested_cuts_at_every_depth() {
let inner =
ElementProperty::Array((0..5).map(|i| ElementProperty::from(i as i64)).collect());
let outer = ElementProperty::Array(vec![inner.clone(), inner.clone(), inner]);
let limit = ItemLimit::new(Some(2));
let cut = cut_nested(&outer, &limit);
assert!(limit.was_cut());
match cut {
ElementProperty::Array(items) => {
assert_eq!(items.len(), 2);
assert!(matches!(&items[0], ElementProperty::Array(a) if a.len() == 2));
}
other => panic!("unexpected {other:?}"),
}
}
#[test]
fn error_frames_keep_only_a_failing_display_step() {
use crate::traversal::source::without_display_steps;
let names = |list: &[&str]| list.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let plan = names(&["v()", "math(\"x\")", "limit(101) [display]"]);
assert_eq!(
without_display_steps(plan.clone(), &[1]),
(names(&["v()", "math(\"x\")"]), vec![1])
);
assert_eq!(without_display_steps(plan.clone(), &[2]), (plan, vec![2]));
let plain = names(&["v()", "limit(3)"]);
assert_eq!(without_display_steps(plain.clone(), &[1]), (plain, vec![1]));
}
#[test]
fn display_limit_is_in_the_executed_plan() {
let graph = crate::TraversalGraph::tinkerpop_modern();
let exec = graph
.traversal()
.v(None::<()>)
.out(None::<()>)
.with_display_limit(Some(2), true)
.execute_with(crate::ExecOptions::profile(crate::ProfileType::none()));
assert_eq!(exec.results().len(), 3);
let stats = exec.profile().expect("profile");
let last = stats.metrics().last().expect("steps");
assert_eq!(last.name(), "limit(3) [display]");
assert_eq!(last.count_in(), 6);
assert_eq!(last.counts().traverser_count(), 3);
let rendered = stats.to_string();
assert!(rendered.contains("limit(3) [display]"), "{rendered}");
}
#[test]
fn no_display_limit_after_barriers_or_mutations() {
let graph = crate::TraversalGraph::tinkerpop_modern();
let stats = graph
.traversal()
.v(None::<()>)
.count()
.with_display_limit(Some(2), true)
.profile()
.unwrap();
assert!(
stats
.metrics()
.iter()
.all(|s| !s.name().contains("[display]"))
);
assert!(
stats
.optimizer_rules_applied()
.iter()
.any(|r| r == "count_pushdown")
);
let mut graph = crate::TraversalGraph::tinkerpop_modern();
let stats = graph
.traversal_mut()
.v(None::<()>)
.property("seen", true)
.with_display_limit(Some(2), true)
.profile()
.unwrap();
assert!(
stats
.metrics()
.iter()
.all(|s| !s.name().contains("[display]"))
);
let stats = graph
.traversal()
.v(None::<()>)
.with_display_limit(Some(2), false)
.profile()
.unwrap();
assert!(
stats
.metrics()
.iter()
.all(|s| !s.name().contains("[display]"))
);
}
#[test]
fn zero_limits_are_raised_to_one() {
let options = RenderOptions::new().with_max_rows(0).with_max_items(0);
assert_eq!(options.max_rows, Some(1));
assert_eq!(options.max_items, Some(1));
assert!(!options.with_no_limit().is_bounded());
}
}