use crate::query::ObjectVisitor;
use crate::query::ast::{
AggFunc, ArithOp, Attr, CompareOp, Expr, FromSource, Query, SelectItem, UnaryOp, Value,
};
use crate::query::carry::Carry;
use crate::query::model::{QueryColumn, QueryResult, QueryValue};
use crate::query::plan::QueryPlan;
pub struct CrossPhaseEntry {
pub slot: usize,
pub name: String,
pub plan: QueryPlan,
pub carry: Carry,
}
#[derive(Default)]
pub struct QueryExecState {
finished: Vec<(usize, QueryResult)>,
pending: Vec<CrossPhaseEntry>,
row_src_by_slot: std::collections::HashMap<usize, Vec<u32>>,
}
impl QueryExecState {
pub fn new() -> Self {
Self::default()
}
pub fn push_finished(&mut self, slot: usize, r: QueryResult) {
self.finished.push((slot, r));
}
pub fn push_finished_with_src(&mut self, slot: usize, r: QueryResult, src: Option<Vec<u32>>) {
self.finished.push((slot, r));
if let Some(v) = src {
self.row_src_by_slot.insert(slot, v);
}
}
pub fn push_cross_phase(&mut self, slot: usize, name: String, plan: QueryPlan, carry: Carry) {
self.pending.push(CrossPhaseEntry {
slot,
name,
plan,
carry,
});
}
#[allow(dead_code)]
pub fn finished_len(&self) -> usize {
self.finished.len()
}
#[allow(dead_code)]
pub fn pending_len(&self) -> usize {
self.pending.len()
}
#[allow(dead_code)]
pub fn pending(&self) -> &[CrossPhaseEntry] {
&self.pending
}
pub fn has_pending(&self) -> bool {
!self.pending.is_empty()
}
pub fn into_parts(self) -> (Vec<(usize, QueryResult)>, Vec<CrossPhaseEntry>) {
(self.finished, self.pending)
}
pub fn take_row_src_by_slot(&mut self) -> std::collections::HashMap<usize, Vec<u32>> {
std::mem::take(&mut self.row_src_by_slot)
}
pub fn push_cross_phase_entry(&mut self, entry: CrossPhaseEntry) {
self.pending.push(entry);
}
}
pub trait ClassResolver {
fn class_name(&self, class_id: u64) -> Option<&str>;
fn is_instance_of(
&self,
class_id: u64,
spec: &crate::query::ast::ClassSpec,
from_regex: Option<®ex::Regex>,
) -> bool {
match self.class_name(class_id) {
Some(name) => class_name_matches_spec(name, spec, from_regex),
None => false,
}
}
fn field(&self, _class_id: u64, _name: &str) -> Option<(u32, crate::types::HprofType)> {
None
}
fn addr_of(&self, _src_idx: usize) -> Option<u64> {
None
}
fn shallow_of(&self, _src_idx: usize) -> Option<u32> {
None
}
fn index_of_addr(&self, _addr: u64) -> Option<usize> {
None
}
fn ref_width(&self) -> usize {
8
}
}
#[cfg(test)]
pub struct TestSchema {
pub names: std::collections::HashMap<u64, String>,
}
#[cfg(test)]
impl ClassResolver for TestSchema {
fn class_name(&self, class_id: u64) -> Option<&str> {
self.names.get(&class_id).map(|s| s.as_str())
}
}
#[allow(clippy::type_complexity)]
pub struct SingleScanExecutor<'a, R: ClassResolver> {
query: &'a Query,
plan: &'a QueryPlan,
resolver: &'a R,
rows: Vec<Vec<QueryValue>>,
matched: u64,
truncated: bool,
carry: Option<Carry>,
in_sets: Vec<InSet>,
from_regex: Option<regex::Regex>,
like_regexes: std::collections::HashMap<String, regex::Regex>,
agg_acc: Option<Vec<AggAcc>>,
target_index: Option<usize>,
row_src: Option<Vec<u32>>,
group_map: Option<std::collections::HashMap<String, (Vec<QueryValue>, Vec<AggAcc>)>>,
exists_bools: Vec<bool>,
exists_cursor: std::cell::Cell<usize>,
}
pub(crate) enum AggAcc {
None,
CountStar { n: i64 },
CountExpr { n: i64 },
Sum { total: QueryValue, any_value: bool },
Avg { sum: f64, count: i64 },
Min { best: Option<QueryValue> },
Max { best: Option<QueryValue> },
Percentile { p: u8, values: Vec<f64> },
}
pub(crate) fn init_agg_acc(item: &SelectItem) -> AggAcc {
match item {
SelectItem::Aggregate { func, arg } => match func {
AggFunc::Count => {
if matches!(arg.as_ref(), SelectItem::Star) {
AggAcc::CountStar { n: 0 }
} else {
AggAcc::CountExpr { n: 0 }
}
}
AggFunc::Sum => AggAcc::Sum {
total: QueryValue::Int(0),
any_value: false,
},
AggFunc::Avg => AggAcc::Avg { sum: 0.0, count: 0 },
AggFunc::Min => AggAcc::Min { best: None },
AggFunc::Max => AggAcc::Max { best: None },
AggFunc::Percentile(p) => AggAcc::Percentile {
p: *p,
values: Vec::new(),
},
AggFunc::Median => AggAcc::Percentile {
p: 50,
values: Vec::new(),
},
},
_ => AggAcc::None,
}
}
pub(crate) fn fold_agg_acc(acc: &mut AggAcc, value: QueryValue) {
match acc {
AggAcc::None => {}
AggAcc::CountStar { n } => *n += 1,
AggAcc::CountExpr { n } => {
if !matches!(value, QueryValue::Null) {
*n += 1;
}
}
AggAcc::Sum { total, any_value } => {
match &value {
QueryValue::Int(v) => {
*any_value = true;
*total = match total {
QueryValue::Int(t) => QueryValue::Int(t.wrapping_add(*v)),
QueryValue::Float(t) => QueryValue::Float(*t + *v as f64),
_ => QueryValue::Int(*v),
};
}
QueryValue::Float(v) => {
*any_value = true;
*total = match total {
QueryValue::Int(t) => QueryValue::Float(*t as f64 + v),
QueryValue::Float(t) => QueryValue::Float(*t + v),
_ => QueryValue::Float(*v),
};
}
_ => {}
}
}
AggAcc::Avg { sum, count } => {
match value {
QueryValue::Int(v) => {
*sum += v as f64;
*count += 1;
}
QueryValue::Float(v) => {
*sum += v;
*count += 1;
}
_ => {}
}
}
AggAcc::Min { best } => {
let candidate = match &value {
QueryValue::Int(_) | QueryValue::Float(_) => Some(value),
_ => None,
};
if let Some(c) = candidate {
*best = match best.take() {
None => Some(c),
Some(prev) => {
let prev_lt = match (&prev, &c) {
(QueryValue::Int(a), QueryValue::Int(b)) => *a <= *b,
(QueryValue::Int(a), QueryValue::Float(b)) => (*a as f64) <= *b,
(QueryValue::Float(a), QueryValue::Int(b)) => *a <= (*b as f64),
(QueryValue::Float(a), QueryValue::Float(b)) => *a <= *b,
_ => true,
};
Some(if prev_lt { prev } else { c })
}
};
}
}
AggAcc::Max { best } => {
let candidate = match &value {
QueryValue::Int(_) | QueryValue::Float(_) => Some(value),
_ => None,
};
if let Some(c) = candidate {
*best = match best.take() {
None => Some(c),
Some(prev) => {
let prev_gt = match (&prev, &c) {
(QueryValue::Int(a), QueryValue::Int(b)) => *a >= *b,
(QueryValue::Int(a), QueryValue::Float(b)) => (*a as f64) >= *b,
(QueryValue::Float(a), QueryValue::Int(b)) => *a >= (*b as f64),
(QueryValue::Float(a), QueryValue::Float(b)) => *a >= *b,
_ => true,
};
Some(if prev_gt { prev } else { c })
}
};
}
}
AggAcc::Percentile { values, .. } => {
match value {
QueryValue::Int(v) => values.push(v as f64),
QueryValue::Float(v) => values.push(v),
_ => {}
}
}
}
}
pub(crate) fn finalize_agg_acc(acc: AggAcc) -> QueryValue {
match acc {
AggAcc::None => QueryValue::Null,
AggAcc::CountStar { n } => QueryValue::Int(n),
AggAcc::CountExpr { n } => QueryValue::Int(n),
AggAcc::Sum { total, any_value } => {
if any_value {
total
} else {
QueryValue::Int(0)
}
}
AggAcc::Avg { sum, count } => {
if count > 0 {
QueryValue::Float(sum / count as f64)
} else {
QueryValue::Null
}
}
AggAcc::Min { best } => best.unwrap_or(QueryValue::Null),
AggAcc::Max { best } => best.unwrap_or(QueryValue::Null),
AggAcc::Percentile { p, mut values } => {
if values.is_empty() {
return QueryValue::Null;
}
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = values.len();
let rank = ((p as f64 / 100.0) * n as f64).ceil() as usize;
let idx = rank.saturating_sub(1).min(n - 1);
QueryValue::Float(values[idx])
}
}
}
pub(crate) fn eval_having_term(
pred: &crate::query::ast::Predicate,
row: &[QueryValue],
query: &Query,
columns: &[QueryColumn],
like_regexes: &std::collections::HashMap<String, regex::Regex>,
) -> bool {
use crate::query::ast::Predicate as P;
match pred {
P::And(a, b) => {
eval_having_term(a, row, query, columns, like_regexes)
&& eval_having_term(b, row, query, columns, like_regexes)
}
P::Or(a, b) => {
eval_having_term(a, row, query, columns, like_regexes)
|| eval_having_term(b, row, query, columns, like_regexes)
}
P::Not(inner) => !eval_having_term(inner, row, query, columns, like_regexes),
P::Compare { lhs, op, rhs } => {
let lv = eval_having_expr(lhs, row, query, columns, like_regexes);
let rv = eval_having_expr(rhs, row, query, columns, like_regexes);
let like_re = if matches!(op, CompareOp::Like | CompareOp::NotLike) {
rhs.as_lit().and_then(|v| {
if let Value::Str(pat) = v {
like_regexes.get(pat.as_str())
} else {
None
}
})
} else {
None
};
compare_values(&lv, *op, &rv, like_re)
}
_ => true,
}
}
fn eval_having_expr(
e: &Expr,
row: &[QueryValue],
query: &Query,
columns: &[QueryColumn],
like_regexes: &std::collections::HashMap<String, regex::Regex>,
) -> QueryValue {
match e {
Expr::Lit(v) => match v {
Value::Int(n) => QueryValue::Int(*n),
Value::Float(f) => QueryValue::Float(*f),
Value::Str(s) => QueryValue::Str(s.clone()),
Value::Bool(b) => QueryValue::Bool(*b),
Value::Null => QueryValue::Null,
},
Expr::Aggregate { func, arg } => {
let pos = query.select.iter().position(|it| match it {
SelectItem::Aggregate { func: f, arg: a } => f == func && a == arg,
_ => false,
});
pos.and_then(|i| row.get(i))
.cloned()
.unwrap_or(QueryValue::Null)
}
Expr::Attr(attr) => {
let name = attr_name(attr);
columns
.iter()
.position(|c| c.name == name)
.and_then(|i| row.get(i))
.cloned()
.unwrap_or(QueryValue::Null)
}
Expr::Binary { op, lhs, rhs } => {
let l = eval_having_expr(lhs, row, query, columns, like_regexes);
let r = eval_having_expr(rhs, row, query, columns, like_regexes);
arith(&l, *op, &r)
}
Expr::Unary { op, arg } => unary(
*op,
&eval_having_expr(arg, row, query, columns, like_regexes),
),
Expr::Method { .. } => QueryValue::Null,
Expr::Case { branches, else_ } => {
for (pred, then_expr) in branches {
if eval_having_term(pred, row, query, columns, like_regexes) {
return eval_having_expr(then_expr, row, query, columns, like_regexes);
}
}
match else_ {
Some(e) => eval_having_expr(e, row, query, columns, like_regexes),
None => QueryValue::Null,
}
}
Expr::Coalesce(args) => {
for arg in args {
let v = eval_having_expr(arg, row, query, columns, like_regexes);
if !matches!(v, QueryValue::Null) {
return v;
}
}
QueryValue::Null
}
Expr::NullIf { lhs, rhs } => {
let lv = eval_having_expr(lhs, row, query, columns, like_regexes);
let rv = eval_having_expr(rhs, row, query, columns, like_regexes);
if lv == rv { QueryValue::Null } else { lv }
}
}
}
pub struct InSet {
pub lhs: Attr,
pub set: std::collections::HashSet<u64>,
pub truncated: bool,
}
fn compile_from_query(query: &Query) -> Option<regex::Regex> {
query
.from
.class_spec()
.and_then(|spec| compile_from_regex(spec).ok().flatten())
}
fn compile_like_for_query(query: &Query) -> std::collections::HashMap<String, regex::Regex> {
compile_like_regexes(query).unwrap_or_default()
}
impl<'a, R: ClassResolver> SingleScanExecutor<'a, R> {
pub fn new(query: &'a Query, plan: &'a QueryPlan, resolver: &'a R) -> Self {
let agg_acc = if plan.kind != crate::query::plan::StageKind::GroupBy
&& query
.select
.iter()
.any(|it| matches!(it, SelectItem::Aggregate { .. }))
{
Some(query.select.iter().map(init_agg_acc).collect())
} else {
None
};
let group_map = if plan.kind == crate::query::plan::StageKind::GroupBy {
Some(std::collections::HashMap::new())
} else {
None
};
let target_index = if let FromSource::Object(addr) = &query.from {
resolver.index_of_addr(*addr)
} else {
None
};
Self {
query,
plan,
resolver,
rows: Vec::new(),
matched: 0,
truncated: false,
carry: None,
in_sets: Vec::new(),
from_regex: compile_from_query(query),
like_regexes: compile_like_for_query(query),
agg_acc,
target_index,
row_src: None,
group_map,
exists_bools: Vec::new(),
exists_cursor: std::cell::Cell::new(0),
}
}
pub fn new_carry(query: &'a Query, plan: &'a QueryPlan, resolver: &'a R, carry: Carry) -> Self {
let target_index = if let FromSource::Object(addr) = &query.from {
resolver.index_of_addr(*addr)
} else {
None
};
Self {
query,
plan,
resolver,
rows: Vec::new(),
matched: 0,
truncated: false,
carry: Some(carry),
in_sets: Vec::new(),
from_regex: compile_from_query(query),
like_regexes: compile_like_for_query(query),
agg_acc: None,
target_index,
row_src: None,
group_map: None,
exists_bools: Vec::new(),
exists_cursor: std::cell::Cell::new(0),
}
}
pub fn set_in_subquery_sets(&mut self, sets: Vec<InSet>) {
if sets.iter().any(|s| s.truncated) {
self.truncated = true;
}
self.in_sets = sets;
}
pub fn set_exists_results(&mut self, bools: Vec<bool>) {
self.exists_bools = bools;
}
pub fn is_carry(&self) -> bool {
self.carry.is_some()
}
pub fn arm_row_capture(&mut self) {
if self.carry.is_none() && self.agg_acc.is_none() {
self.row_src = Some(Vec::new());
}
}
pub fn wants_arrays(&self) -> bool {
if self.query.from.as_subquery().is_some() {
return true;
}
if matches!(self.query.from, FromSource::Object(_)) {
return true;
}
let from = self.query.from.class_name();
if self.from_regex.is_some() {
return true;
}
from.ends_with("[]") || from.contains('*')
}
pub fn plan(&self) -> &QueryPlan {
self.plan
}
pub fn query(&self) -> &Query {
self.query
}
pub fn resolver(&self) -> &'a R {
self.resolver
}
pub fn take_carry(self) -> Carry {
self.carry.expect("take_carry on a non-carry executor")
}
fn class_matches(&self, class_id: u64) -> bool {
if self.query.from.as_subquery().is_some() {
return true;
}
let Some(spec) = self.query.from.class_spec() else {
return true;
};
if spec.instanceof {
return self
.resolver
.is_instance_of(class_id, spec, self.from_regex.as_ref());
}
match self.resolver.class_name(class_id) {
None => false,
Some(name) => class_name_matches_spec(name, spec, self.from_regex.as_ref()),
}
}
fn strip_alias<'n>(&self, name: &'n str) -> &'n str {
if let Some(alias) = &self.query.alias {
if let Some(rest) = name.strip_prefix(alias.as_str()) {
if let Some(field) = rest.strip_prefix('.') {
return field;
}
}
}
name
}
fn project_row(&self, src_idx: usize, class_id: u64, blob: &[u8]) -> Vec<QueryValue> {
self.query
.select
.iter()
.map(|item| self.project_item(item, src_idx, class_id, blob))
.collect()
}
fn project_item(
&self,
item: &SelectItem,
src_idx: usize,
class_id: u64,
blob: &[u8],
) -> QueryValue {
match item {
SelectItem::Star => QueryValue::ObjRef {
index: src_idx as u64,
class: self
.resolver
.class_name(class_id)
.unwrap_or("?")
.to_string(),
addr: self.resolver.addr_of(src_idx),
},
SelectItem::Aggregate { .. } => QueryValue::Null,
SelectItem::Attr(a) => self.project_attr(a, src_idx, class_id, blob),
SelectItem::Path { .. } => QueryValue::Null,
SelectItem::ToString(_) => self.tostring_display(class_id, src_idx),
SelectItem::Expr(e) => self.eval_expr(e, src_idx, class_id, blob),
}
}
#[allow(clippy::wrong_self_convention)]
fn from_is_string(&self) -> bool {
crate::query::plan::is_string_class_name(self.query.from.class_name())
}
fn tostring_display(&self, class_id: u64, src_idx: usize) -> QueryValue {
if self.from_is_string() {
return QueryValue::Null;
}
let cname = self.resolver.class_name(class_id).unwrap_or("?");
self.tostring_display_named(cname, src_idx)
}
fn tostring_display_named(&self, class_name: &str, src_idx: usize) -> QueryValue {
if self.from_is_string() {
return QueryValue::Null;
}
match self.resolver.addr_of(src_idx) {
Some(a) => QueryValue::Str(format!("{class_name} @ 0x{a:x}")),
None => QueryValue::Str(format!("{class_name} @ ?")),
}
}
fn project_attr(&self, a: &Attr, src_idx: usize, class_id: u64, blob: &[u8]) -> QueryValue {
match a {
Attr::ObjectId => QueryValue::Int(src_idx as i64),
Attr::ObjectAddress => self
.resolver
.addr_of(src_idx)
.map(|x| QueryValue::Int(x as i64))
.unwrap_or(QueryValue::Null),
Attr::UsedHeapSize => self
.resolver
.shallow_of(src_idx)
.map(|x| QueryValue::Int(x as i64))
.unwrap_or(QueryValue::Null),
Attr::RetainedHeapSize => QueryValue::Null,
Attr::Dominators(_) | Attr::DominatorOf(_) => QueryValue::Null,
Attr::ClassOf | Attr::DisplayName => QueryValue::Str(
self.resolver
.class_name(class_id)
.unwrap_or("?")
.to_string(),
),
Attr::Length => QueryValue::Null,
Attr::Inbounds | Attr::Outbounds => QueryValue::Null,
Attr::Field(name) => self.decode_field(class_id, name, blob),
Attr::RefPath { .. } => QueryValue::Null,
Attr::ValueArray | Attr::ReferenceArray => QueryValue::Null,
Attr::GcRoots | Attr::GcRootInfo => QueryValue::Null,
Attr::ArrayIndex { .. } | Attr::ArraySlice { .. } => QueryValue::Null,
Attr::ToString(_) => {
self.tostring_display(class_id, src_idx)
}
Attr::ToHex(inner) => match self.eval_expr(inner, src_idx, class_id, blob) {
QueryValue::Int(n) => QueryValue::Str(format!("0x{:x}", n as u64)),
_ => QueryValue::Null,
},
}
}
fn project_array_row(&self, src_idx: usize, class_name: &str, length: u32) -> Vec<QueryValue> {
self.query
.select
.iter()
.map(|item| self.project_array_item(item, src_idx, class_name, length))
.collect()
}
fn project_array_item(
&self,
item: &SelectItem,
src_idx: usize,
class_name: &str,
length: u32,
) -> QueryValue {
match item {
SelectItem::Star => QueryValue::ObjRef {
index: src_idx as u64,
class: class_name.to_string(),
addr: self.resolver.addr_of(src_idx),
},
SelectItem::Aggregate { .. } => QueryValue::Null,
SelectItem::Attr(a) => self.project_array_attr(a, src_idx, class_name, length),
SelectItem::Path { .. } => QueryValue::Null,
SelectItem::ToString(_) => self.tostring_display_named(class_name, src_idx),
SelectItem::Expr(e) => self.eval_expr_array(e, src_idx, class_name, length),
}
}
fn project_array_attr(
&self,
a: &Attr,
src_idx: usize,
class_name: &str,
length: u32,
) -> QueryValue {
match a {
Attr::ObjectId => QueryValue::Int(src_idx as i64),
Attr::ObjectAddress => self
.resolver
.addr_of(src_idx)
.map(|x| QueryValue::Int(x as i64))
.unwrap_or(QueryValue::Null),
Attr::UsedHeapSize => self
.resolver
.shallow_of(src_idx)
.map(|x| QueryValue::Int(x as i64))
.unwrap_or(QueryValue::Null),
Attr::RetainedHeapSize => QueryValue::Null,
Attr::Dominators(_) | Attr::DominatorOf(_) => QueryValue::Null,
Attr::ClassOf | Attr::DisplayName => QueryValue::Str(class_name.to_string()),
Attr::Length => QueryValue::Int(length as i64),
Attr::Inbounds | Attr::Outbounds => QueryValue::Null,
Attr::Field(_) => QueryValue::Null,
Attr::RefPath { .. } => QueryValue::Null,
Attr::ValueArray => QueryValue::Null,
Attr::ReferenceArray => QueryValue::ObjRef {
index: src_idx as u64,
class: class_name.to_string(),
addr: self.resolver.addr_of(src_idx),
},
Attr::GcRoots | Attr::GcRootInfo => QueryValue::Null,
Attr::ArrayIndex { .. } | Attr::ArraySlice { .. } => QueryValue::Null,
Attr::ToString(_) => {
self.tostring_display_named(class_name, src_idx)
}
Attr::ToHex(inner) => match self.eval_expr_array(inner, src_idx, class_name, length) {
QueryValue::Int(n) => QueryValue::Str(format!("0x{:x}", n as u64)),
_ => QueryValue::Null,
},
}
}
fn eval_expr(&self, e: &Expr, src_idx: usize, class_id: u64, blob: &[u8]) -> QueryValue {
match e {
Expr::Attr(a) => self.project_attr(a, src_idx, class_id, blob),
Expr::Lit(v) => value_to_qv(v),
Expr::Binary { op, lhs, rhs } => {
let l = self.eval_expr(lhs, src_idx, class_id, blob);
let r = self.eval_expr(rhs, src_idx, class_id, blob);
arith(&l, *op, &r)
}
Expr::Unary { op, arg } => unary(*op, &self.eval_expr(arg, src_idx, class_id, blob)),
Expr::Method {
receiver,
name,
args,
} => self.dispatch_method(receiver, name, args, src_idx, class_id, blob),
Expr::Aggregate { .. } => QueryValue::Null,
Expr::Case { branches, else_ } => {
for (cond, then_expr) in branches {
if self.eval_pred(cond, src_idx, class_id, blob) {
return self.eval_expr(then_expr, src_idx, class_id, blob);
}
}
match else_ {
Some(e) => self.eval_expr(e, src_idx, class_id, blob),
None => QueryValue::Null,
}
}
Expr::Coalesce(args) => {
for arg in args {
let v = self.eval_expr(arg, src_idx, class_id, blob);
if !matches!(v, QueryValue::Null) {
return v;
}
}
QueryValue::Null
}
Expr::NullIf { lhs, rhs } => {
let lv = self.eval_expr(lhs, src_idx, class_id, blob);
let rv = self.eval_expr(rhs, src_idx, class_id, blob);
if lv == rv { QueryValue::Null } else { lv }
}
}
}
fn eval_expr_array(
&self,
e: &Expr,
src_idx: usize,
class_name: &str,
length: u32,
) -> QueryValue {
match e {
Expr::Attr(a) => self.project_array_attr(a, src_idx, class_name, length),
Expr::Lit(v) => value_to_qv(v),
Expr::Binary { op, lhs, rhs } => {
let l = self.eval_expr_array(lhs, src_idx, class_name, length);
let r = self.eval_expr_array(rhs, src_idx, class_name, length);
arith(&l, *op, &r)
}
Expr::Unary { op, arg } => {
unary(*op, &self.eval_expr_array(arg, src_idx, class_name, length))
}
Expr::Method { name, .. } => match name.as_str() {
"length" | "size" => QueryValue::Int(length as i64),
_ => QueryValue::Null,
},
Expr::Aggregate { .. } => QueryValue::Null,
Expr::Case { branches, else_ } => {
for (cond, then_expr) in branches {
if self.array_eval_pred(cond, src_idx, class_name, length) {
return self.eval_expr_array(then_expr, src_idx, class_name, length);
}
}
match else_ {
Some(e) => self.eval_expr_array(e, src_idx, class_name, length),
None => QueryValue::Null,
}
}
Expr::Coalesce(args) => {
for arg in args {
let v = self.eval_expr_array(arg, src_idx, class_name, length);
if !matches!(v, QueryValue::Null) {
return v;
}
}
QueryValue::Null
}
Expr::NullIf { lhs, rhs } => {
let lv = self.eval_expr_array(lhs, src_idx, class_name, length);
let rv = self.eval_expr_array(rhs, src_idx, class_name, length);
if lv == rv { QueryValue::Null } else { lv }
}
}
}
fn dispatch_method(
&self,
receiver: &Expr,
name: &str,
args: &[Expr],
src_idx: usize,
class_id: u64,
blob: &[u8],
) -> QueryValue {
match name {
"getName" => self.project_attr(&Attr::DisplayName, src_idx, class_id, blob),
"getObjectAddress" => self.project_attr(&Attr::ObjectAddress, src_idx, class_id, blob),
"getObjectId" => self.project_attr(&Attr::ObjectId, src_idx, class_id, blob),
"getUsedHeapSize" => self.project_attr(&Attr::UsedHeapSize, src_idx, class_id, blob),
"getRetainedHeapSize" => {
self.project_attr(&Attr::RetainedHeapSize, src_idx, class_id, blob)
}
"getClazz" => self.project_attr(&Attr::ClassOf, src_idx, class_id, blob),
"toString" => {
self.project_attr(&Attr::ToString(String::new()), src_idx, class_id, blob)
}
"length" => self.project_attr(&Attr::Length, src_idx, class_id, blob),
_ => self.emulate_jvm_method(receiver, name, args, src_idx, class_id, blob),
}
}
fn emulate_jvm_method(
&self,
receiver: &Expr,
name: &str,
args: &[Expr],
src_idx: usize,
class_id: u64,
blob: &[u8],
) -> QueryValue {
let cname = self.resolver.class_name(class_id).unwrap_or("");
match (name, cname) {
("intValue" | "longValue" | "shortValue" | "byteValue", c) if is_boxed_integral(c) => {
self.decode_field(class_id, "value", blob)
}
("floatValue" | "doubleValue", c) if is_boxed_fp(c) => {
self.decode_field(class_id, "value", blob)
}
("booleanValue", "java.lang.Boolean") => self.decode_field(class_id, "value", blob),
("charValue", "java.lang.Character") => self.decode_field(class_id, "value", blob),
("size", c) if is_sized_collection(c) => self.decode_field(class_id, "size", blob),
("equals", _) => {
let recv = self.eval_expr(receiver, src_idx, class_id, blob);
let arg = args
.first()
.map(|a| self.eval_expr(a, src_idx, class_id, blob))
.unwrap_or(QueryValue::Null);
QueryValue::Bool(qv_value_eq(&recv, &arg))
}
("contains", "java.lang.String") => {
let recv = self.eval_expr(receiver, src_idx, class_id, blob);
let arg = args
.first()
.map(|a| self.eval_expr(a, src_idx, class_id, blob));
match (recv, arg) {
(QueryValue::Str(hay), Some(QueryValue::Str(needle))) => {
QueryValue::Bool(hay.contains(&needle))
}
_ => QueryValue::Null,
}
}
_ => QueryValue::Null, }
}
fn decode_field(&self, class_id: u64, name: &str, blob: &[u8]) -> QueryValue {
use crate::types::HprofType;
let name = self.strip_alias(name);
let Some((off, ty)) = self.resolver.field(class_id, name) else {
return QueryValue::Null;
};
let o = off as usize;
match ty {
HprofType::Boolean | HprofType::Byte => blob
.get(o)
.map(|&b| {
if ty == HprofType::Boolean {
QueryValue::Bool(b != 0)
} else {
QueryValue::Int(b as i64)
}
})
.unwrap_or(QueryValue::Null),
HprofType::Short => read_be(blob, o, 2)
.map(|v| QueryValue::Int(v as i16 as i64))
.unwrap_or(QueryValue::Null),
HprofType::Char => read_be(blob, o, 2)
.map(|v| QueryValue::Int(v as i64))
.unwrap_or(QueryValue::Null),
HprofType::Int => read_be(blob, o, 4)
.map(|v| QueryValue::Int(v as i32 as i64))
.unwrap_or(QueryValue::Null),
HprofType::Long => read_be(blob, o, 8)
.map(|v| QueryValue::Int(v as i64))
.unwrap_or(QueryValue::Null),
HprofType::Float => read_be(blob, o, 4)
.map(|v| QueryValue::Float(f32::from_bits(v as u32) as f64))
.unwrap_or(QueryValue::Null),
HprofType::Double => read_be(blob, o, 8)
.map(|v| QueryValue::Float(f64::from_bits(v)))
.unwrap_or(QueryValue::Null),
HprofType::Object => QueryValue::Null,
}
}
fn where_passes(&self, src_idx: usize, class_id: u64, blob: &[u8]) -> bool {
self.exists_cursor.set(0);
for term in &self.plan.where_terms {
if self.carry.is_some()
&& (crate::query::plan::pred_uses_retained(&term.pred)
|| crate::query::plan::pred_uses_refpath(&term.pred)
|| (self.from_is_string()
&& crate::query::plan::pred_uses_tostring(&term.pred)))
{
continue;
}
if !self.eval_pred(&term.pred, src_idx, class_id, blob) {
return false;
}
}
true
}
fn like_re_for(&self, rhs: &Value) -> Option<®ex::Regex> {
match rhs {
Value::Str(pat) => self.like_regexes.get(pat),
_ => None,
}
}
fn eval_pred(
&self,
pred: &crate::query::ast::Predicate,
src_idx: usize,
class_id: u64,
blob: &[u8],
) -> bool {
use crate::query::ast::Predicate as P;
match pred {
P::And(a, b) => {
self.eval_pred(a, src_idx, class_id, blob)
&& self.eval_pred(b, src_idx, class_id, blob)
}
P::Or(a, b) => {
self.eval_pred(a, src_idx, class_id, blob)
|| self.eval_pred(b, src_idx, class_id, blob)
}
P::Not(a) => !self.eval_pred(a, src_idx, class_id, blob),
P::InstanceOf(cname) => {
let spec = crate::query::ast::ClassSpec {
instanceof: true,
class_name: cname.clone(),
is_regex: false,
};
self.resolver.is_instance_of(class_id, &spec, None)
}
P::InSubquery { lhs, .. } => self.eval_in_subquery(lhs, src_idx),
P::Exists { .. } => {
let idx = self.exists_cursor.get();
self.exists_cursor.set(idx + 1);
self.exists_bools.get(idx).copied().unwrap_or(false)
}
P::Compare { lhs, op, rhs } => {
let lv = self.eval_expr(lhs, src_idx, class_id, blob);
let rv = self.eval_expr(rhs, src_idx, class_id, blob);
let like_re = rhs.as_lit().and_then(|v| self.like_re_for(v));
compare_values(&lv, *op, &rv, like_re)
}
}
}
fn eval_in_subquery(&self, lhs: &Attr, src_idx: usize) -> bool {
let Some(inset) = self.in_sets.iter().find(|s| &s.lhs == lhs) else {
return in_subquery_unresolved();
};
match self.resolver.addr_of(src_idx) {
Some(addr) => crate::query::run::in_subquery_contains(&inset.set, addr),
None => false,
}
}
fn array_where_passes(&self, src_idx: usize, class_name: &str, length: u32) -> bool {
self.exists_cursor.set(0);
for term in &self.plan.where_terms {
if self.carry.is_some()
&& (crate::query::plan::pred_uses_retained(&term.pred)
|| crate::query::plan::pred_uses_refpath(&term.pred)
|| (self.from_is_string()
&& crate::query::plan::pred_uses_tostring(&term.pred)))
{
continue;
}
if !self.array_eval_pred(&term.pred, src_idx, class_name, length) {
return false;
}
}
true
}
fn array_eval_pred(
&self,
pred: &crate::query::ast::Predicate,
src_idx: usize,
class_name: &str,
length: u32,
) -> bool {
use crate::query::ast::Predicate as P;
match pred {
P::And(a, b) => {
self.array_eval_pred(a, src_idx, class_name, length)
&& self.array_eval_pred(b, src_idx, class_name, length)
}
P::Or(a, b) => {
self.array_eval_pred(a, src_idx, class_name, length)
|| self.array_eval_pred(b, src_idx, class_name, length)
}
P::Not(a) => !self.array_eval_pred(a, src_idx, class_name, length),
P::InstanceOf(cname) => class_name_matches(class_name, cname),
P::InSubquery { lhs, .. } => self.eval_in_subquery(lhs, src_idx),
P::Exists { .. } => {
let idx = self.exists_cursor.get();
self.exists_cursor.set(idx + 1);
self.exists_bools.get(idx).copied().unwrap_or(false)
}
P::Compare { lhs, op, rhs } => {
let lv = self.eval_expr_array(lhs, src_idx, class_name, length);
let rv = self.eval_expr_array(rhs, src_idx, class_name, length);
let like_re = rhs.as_lit().and_then(|v| self.like_re_for(v));
compare_values(&lv, *op, &rv, like_re)
}
}
}
pub fn finish(self, name: &str) -> QueryResult {
self.finish_with_src(name).0
}
pub fn finish_with_src(self, name: &str) -> (QueryResult, Option<Vec<u32>>) {
let columns = query_columns(self.query);
if let Some(group_map) = self.group_map {
let mut rows: Vec<Vec<QueryValue>> = Vec::with_capacity(group_map.len());
for (_key_str, (key, accs)) in group_map {
let finalized: Vec<QueryValue> = accs.into_iter().map(finalize_agg_acc).collect();
let row: Vec<QueryValue> = self
.query
.select
.iter()
.enumerate()
.map(|(i, item)| match item {
SelectItem::Aggregate { .. } => {
finalized.get(i).cloned().unwrap_or(QueryValue::Null)
}
_ => {
let col_name = column_name(item);
let gb_match =
self.plan.group_by_exprs.iter().enumerate().find(|(_, ge)| {
let ge_name = expr_name(ge);
ge_name == col_name
|| match (ge, item) {
(Expr::Attr(ga), SelectItem::Attr(a)) => ga == a,
(Expr::Attr(ga), SelectItem::Expr(e)) => {
matches!(e.as_ref(), Expr::Attr(ea) if ea == ga)
}
_ => false,
}
});
match gb_match {
Some((j, _)) => key.get(j).cloned().unwrap_or(QueryValue::Null),
None => key.first().cloned().unwrap_or(QueryValue::Null),
}
}
})
.collect();
let having_ok = self.plan.having_terms.iter().all(|term| {
eval_having_term(&term.pred, &row, self.query, &columns, &self.like_regexes)
});
if having_ok {
rows.push(row);
}
}
if let Some(ob) = &self.query.order_by {
if let Some(idx) = order_by_column_index(self.query, &columns, &ob.key) {
sort_rows_by_column(&mut rows, idx, ob.dir);
}
}
if let Some(limit) = self.plan.limit {
if rows.len() > limit as usize {
rows.truncate(limit as usize);
}
}
let row_count = rows.len() as u64;
return (
QueryResult {
name: name.to_string(),
oql: String::new(),
columns,
row_count,
rows,
truncated: self.truncated,
error: None,
note: None,
viz: None,
elapsed_ms: None,
},
None,
);
}
if let Some(accs) = self.agg_acc {
if self.plan.limit == Some(0) {
return (
QueryResult {
name: name.to_string(),
oql: String::new(),
columns,
row_count: 0,
rows: vec![],
truncated: false,
error: None,
note: None,
viz: None,
elapsed_ms: None,
},
None,
);
}
let row: Vec<QueryValue> = accs.into_iter().map(finalize_agg_acc).collect();
(
QueryResult {
name: name.to_string(),
oql: String::new(),
columns,
row_count: 1,
rows: vec![row],
truncated: self.truncated,
error: None,
note: None,
viz: None,
elapsed_ms: None,
},
None,
)
} else {
let mut rows = self.rows;
let mut row_src = self.row_src;
let mut note = None;
if self.carry.is_none() {
let defer_limit = self.query.from.as_subquery().is_some();
if let Some(ob) = &self.query.order_by {
match order_by_column_index(self.query, &columns, &ob.key) {
Some(idx) => {
sort_rows_with_src(&mut rows, &mut row_src, idx, ob.dir);
if !defer_limit {
if let Some(limit) = self.plan.limit {
if rows.len() > limit as usize {
rows.truncate(limit as usize);
if let Some(v) = &mut row_src {
v.truncate(limit as usize);
}
}
}
}
}
None => {
note = Some(format!(
"ORDER BY `{}` was not applied: the sort key must be a \
selected column on this query path; rows are in scan order",
attr_name(&ob.key)
));
if !defer_limit {
if let Some(limit) = self.plan.limit {
if rows.len() > limit as usize {
rows.truncate(limit as usize);
if let Some(v) = &mut row_src {
v.truncate(limit as usize);
}
}
}
}
}
}
}
}
(
QueryResult {
name: name.to_string(),
oql: String::new(),
columns,
row_count: rows.len() as u64,
rows,
truncated: self.truncated,
error: None,
note,
viz: None,
elapsed_ms: None,
},
row_src,
)
}
}
fn eval_agg_arg_instance(
&self,
arg: &SelectItem,
src_idx: usize,
class_id: u64,
blob: &[u8],
) -> QueryValue {
match arg {
SelectItem::Star => QueryValue::Int(1), SelectItem::Attr(a) => self.project_attr(a, src_idx, class_id, blob),
SelectItem::Expr(e) => self.eval_expr(e, src_idx, class_id, blob),
_ => QueryValue::Null,
}
}
fn eval_agg_arg_array(
&self,
arg: &SelectItem,
src_idx: usize,
class_name: &str,
length: u32,
) -> QueryValue {
match arg {
SelectItem::Star => QueryValue::Int(1),
SelectItem::Attr(a) => self.project_array_attr(a, src_idx, class_name, length),
SelectItem::Expr(e) => self.eval_expr_array(e, src_idx, class_name, length),
_ => QueryValue::Null,
}
}
}
impl<'a, R: ClassResolver> ObjectVisitor for SingleScanExecutor<'a, R> {
fn visit_instance(&mut self, src_idx: usize, class_id: u64, blob: &[u8]) {
if let FromSource::Object(_) = &self.query.from {
if Some(src_idx) != self.target_index {
return;
}
}
if !self.class_matches(class_id) {
return;
}
if !self.where_passes(src_idx, class_id, blob) {
return;
}
if let Some(carry) = &mut self.carry {
carry.push_index(src_idx as u32);
return;
}
if self.group_map.is_some() {
let key: Vec<QueryValue> = self
.plan
.group_by_exprs
.iter()
.map(|e| self.eval_expr(e, src_idx, class_id, blob))
.collect();
let key_str = format!("{key:?}");
let values: Vec<QueryValue> = self
.query
.select
.iter()
.map(|item| match item {
SelectItem::Aggregate { arg, .. } => {
self.eval_agg_arg_instance(arg, src_idx, class_id, blob)
}
_ => QueryValue::Null,
})
.collect();
let query_select = self.query.select.as_slice();
#[allow(clippy::unnecessary_unwrap)]
let entry = self
.group_map
.as_mut()
.unwrap()
.entry(key_str)
.or_insert_with(|| {
let init_accs: Vec<AggAcc> = query_select.iter().map(init_agg_acc).collect();
(key.clone(), init_accs)
});
for (i, acc) in entry.1.iter_mut().enumerate() {
fold_agg_acc(acc, values[i].clone());
}
self.matched += 1;
return;
}
if self.agg_acc.is_some() {
self.matched += 1;
let n = self.query.select.len();
let mut values = Vec::with_capacity(n);
for item in self.query.select.iter() {
let v = match item {
SelectItem::Aggregate { arg, .. } => {
self.eval_agg_arg_instance(arg, src_idx, class_id, blob)
}
_ => QueryValue::Null,
};
values.push(v);
}
#[allow(clippy::unnecessary_unwrap)]
let accs = self.agg_acc.as_mut().unwrap();
for (i, acc) in accs.iter_mut().enumerate() {
if !matches!(acc, AggAcc::None) {
fold_agg_acc(acc, values[i].clone());
}
}
return;
}
if self.query.order_by.is_none() && self.query.from.as_subquery().is_none() {
if let Some(limit) = self.plan.limit {
if self.matched >= limit {
self.truncated = true;
return;
}
}
}
self.matched += 1;
let row = self.project_row(src_idx, class_id, blob);
self.rows.push(row);
if let Some(v) = &mut self.row_src {
v.push(src_idx as u32);
}
}
fn visit_array(&mut self, src_idx: usize, class_name: &str, length: u32) {
if let FromSource::Object(_) = &self.query.from {
if Some(src_idx) != self.target_index {
return;
}
}
let class_ok = self.query.from.as_subquery().is_some()
|| matches!(self.query.from, FromSource::Object(_))
|| match self.query.from.class_spec() {
Some(spec) => class_name_matches_spec(class_name, spec, self.from_regex.as_ref()),
None => false,
};
if !class_ok {
return;
}
if !self.array_where_passes(src_idx, class_name, length) {
return;
}
if let Some(carry) = &mut self.carry {
carry.push_index(src_idx as u32);
return;
}
if self.group_map.is_some() {
let key: Vec<QueryValue> = self
.plan
.group_by_exprs
.iter()
.map(|e| self.eval_expr_array(e, src_idx, class_name, length))
.collect();
let key_str = format!("{key:?}");
let values: Vec<QueryValue> = self
.query
.select
.iter()
.map(|item| match item {
SelectItem::Aggregate { arg, .. } => {
self.eval_agg_arg_array(arg, src_idx, class_name, length)
}
_ => QueryValue::Null,
})
.collect();
let query_select = self.query.select.as_slice();
#[allow(clippy::unnecessary_unwrap)]
let entry = self
.group_map
.as_mut()
.unwrap()
.entry(key_str)
.or_insert_with(|| {
let init_accs: Vec<AggAcc> = query_select.iter().map(init_agg_acc).collect();
(key.clone(), init_accs)
});
for (i, acc) in entry.1.iter_mut().enumerate() {
fold_agg_acc(acc, values[i].clone());
}
self.matched += 1;
return;
}
if self.agg_acc.is_some() {
self.matched += 1;
let n = self.query.select.len();
let mut values = Vec::with_capacity(n);
for item in self.query.select.iter() {
let v = match item {
SelectItem::Aggregate { arg, .. } => {
self.eval_agg_arg_array(arg, src_idx, class_name, length)
}
_ => QueryValue::Null,
};
values.push(v);
}
#[allow(clippy::unnecessary_unwrap)]
let accs = self.agg_acc.as_mut().unwrap();
for (i, acc) in accs.iter_mut().enumerate() {
if !matches!(acc, AggAcc::None) {
fold_agg_acc(acc, values[i].clone());
}
}
return;
}
if self.query.order_by.is_none() && self.query.from.as_subquery().is_none() {
if let Some(limit) = self.plan.limit {
if self.matched >= limit {
self.truncated = true;
return;
}
}
}
self.matched += 1;
let row = self.project_array_row(src_idx, class_name, length);
self.rows.push(row);
if let Some(v) = &mut self.row_src {
v.push(src_idx as u32);
}
}
}
fn in_subquery_unresolved() -> bool {
unreachable!("IN(<subquery>) predicate must be resolved at plan time, not during the scan")
}
pub fn class_name_matches(name_dotted: &str, pattern: &str) -> bool {
if let Some(prefix) = pattern
.strip_suffix(".*")
.or_else(|| pattern.strip_suffix("/*"))
{
if !sep_eq(name_dotted.get(..prefix.len()).unwrap_or(""), prefix) {
return false;
}
return name_dotted.len() == prefix.len()
|| matches!(
name_dotted.as_bytes().get(prefix.len()),
Some(b'.') | Some(b'/')
);
}
name_dotted.len() == pattern.len() && sep_eq(name_dotted, pattern)
}
pub fn compile_from_regex(
spec: &crate::query::ast::ClassSpec,
) -> Result<Option<regex::Regex>, crate::query::QueryError> {
if !spec.is_regex {
return Ok(None);
}
let anchored = format!("^(?:{})$", spec.class_name);
match regex::Regex::new(&anchored) {
Ok(re) => Ok(Some(re)),
Err(e) => Err(crate::query::QueryError(format!(
"invalid regex in FROM \"{}\": {} \
(the quoted FROM target is matched as a Java-style regex; \
fix the pattern or use a bare class name / `pkg.*` glob instead)",
spec.class_name, e
))),
}
}
pub fn class_name_matches_spec(
name_dotted: &str,
spec: &crate::query::ast::ClassSpec,
from_regex: Option<®ex::Regex>,
) -> bool {
if spec.is_regex {
return match from_regex {
Some(re) => re.is_match(name_dotted),
None => false,
};
}
class_name_matches(name_dotted, &spec.class_name)
}
fn sep_eq(a: &str, b: &str) -> bool {
a.len() == b.len()
&& a.bytes()
.zip(b.bytes())
.all(|(x, y)| x == y || (matches!(x, b'.' | b'/') && matches!(y, b'.' | b'/')))
}
fn read_be(blob: &[u8], off: usize, n: usize) -> Option<u64> {
if off + n > blob.len() {
return None;
}
let mut v = 0u64;
for i in 0..n {
v = (v << 8) | blob[off + i] as u64;
}
Some(v)
}
pub(crate) fn value_to_qv(v: &Value) -> QueryValue {
match v {
Value::Int(n) => QueryValue::Int(*n),
Value::Float(f) => QueryValue::Float(*f),
Value::Str(s) => QueryValue::Str(s.clone()),
Value::Bool(b) => QueryValue::Bool(*b),
Value::Null => QueryValue::Null,
}
}
pub(crate) fn arith(lhs: &QueryValue, op: ArithOp, rhs: &QueryValue) -> QueryValue {
match (lhs, rhs) {
(QueryValue::Int(a), QueryValue::Int(b)) => match op {
ArithOp::Add => QueryValue::Int(a.wrapping_add(*b)),
ArithOp::Sub => QueryValue::Int(a.wrapping_sub(*b)),
ArithOp::Mul => QueryValue::Int(a.wrapping_mul(*b)),
ArithOp::Div => {
if *b == 0 {
QueryValue::Null
} else {
QueryValue::Int(a.wrapping_div(*b))
}
}
},
(QueryValue::Float(a), QueryValue::Float(b)) => match op {
ArithOp::Add => QueryValue::Float(a + b),
ArithOp::Sub => QueryValue::Float(a - b),
ArithOp::Mul => QueryValue::Float(a * b),
ArithOp::Div => QueryValue::Float(a / b), },
(QueryValue::Int(a), QueryValue::Float(b)) => {
let a = *a as f64;
match op {
ArithOp::Add => QueryValue::Float(a + b),
ArithOp::Sub => QueryValue::Float(a - b),
ArithOp::Mul => QueryValue::Float(a * b),
ArithOp::Div => QueryValue::Float(a / b),
}
}
(QueryValue::Float(a), QueryValue::Int(b)) => {
let b = *b as f64;
match op {
ArithOp::Add => QueryValue::Float(a + b),
ArithOp::Sub => QueryValue::Float(a - b),
ArithOp::Mul => QueryValue::Float(a * b),
ArithOp::Div => QueryValue::Float(a / b),
}
}
_ => QueryValue::Null,
}
}
pub(crate) fn unary(op: UnaryOp, v: &QueryValue) -> QueryValue {
match op {
UnaryOp::Pos => v.clone(),
UnaryOp::Neg => match v {
QueryValue::Int(n) => QueryValue::Int(n.wrapping_neg()),
QueryValue::Float(f) => QueryValue::Float(-f),
_ => QueryValue::Null,
},
}
}
pub(crate) fn compare_values(
lv: &QueryValue,
op: CompareOp,
rv: &QueryValue,
like_re: Option<®ex::Regex>,
) -> bool {
if matches!(op, CompareOp::Like | CompareOp::NotLike) {
let is_like = match (lv, rv) {
(QueryValue::Str(s), QueryValue::Str(_)) => {
like_re.map(|re| re.is_match(s)).unwrap_or(false)
}
_ => false,
};
return if matches!(op, CompareOp::Like) {
is_like
} else {
!is_like
};
}
let ord = match (lv, rv) {
(QueryValue::Int(a), QueryValue::Int(b)) => (*a).partial_cmp(b),
(QueryValue::Int(a), QueryValue::Float(b)) => (*a as f64).partial_cmp(b),
(QueryValue::Float(a), QueryValue::Int(b)) => a.partial_cmp(&(*b as f64)),
(QueryValue::Float(a), QueryValue::Float(b)) => a.partial_cmp(b),
(QueryValue::Str(a), QueryValue::Str(b)) => Some(a.as_str().cmp(b.as_str())),
(QueryValue::Bool(a), QueryValue::Bool(b)) => Some(a.cmp(b)),
(QueryValue::Null, QueryValue::Null) => Some(std::cmp::Ordering::Equal),
_ => None,
};
match ord {
None => matches!(op, CompareOp::Ne),
Some(o) => match op {
CompareOp::Eq => o.is_eq(),
CompareOp::Ne => o.is_ne(),
CompareOp::Lt => o.is_lt(),
CompareOp::Le => o.is_le(),
CompareOp::Gt => o.is_gt(),
CompareOp::Ge => o.is_ge(),
CompareOp::Like | CompareOp::NotLike => false,
},
}
}
pub fn compile_like_regexes(
query: &Query,
) -> Result<std::collections::HashMap<String, regex::Regex>, crate::query::QueryError> {
let mut out = std::collections::HashMap::new();
if let Some(pred) = &query.where_ {
collect_like_regexes(pred, &mut out)?;
}
for item in &query.select {
collect_like_in_select_item(item, &mut out)?;
}
for expr in &query.group_by {
collect_like_in_expr(expr, &mut out)?;
}
if let Some(pred) = &query.having {
collect_like_regexes(pred, &mut out)?;
}
Ok(out)
}
fn collect_like_in_select_item(
item: &crate::query::ast::SelectItem,
out: &mut std::collections::HashMap<String, regex::Regex>,
) -> Result<(), crate::query::QueryError> {
use crate::query::ast::SelectItem;
match item {
SelectItem::Expr(e) => collect_like_in_expr(e, out)?,
SelectItem::Aggregate { arg, .. } => collect_like_in_select_item(arg, out)?,
_ => {}
}
Ok(())
}
fn collect_like_in_expr(
e: &crate::query::ast::Expr,
out: &mut std::collections::HashMap<String, regex::Regex>,
) -> Result<(), crate::query::QueryError> {
use crate::query::ast::Expr;
match e {
Expr::Attr(_) | Expr::Lit(_) | Expr::Aggregate { .. } => {}
Expr::Binary { lhs, rhs, .. } => {
collect_like_in_expr(lhs, out)?;
collect_like_in_expr(rhs, out)?;
}
Expr::Unary { arg, .. } => collect_like_in_expr(arg, out)?,
Expr::Method { receiver, args, .. } => {
collect_like_in_expr(receiver, out)?;
for a in args {
collect_like_in_expr(a, out)?;
}
}
Expr::Case { branches, else_ } => {
for (cond, then_e) in branches {
collect_like_regexes(cond, out)?;
collect_like_in_expr(then_e, out)?;
}
if let Some(e) = else_ {
collect_like_in_expr(e, out)?;
}
}
Expr::Coalesce(args) => {
for arg in args {
collect_like_in_expr(arg, out)?;
}
}
Expr::NullIf { lhs, rhs } => {
collect_like_in_expr(lhs, out)?;
collect_like_in_expr(rhs, out)?;
}
}
Ok(())
}
fn collect_like_regexes(
pred: &crate::query::ast::Predicate,
out: &mut std::collections::HashMap<String, regex::Regex>,
) -> Result<(), crate::query::QueryError> {
use crate::query::ast::Predicate as P;
match pred {
P::And(a, b) | P::Or(a, b) => {
collect_like_regexes(a, out)?;
collect_like_regexes(b, out)?;
}
P::Not(a) => collect_like_regexes(a, out)?,
P::Compare {
op: CompareOp::Like | CompareOp::NotLike,
rhs,
..
} => {
if let Some(Value::Str(pat)) = rhs.as_lit() {
if !out.contains_key(pat) {
if pat.contains('%') && !pat.contains(".*") && !pat.starts_with('^') {
let suggested = pat.replace('%', ".*").replace('_', ".");
return Err(crate::query::QueryError(format!(
"LIKE \"{pat}\": looks like a SQL glob (`%` wildcard), but LIKE \
uses Java-style regex (full-match). \
Did you mean LIKE \"{suggested}\"?"
)));
}
let anchored = format!("^(?:{pat})$");
let re = regex::Regex::new(&anchored).map_err(|e| {
crate::query::QueryError(format!(
"invalid regex in LIKE \"{pat}\": {e} \
(the LIKE right-hand side is matched as a Java-style regex \
with whole-string semantics; fix the pattern)"
))
})?;
out.insert(pat.clone(), re);
}
}
}
P::Compare { .. } | P::InstanceOf(_) | P::InSubquery { .. } | P::Exists { .. } => {}
}
Ok(())
}
pub(crate) fn order_by_column_index(
q: &Query,
columns: &[QueryColumn],
key: &Attr,
) -> Option<usize> {
let key_name = attr_name(key);
let bare = strip_leading_alias(&key_name, q.alias.as_deref());
columns.iter().position(|c| {
c.name == key_name || strip_leading_alias(&c.name, q.alias.as_deref()) == bare
})
}
fn strip_leading_alias<'n>(name: &'n str, alias: Option<&str>) -> &'n str {
if let Some(a) = alias {
if let Some(rest) = name.strip_prefix(a).and_then(|r| r.strip_prefix('.')) {
return rest;
}
}
name
}
pub(crate) fn sort_rows_by_column(
rows: &mut [Vec<QueryValue>],
idx: usize,
dir: crate::query::ast::SortDir,
) {
use crate::query::ast::SortDir;
rows.sort_by(|a, b| {
let av = a.get(idx).unwrap_or(&QueryValue::Null);
let bv = b.get(idx).unwrap_or(&QueryValue::Null);
let ord = total_cmp_query_value(av, bv);
match dir {
SortDir::Asc => ord,
SortDir::Desc => ord.reverse(),
}
});
}
pub(crate) fn sort_rows_with_src(
rows: &mut Vec<Vec<QueryValue>>,
src: &mut Option<Vec<u32>>,
idx: usize,
dir: crate::query::ast::SortDir,
) {
use crate::query::ast::SortDir;
let Some(src_vec) = src.as_mut() else {
sort_rows_by_column(rows, idx, dir);
return;
};
let mut perm: Vec<usize> = (0..rows.len()).collect();
perm.sort_by(|&a, &b| {
let av = rows[a].get(idx).unwrap_or(&QueryValue::Null);
let bv = rows[b].get(idx).unwrap_or(&QueryValue::Null);
let ord = total_cmp_query_value(av, bv);
match dir {
SortDir::Asc => ord,
SortDir::Desc => ord.reverse(),
}
});
let new_rows: Vec<Vec<QueryValue>> =
perm.iter().map(|&i| std::mem::take(&mut rows[i])).collect();
let new_src: Vec<u32> = perm.iter().map(|&i| src_vec[i]).collect();
*rows = new_rows;
*src_vec = new_src;
}
fn total_cmp_query_value(a: &QueryValue, b: &QueryValue) -> std::cmp::Ordering {
use std::cmp::Ordering;
fn num(v: &QueryValue) -> Option<f64> {
match v {
QueryValue::Int(i) => Some(*i as f64),
QueryValue::Float(f) => Some(*f),
_ => None,
}
}
if let (Some(x), Some(y)) = (num(a), num(b)) {
return x.partial_cmp(&y).unwrap_or_else(|| {
match (x.is_nan(), y.is_nan()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
_ => Ordering::Equal,
}
});
}
match (a, b) {
(QueryValue::Str(x), QueryValue::Str(y)) => x.cmp(y),
(QueryValue::Bool(x), QueryValue::Bool(y)) => x.cmp(y),
(QueryValue::Null, QueryValue::Null) => Ordering::Equal,
_ => kind_rank(a).cmp(&kind_rank(b)),
}
}
fn kind_rank(v: &QueryValue) -> u8 {
match v {
QueryValue::Null => 0,
QueryValue::Bool(_) => 1,
QueryValue::Int(_) | QueryValue::Float(_) => 2,
QueryValue::Str(_) => 3,
QueryValue::ObjRef { .. } => 4,
}
}
pub fn column_name(it: &SelectItem) -> String {
match it {
SelectItem::Star => "*".to_string(),
SelectItem::Attr(a) => attr_name(a),
SelectItem::Aggregate { func, arg } => {
let f = format!("{func:?}").to_uppercase();
format!("{f}({})", column_name(arg))
}
SelectItem::Path { from, to } => format!(
"path({}, {})",
path_operand_name(from),
path_operand_name(to)
),
SelectItem::ToString(a) => format!("toString({a})"),
SelectItem::Expr(e) => expr_name(e),
}
}
pub(crate) fn query_columns(q: &Query) -> Vec<QueryColumn> {
debug_assert_eq!(
q.select.len(),
q.select_aliases.len(),
"select and select_aliases must stay parallel"
);
q.select
.iter()
.zip(
q.select_aliases
.iter()
.map(Option::as_deref)
.chain(std::iter::repeat(None)),
)
.map(|(it, alias)| QueryColumn {
name: alias
.map(|s| s.to_string())
.unwrap_or_else(|| column_name(it)),
})
.collect()
}
fn qv_value_eq(a: &QueryValue, b: &QueryValue) -> bool {
use QueryValue::*;
match (a, b) {
(Null, Null) => true,
(Bool(x), Bool(y)) => x == y,
(Int(x), Int(y)) => x == y,
(Float(x), Float(y)) => x == y,
(Str(x), Str(y)) => x == y,
(Int(x), Float(y)) | (Float(y), Int(x)) => (*x as f64) == *y,
(ObjRef { index: i, .. }, ObjRef { index: j, .. }) => i == j,
_ => false,
}
}
fn is_boxed_integral(c: &str) -> bool {
matches!(
c,
"java.lang.Integer" | "java.lang.Long" | "java.lang.Short" | "java.lang.Byte"
)
}
fn is_boxed_fp(c: &str) -> bool {
matches!(c, "java.lang.Float" | "java.lang.Double")
}
fn is_sized_collection(c: &str) -> bool {
matches!(
c,
"java.util.ArrayList"
| "java.util.Vector"
| "java.util.LinkedList"
| "java.util.HashMap"
| "java.util.LinkedHashMap"
| "java.util.WeakHashMap"
| "java.util.TreeMap"
| "java.util.IdentityHashMap"
)
}
fn path_operand_name(p: &crate::query::ast::PathOperand) -> String {
use crate::query::ast::PathOperand;
match p {
PathOperand::Alias(s) | PathOperand::Class(s) => s.clone(),
}
}
pub fn expr_name(e: &Expr) -> String {
match e {
Expr::Attr(a) => attr_name(a),
Expr::Lit(v) => match v {
Value::Int(n) => n.to_string(),
Value::Float(f) => f.to_string(),
Value::Str(s) => format!("\"{s}\""),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
},
Expr::Binary { op, lhs, rhs } => {
let op_str = match op {
ArithOp::Add => " + ",
ArithOp::Sub => " - ",
ArithOp::Mul => " * ",
ArithOp::Div => " / ",
};
let l = if matches!(lhs.as_ref(), Expr::Binary { .. }) {
format!("({})", expr_name(lhs))
} else {
expr_name(lhs)
};
let r = if matches!(rhs.as_ref(), Expr::Binary { .. }) {
format!("({})", expr_name(rhs))
} else {
expr_name(rhs)
};
format!("{l}{op_str}{r}")
}
Expr::Unary { op, arg } => match op {
UnaryOp::Neg => format!("-{}", expr_name(arg)),
UnaryOp::Pos => expr_name(arg),
},
Expr::Method { name, args, .. } => format!(
"{name}({})",
args.iter().map(expr_name).collect::<Vec<_>>().join(", ")
), Expr::Aggregate { func, arg } => {
let func_name = match func {
AggFunc::Count => "COUNT",
AggFunc::Sum => "SUM",
AggFunc::Min => "MIN",
AggFunc::Max => "MAX",
AggFunc::Avg => "AVG",
AggFunc::Percentile(p) => return format!("PERCENTILE(_, {p})"),
AggFunc::Median => "MEDIAN",
};
let arg_str = match arg.as_ref() {
SelectItem::Star => "*".to_string(),
SelectItem::Attr(a) => attr_name(a),
_ => unreachable!("Expr::Aggregate arg is always Star or Attr"),
};
format!("{func_name}({arg_str})")
}
Expr::Case { .. } => "CASE".to_string(),
Expr::Coalesce(_) => "COALESCE".to_string(),
Expr::NullIf { .. } => "NULLIF".to_string(),
}
}
pub(crate) fn attr_name(a: &Attr) -> String {
match a {
Attr::ObjectId => "@objectId".into(),
Attr::ObjectAddress => "@objectAddress".into(),
Attr::UsedHeapSize => "@usedHeapSize".into(),
Attr::RetainedHeapSize => "@retainedHeapSize".into(),
Attr::DisplayName => "@displayName".into(),
Attr::Length => "@length".into(),
Attr::Inbounds => "@inbounds".into(),
Attr::Outbounds => "@outbounds".into(),
Attr::ClassOf => "classof".into(),
Attr::Dominators(a) => format!("dominators({a})"),
Attr::DominatorOf(a) => format!("dominatorof({a})"),
Attr::ToString(a) => format!("toString({a})"),
Attr::ToHex(inner) => format!("toHex({})", expr_name(inner)),
Attr::Field(f) => f.clone(),
Attr::RefPath { hops, tail, .. } => {
let mut s = hops.join(".");
s.push('.');
s.push_str(&attr_name(tail));
s
}
Attr::ValueArray => "@valueArray".into(),
Attr::ReferenceArray => "@referenceArray".into(),
Attr::GcRoots => "@GCRoots".into(),
Attr::GcRootInfo => "@GCRootInfo".into(),
Attr::ArrayIndex { base, .. } => format!("{}[...]", attr_name(base)),
Attr::ArraySlice { base, .. } => format!("{}[...]", attr_name(base)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::model::QueryValue;
use crate::query::parse::parse;
fn schema(pairs: &[(u64, &str)]) -> TestSchema {
TestSchema {
names: pairs.iter().map(|(id, n)| (*id, n.to_string())).collect(),
}
}
fn pq(q: &crate::query::ast::Query) -> crate::query::plan::QueryPlan {
crate::query::plan::plan_query(q, crate::query::DEFAULT_PATH_DEPTH_CAP).unwrap()
}
#[test]
fn exec_state_separates_finished_and_pending() {
use crate::query::plan::Phase;
let mut st = QueryExecState::new();
st.push_finished(
0,
QueryResult {
name: "q1".into(),
oql: String::new(),
columns: vec![],
rows: vec![],
row_count: 0,
truncated: false,
error: None,
note: None,
viz: None,
elapsed_ms: None,
},
);
let q = crate::query::parse::parse("SELECT @retainedHeapSize FROM C").unwrap();
let plan = pq(&q);
assert_eq!(plan.finalize_at, Phase::P3);
let mut carry = crate::query::carry::Carry::index_only(100);
carry.push_index(42);
st.push_cross_phase(1, "q2".to_string(), plan.clone(), carry);
assert_eq!(st.finished_len(), 1);
assert_eq!(st.pending_len(), 1);
assert_eq!(st.pending()[0].slot, 1);
assert_eq!(st.pending()[0].carry.indices(), vec![42]);
}
#[test]
fn carry_mode_carries_matched_indices_not_rows() {
let q = parse("SELECT @objectId, @retainedHeapSize FROM com.acme.Foo").unwrap();
let plan = pq(&q);
assert!(plan.finalize_at == crate::query::plan::Phase::P3);
let sc = schema(&[(10, "com.acme.Foo"), (20, "com.acme.Bar")]);
let carry = crate::query::carry::Carry::index_only(100);
let mut ex = SingleScanExecutor::new_carry(&q, &plan, &sc, carry);
assert!(ex.is_carry());
ex.visit_instance(3, 10, &[]); ex.visit_instance(4, 20, &[]); ex.visit_instance(7, 10, &[]); let carry = ex.take_carry();
assert_eq!(carry.indices(), vec![3, 7]);
}
#[test]
fn carry_mode_skips_retained_where_terms() {
let q = parse("SELECT @objectId FROM com.acme.Foo WHERE @retainedHeapSize > 1000").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let carry = crate::query::carry::Carry::index_only(100);
let mut ex = SingleScanExecutor::new_carry(&q, &plan, &sc, carry);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]);
let carry = ex.take_carry();
assert_eq!(
carry.indices(),
vec![1, 2],
"retained WHERE must not filter during scan"
);
}
#[test]
fn carry_mode_ignores_limit_during_scan() {
let q = parse("SELECT @objectId FROM com.acme.Foo ORDER BY @retainedHeapSize DESC LIMIT 1")
.unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let carry = crate::query::carry::Carry::index_only(100);
let mut ex = SingleScanExecutor::new_carry(&q, &plan, &sc, carry);
for i in 1..=5u32 {
ex.visit_instance(i as usize, 10, &[]);
}
let carry = ex.take_carry();
assert_eq!(
carry.indices(),
vec![1, 2, 3, 4, 5],
"LIMIT must be deferred to the late phase"
);
}
#[test]
fn carry_mode_still_applies_non_retained_where() {
let q = parse("SELECT @objectId FROM com.acme.Foo WHERE @retainedHeapSize > 0").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo"), (20, "com.acme.Bar")]);
let carry = crate::query::carry::Carry::index_only(100);
let mut ex = SingleScanExecutor::new_carry(&q, &plan, &sc, carry);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 20, &[]); let carry = ex.take_carry();
assert_eq!(carry.indices(), vec![1]);
}
#[test]
fn matches_exact_class_and_projects_object_id() {
let q = parse("SELECT @objectId FROM com.acme.Foo").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo"), (20, "com.acme.Bar")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(3, 10, &[]);
ex.visit_instance(4, 20, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], QueryValue::Int(3));
}
#[test]
fn respects_limit() {
let q = parse("SELECT @objectId FROM com.acme.Foo LIMIT 1").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(3, 10, &[]);
ex.visit_instance(4, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert!(res.truncated);
}
fn regex_spec(src: &str) -> crate::query::ast::ClassSpec {
crate::query::ast::ClassSpec {
instanceof: false,
class_name: src.into(),
is_regex: true,
}
}
fn glob_spec(src: &str) -> crate::query::ast::ClassSpec {
crate::query::ast::ClassSpec {
instanceof: false,
class_name: src.into(),
is_regex: false,
}
}
#[test]
fn matches_spec_regex_full_anchored_match() {
let spec = regex_spec(r"java\.lang\..*");
let re = compile_from_regex(&spec).unwrap();
assert!(class_name_matches_spec(
"java.lang.String",
&spec,
re.as_ref()
));
let lang = regex_spec("lang");
let lang_re = compile_from_regex(&lang).unwrap();
assert!(
!class_name_matches_spec("java.lang.String", &lang, lang_re.as_ref()),
"regex must match the WHOLE class name (Pattern.matches), not a substring"
);
}
#[test]
fn matches_spec_regex_trailing_string() {
let spec = regex_spec(".*String");
let re = compile_from_regex(&spec).unwrap();
assert!(class_name_matches_spec(
"java.lang.String",
&spec,
re.as_ref()
));
assert!(!class_name_matches_spec(
"java.lang.Integer",
&spec,
re.as_ref()
));
}
#[test]
fn matches_spec_regex_alternation() {
let spec = regex_spec("java\\.lang\\.String|java\\.util\\.HashMap");
let re = compile_from_regex(&spec).unwrap();
assert!(class_name_matches_spec(
"java.lang.String",
&spec,
re.as_ref()
));
assert!(class_name_matches_spec(
"java.util.HashMap",
&spec,
re.as_ref()
));
assert!(!class_name_matches_spec(
"java.lang.Integer",
&spec,
re.as_ref()
));
}
#[test]
fn matches_spec_regex_dot_is_regex_metachar() {
let spec = regex_spec("java.lang.String");
let re = compile_from_regex(&spec).unwrap();
assert!(class_name_matches_spec(
"java.lang.String",
&spec,
re.as_ref()
));
assert!(class_name_matches_spec(
"javaXlangXString",
&spec,
re.as_ref()
));
}
#[test]
fn matches_spec_glob_falls_through_to_class_name_matches() {
let spec = glob_spec("com.acme.*");
assert!(compile_from_regex(&spec).unwrap().is_none());
assert!(class_name_matches_spec("com.acme.Foo", &spec, None));
assert!(!class_name_matches_spec("org.other.Foo", &spec, None));
}
#[test]
fn compile_from_regex_bad_pattern_is_actionable_error() {
let spec = regex_spec("[");
let err = compile_from_regex(&spec).expect_err("unclosed class must be an error");
assert!(
err.0.contains("invalid regex") && err.0.contains('['),
"error must name the regex problem; got: {}",
err.0
);
}
#[test]
fn matches_spec_regex_matches_nothing_returns_false() {
let spec = regex_spec("no\\.such\\.Class");
let re = compile_from_regex(&spec).unwrap();
assert!(!class_name_matches_spec(
"java.lang.String",
&spec,
re.as_ref()
));
}
#[test]
fn matches_spec_regex_none_when_uncompiled_is_false() {
let spec = regex_spec("java\\.lang\\..*");
assert!(!class_name_matches_spec("java.lang.String", &spec, None));
}
#[test]
fn class_name_matches_exact() {
assert!(class_name_matches("com.acme.Foo", "com.acme.Foo"));
assert!(!class_name_matches("com.acme.Foo", "com.acme.Bar"));
assert!(!class_name_matches("com.acme.Foo", "com.acme"));
}
#[test]
fn class_name_matches_glob() {
assert!(class_name_matches("com.acme.Foo", "com.acme.*"));
assert!(class_name_matches("com.acme.sub.Bar", "com.acme.*"));
assert!(class_name_matches("com.acme", "com.acme.*"));
assert!(!class_name_matches("com.acmeX.Foo", "com.acme.*"));
assert!(!class_name_matches("org.other.Foo", "com.acme.*"));
}
#[test]
fn class_name_matches_separator_normalization() {
assert!(class_name_matches("com/acme/Foo", "com.acme.Foo"));
assert!(class_name_matches("com.acme.Foo", "com/acme/Foo"));
assert!(class_name_matches("com/acme/Foo", "com.acme.*"));
assert!(class_name_matches("com/acme/Foo", "com/acme/*"));
}
#[test]
fn class_name_matches_arrays_and_edges() {
assert!(class_name_matches("char[]", "char[]"));
assert!(class_name_matches(
"java.lang.Object[]",
"java.lang.Object[]"
));
assert!(!class_name_matches("char[]", "byte[]"));
assert!(!class_name_matches("com", "com.acme.*"));
assert!(!class_name_matches("com.acme.Foo", "com.acme.Foobar"));
}
#[test]
fn column_name_for_star() {
assert_eq!(column_name(&SelectItem::Star), "*");
}
#[test]
fn column_name_for_at_attr() {
assert_eq!(column_name(&SelectItem::Attr(Attr::ObjectId)), "@objectId");
assert_eq!(
column_name(&SelectItem::Attr(Attr::UsedHeapSize)),
"@usedHeapSize"
);
}
#[test]
fn column_name_for_field() {
assert_eq!(
column_name(&SelectItem::Attr(Attr::Field("count".into()))),
"count"
);
}
#[test]
fn column_name_for_aggregates() {
let count_star = SelectItem::Aggregate {
func: crate::query::ast::AggFunc::Count,
arg: Box::new(SelectItem::Star),
};
assert_eq!(column_name(&count_star), "COUNT(*)");
let sum_heap = SelectItem::Aggregate {
func: crate::query::ast::AggFunc::Sum,
arg: Box::new(SelectItem::Attr(Attr::UsedHeapSize)),
};
assert_eq!(column_name(&sum_heap), "SUM(@usedHeapSize)");
}
#[test]
fn projects_star_as_objref() {
let q = parse("SELECT * FROM com.acme.Foo").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(7, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(
res.rows[0][0],
QueryValue::ObjRef {
index: 7,
class: "com.acme.Foo".into(),
addr: None,
}
);
}
#[test]
fn no_match_yields_empty_untruncated() {
let q = parse("SELECT @objectId FROM com.acme.Missing").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo"), (20, "com.acme.Bar")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(3, 10, &[]);
ex.visit_instance(4, 20, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 0);
assert!(!res.truncated);
}
#[test]
fn glob_from_matches_multiple_classes() {
let q = parse("SELECT @objectId FROM com.acme.*").unwrap();
let plan = pq(&q);
let sc = schema(&[
(10, "com.acme.Foo"),
(20, "com.acme.Bar"),
(30, "org.other.Baz"),
]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 20, &[]);
ex.visit_instance(3, 30, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 2);
assert_eq!(res.rows[0][0], QueryValue::Int(1));
assert_eq!(res.rows[1][0], QueryValue::Int(2));
assert!(!res.truncated);
}
#[test]
fn unknown_class_id_never_matches() {
let q = parse("SELECT @objectId FROM com.acme.Foo").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(3, 999, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 0);
}
struct FieldSchema {
names: std::collections::HashMap<u64, String>,
fields: std::collections::HashMap<String, (u32, crate::types::HprofType)>,
}
impl FieldSchema {
fn count_only() -> Self {
FieldSchema {
names: std::iter::once((10u64, "C".to_string())).collect(),
fields: std::iter::once((
"count".to_string(),
(0u32, crate::types::HprofType::Int),
))
.collect(),
}
}
fn with_fields(pairs: &[(&str, u32, crate::types::HprofType)]) -> Self {
FieldSchema {
names: std::iter::once((10u64, "C".to_string())).collect(),
fields: pairs
.iter()
.map(|(n, o, t)| (n.to_string(), (*o, *t)))
.collect(),
}
}
}
impl ClassResolver for FieldSchema {
fn class_name(&self, class_id: u64) -> Option<&str> {
self.names.get(&class_id).map(|s| s.as_str())
}
fn field(&self, _class_id: u64, name: &str) -> Option<(u32, crate::types::HprofType)> {
self.fields.get(name).copied()
}
}
#[test]
fn where_filters_on_scalar_field() {
let q = crate::query::parse::parse("SELECT @objectId FROM C WHERE count > 5").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 3]); ex.visit_instance(2, 10, &[0, 0, 0, 9]); let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], crate::query::model::QueryValue::Int(2));
}
#[test]
fn projects_scalar_field_value() {
let q = crate::query::parse::parse("SELECT n FROM C").unwrap();
let plan = pq(&q);
let sc = FieldSchema::with_fields(&[("n", 0, crate::types::HprofType::Int)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 7]);
let res = ex.finish("q1");
assert_eq!(res.rows[0][0], crate::query::model::QueryValue::Int(7));
}
#[test]
fn read_be_assembles_big_endian_widths() {
assert_eq!(read_be(&[0x12, 0x34], 0, 2), Some(0x1234));
assert_eq!(read_be(&[0xde, 0xad, 0xbe, 0xef], 0, 4), Some(0xdead_beef));
assert_eq!(
read_be(&[1, 2, 3, 4, 5, 6, 7, 8], 0, 8),
Some(0x0102_0304_0506_0708)
);
assert_eq!(read_be(&[0xff, 0x12, 0x34], 1, 2), Some(0x1234));
}
#[test]
fn read_be_out_of_range_is_none() {
assert_eq!(read_be(&[0x12], 0, 2), None); assert_eq!(read_be(&[], 0, 4), None);
assert_eq!(read_be(&[1, 2, 3, 4], 2, 4), None); }
fn decode(field: &str, off: u32, ty: crate::types::HprofType, blob: &[u8]) -> QueryValue {
let q = crate::query::parse::parse(&format!("SELECT {field} FROM C")).unwrap();
let plan = pq(&q);
let sc = FieldSchema::with_fields(&[(field, off, ty)]);
let ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.decode_field(10, field, blob)
}
#[test]
fn decode_byte() {
assert_eq!(
decode("b", 0, crate::types::HprofType::Byte, &[0x2a]),
QueryValue::Int(42)
);
}
#[test]
fn decode_short_sign_extends_negative() {
assert_eq!(
decode("s", 0, crate::types::HprofType::Short, &[0xff, 0xfe]),
QueryValue::Int(-2)
);
}
#[test]
fn decode_char_is_unsigned() {
assert_eq!(
decode("c", 0, crate::types::HprofType::Char, &[0xff, 0xff]),
QueryValue::Int(65535)
);
}
#[test]
fn decode_int_sign_extends_negative() {
assert_eq!(
decode(
"i",
0,
crate::types::HprofType::Int,
&[0xff, 0xff, 0xff, 0xff]
),
QueryValue::Int(-1)
);
}
#[test]
fn decode_long() {
assert_eq!(
decode(
"l",
0,
crate::types::HprofType::Long,
&[0, 0, 0, 0, 0, 0, 0x04, 0xd2]
),
QueryValue::Int(1234)
);
}
#[test]
fn decode_float() {
assert_eq!(
decode(
"f",
0,
crate::types::HprofType::Float,
&[0x3f, 0xc0, 0x00, 0x00]
),
QueryValue::Float(1.5)
);
}
#[test]
fn decode_double() {
assert_eq!(
decode(
"d",
0,
crate::types::HprofType::Double,
&[0x3f, 0xf8, 0, 0, 0, 0, 0, 0]
),
QueryValue::Float(1.5)
);
}
#[test]
fn decode_boolean_false_and_true() {
assert_eq!(
decode("bo", 0, crate::types::HprofType::Boolean, &[0]),
QueryValue::Bool(false)
);
assert_eq!(
decode("bo", 0, crate::types::HprofType::Boolean, &[1]),
QueryValue::Bool(true)
);
assert_eq!(
decode("bo", 0, crate::types::HprofType::Boolean, &[0x7f]),
QueryValue::Bool(true)
);
}
#[test]
fn decode_object_field_is_null() {
assert_eq!(
decode(
"o",
0,
crate::types::HprofType::Object,
&[0, 0, 0, 0, 0, 0, 0, 1]
),
QueryValue::Null
);
}
#[test]
fn decode_unknown_field_is_null() {
let q = crate::query::parse::parse("SELECT missing FROM C").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let ex = SingleScanExecutor::new(&q, &plan, &sc);
assert_eq!(
ex.decode_field(10, "missing", &[0, 0, 0, 1]),
QueryValue::Null
);
}
#[test]
fn decode_blob_too_short_is_null_no_panic() {
assert_eq!(
decode("i", 4, crate::types::HprofType::Int, &[0, 1]),
QueryValue::Null
);
}
#[test]
fn decode_honors_nonzero_offset() {
assert_eq!(
decode(
"i",
2,
crate::types::HprofType::Int,
&[0xaa, 0xbb, 0, 0, 0, 0x05, 0xcc, 0xdd]
),
QueryValue::Int(5)
);
}
#[test]
fn compare_type_mismatch_eq_false_ne_true() {
let lv = QueryValue::Int(1);
let rhs = QueryValue::Str("x".into());
assert!(!compare_values(
&lv,
crate::query::ast::CompareOp::Eq,
&rhs,
None
));
assert!(compare_values(
&lv,
crate::query::ast::CompareOp::Ne,
&rhs,
None
));
assert!(!compare_values(
&lv,
crate::query::ast::CompareOp::Lt,
&rhs,
None
));
assert!(!compare_values(
&lv,
crate::query::ast::CompareOp::Gt,
&rhs,
None
));
}
#[test]
fn compare_null_equals_null() {
use crate::query::ast::CompareOp;
assert!(compare_values(
&QueryValue::Null,
CompareOp::Eq,
&QueryValue::Null,
None
));
assert!(!compare_values(
&QueryValue::Null,
CompareOp::Ne,
&QueryValue::Null,
None
));
}
#[test]
fn compare_int_vs_float_cross() {
use crate::query::ast::CompareOp;
assert!(compare_values(
&QueryValue::Int(2),
CompareOp::Lt,
&QueryValue::Float(2.5),
None
));
assert!(compare_values(
&QueryValue::Float(2.5),
CompareOp::Gt,
&QueryValue::Int(2),
None
));
assert!(compare_values(
&QueryValue::Int(3),
CompareOp::Eq,
&QueryValue::Float(3.0),
None
));
}
#[test]
fn compare_string_ordering() {
use crate::query::ast::CompareOp;
assert!(compare_values(
&QueryValue::Str("abc".into()),
CompareOp::Lt,
&QueryValue::Str("abd".into()),
None
));
assert!(compare_values(
&QueryValue::Str("abc".into()),
CompareOp::Eq,
&QueryValue::Str("abc".into()),
None
));
}
#[test]
fn compare_bool_ordering() {
use crate::query::ast::CompareOp;
assert!(compare_values(
&QueryValue::Bool(false),
CompareOp::Lt,
&QueryValue::Bool(true),
None
));
assert!(compare_values(
&QueryValue::Bool(true),
CompareOp::Eq,
&QueryValue::Bool(true),
None
));
}
fn like_re(pat: &str) -> regex::Regex {
regex::Regex::new(&format!("^(?:{pat})$")).unwrap()
}
#[test]
fn like_full_match_true() {
use crate::query::ast::CompareOp;
let re = like_re("m.*");
assert!(compare_values(
&QueryValue::Str("main".into()),
CompareOp::Like,
&QueryValue::Str("m.*".into()),
Some(&re)
));
}
#[test]
fn like_non_match_false() {
use crate::query::ast::CompareOp;
let re = like_re("m.*");
assert!(!compare_values(
&QueryValue::Str("worker".into()),
CompareOp::Like,
&QueryValue::Str("m.*".into()),
Some(&re)
));
}
#[test]
fn like_is_anchored_full_match_not_substring() {
use crate::query::ast::CompareOp;
let re = like_re("m.*");
assert!(!compare_values(
&QueryValue::Str("submaine".into()),
CompareOp::Like,
&QueryValue::Str("m.*".into()),
Some(&re)
));
}
#[test]
fn not_like_inverts_like() {
use crate::query::ast::CompareOp;
let re = like_re("m.*");
assert!(!compare_values(
&QueryValue::Str("main".into()),
CompareOp::NotLike,
&QueryValue::Str("m.*".into()),
Some(&re)
));
assert!(compare_values(
&QueryValue::Str("worker".into()),
CompareOp::NotLike,
&QueryValue::Str("m.*".into()),
Some(&re)
));
}
#[test]
fn like_alternation_anchored() {
use crate::query::ast::CompareOp;
let re = like_re("foo|bar");
assert!(compare_values(
&QueryValue::Str("foo".into()),
CompareOp::Like,
&QueryValue::Str("foo|bar".into()),
Some(&re)
));
assert!(compare_values(
&QueryValue::Str("bar".into()),
CompareOp::Like,
&QueryValue::Str("foo|bar".into()),
Some(&re)
));
assert!(!compare_values(
&QueryValue::Str("xfooy".into()),
CompareOp::Like,
&QueryValue::Str("foo|bar".into()),
Some(&re)
));
}
#[test]
fn like_on_numeric_lhs_false_not_like_true() {
use crate::query::ast::CompareOp;
let re = like_re("m.*");
assert!(!compare_values(
&QueryValue::Int(42),
CompareOp::Like,
&QueryValue::Str("m.*".into()),
Some(&re)
));
assert!(compare_values(
&QueryValue::Int(42),
CompareOp::NotLike,
&QueryValue::Str("m.*".into()),
Some(&re)
));
}
#[test]
fn like_missing_compiled_regex_is_no_match() {
use crate::query::ast::CompareOp;
assert!(!compare_values(
&QueryValue::Str("main".into()),
CompareOp::Like,
&QueryValue::Str("m.*".into()),
None
));
}
#[test]
fn compile_like_regexes_bad_pattern_is_actionable_error() {
let q = crate::query::parse::parse(r#"SELECT * FROM C WHERE name LIKE "[""#).unwrap();
let err = compile_like_regexes(&q).expect_err("unclosed class must be an error");
let msg = err.to_string();
assert!(msg.contains("invalid regex in LIKE"), "got: {msg}");
assert!(msg.contains("Java-style regex"), "got: {msg}");
}
#[test]
fn compile_like_regexes_collects_each_pattern_once() {
let q = crate::query::parse::parse(
r#"SELECT * FROM C WHERE name LIKE "a.*" AND other NOT LIKE "a.*""#,
)
.unwrap();
let map = compile_like_regexes(&q).unwrap();
assert_eq!(map.len(), 1);
assert!(map.contains_key("a.*"));
}
#[test]
fn where_and_filters_both_bounds() {
let q =
crate::query::parse::parse("SELECT @objectId FROM C WHERE count > 5 AND count < 100")
.unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 3]); ex.visit_instance(2, 10, &[0, 0, 0, 50]); ex.visit_instance(3, 10, &[0, 0, 0, 200]); let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], QueryValue::Int(2));
}
#[test]
fn where_or_matches_either() {
let q =
crate::query::parse::parse("SELECT @objectId FROM C WHERE count < 5 OR count > 100")
.unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 3]); ex.visit_instance(2, 10, &[0, 0, 0, 50]); ex.visit_instance(3, 10, &[0, 0, 0, 200]); let res = ex.finish("q1");
assert_eq!(res.row_count, 2);
assert_eq!(res.rows[0][0], QueryValue::Int(1));
assert_eq!(res.rows[1][0], QueryValue::Int(3));
}
#[test]
fn where_not_negates() {
let q = crate::query::parse::parse("SELECT @objectId FROM C WHERE NOT count = 7").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 7]); ex.visit_instance(2, 10, &[0, 0, 0, 8]); let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], QueryValue::Int(2));
}
#[test]
fn where_instanceof_matches_by_class_name() {
let q = crate::query::parse::parse("SELECT @objectId FROM C WHERE x INSTANCEOF C").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 1]); let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], QueryValue::Int(1));
}
#[test]
fn where_instanceof_excludes_other_class() {
let q = crate::query::parse::parse("SELECT @objectId FROM C WHERE x INSTANCEOF D").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 1]); let res = ex.finish("q1");
assert_eq!(res.row_count, 0);
}
#[test]
fn where_unknown_field_excludes_for_eq() {
let q = crate::query::parse::parse("SELECT @objectId FROM C WHERE missing = 1").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 1]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 0);
}
#[test]
fn alias_prefixed_field_projects_same_as_bare() {
let q = crate::query::parse::parse("SELECT c.n FROM C c").unwrap();
assert_eq!(q.alias.as_deref(), Some("c"), "parser must bind alias `c`");
let plan = pq(&q);
let sc = FieldSchema::with_fields(&[("n", 0, crate::types::HprofType::Int)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 7]);
let res = ex.finish("q1");
assert_eq!(res.rows[0][0], QueryValue::Int(7));
}
#[test]
fn alias_prefixed_field_filters_in_where() {
let q = crate::query::parse::parse("SELECT @objectId FROM C c WHERE c.count > 5").unwrap();
assert_eq!(q.alias.as_deref(), Some("c"));
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[0, 0, 0, 3]); ex.visit_instance(2, 10, &[0, 0, 0, 9]); let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], QueryValue::Int(2));
}
#[test]
fn strip_alias_only_strips_matching_prefix() {
let q = crate::query::parse::parse("SELECT n FROM C c").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let ex = SingleScanExecutor::new(&q, &plan, &sc);
assert_eq!(ex.strip_alias("c.count"), "count");
assert_eq!(ex.strip_alias("d.count"), "d.count");
assert_eq!(ex.strip_alias("count"), "count");
assert_eq!(ex.strip_alias("c.a.b"), "a.b");
}
#[test]
fn strip_alias_noop_without_alias() {
let q = crate::query::parse::parse("SELECT n FROM C").unwrap();
assert!(q.alias.is_none());
let plan = pq(&q);
let sc = FieldSchema::count_only();
let ex = SingleScanExecutor::new(&q, &plan, &sc);
assert_eq!(ex.strip_alias("c.count"), "c.count");
}
#[test]
fn array_length_projects_real_count() {
let q = crate::query::parse::parse("SELECT @length FROM char[]").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_array(1, "char[]", 42);
ex.visit_array(2, "char[]", 7);
ex.visit_array(3, "int[]", 99);
let res = ex.finish("q1");
assert_eq!(res.row_count, 2);
assert_eq!(res.rows[0][0], QueryValue::Int(42));
assert_eq!(res.rows[1][0], QueryValue::Int(7));
}
#[test]
fn array_length_filters_in_where() {
let q = crate::query::parse::parse("SELECT @length FROM char[] WHERE @length > 8").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_array(1, "char[]", 4); ex.visit_array(2, "char[]", 16); let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], QueryValue::Int(16));
}
#[test]
fn array_respects_limit() {
let q = crate::query::parse::parse("SELECT @length FROM char[] LIMIT 2").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_array(1, "char[]", 1);
ex.visit_array(2, "char[]", 2);
ex.visit_array(3, "char[]", 3); let res = ex.finish("q1");
assert_eq!(res.row_count, 2);
assert!(res.truncated, "hitting the LIMIT cap must set truncated");
}
#[test]
fn from_subquery_does_not_early_stop_on_limit() {
let q = crate::query::parse::parse("SELECT * FROM (SELECT * FROM C c) x LIMIT 2").unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]);
ex.visit_instance(3, 10, &[]);
ex.visit_instance(4, 10, &[]);
let res = ex.finish("q1");
assert_eq!(
res.row_count, 4,
"FROM-subquery scan must collect ALL matches (no scan-time LIMIT); \
the LIMIT is applied post-semi-join in run.rs"
);
assert!(
!res.truncated,
"FROM-subquery scan must not set truncated from a LIMIT cap"
);
}
#[test]
fn from_subquery_order_by_does_not_truncate_in_finish() {
let q = crate::query::parse::parse(
"SELECT @objectId, @usedHeapSize FROM (SELECT * FROM C c) x \
ORDER BY @usedHeapSize ASC LIMIT 2",
)
.unwrap();
let plan = pq(&q);
let sc = FieldSchema::count_only();
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(3, 10, &[]);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]);
let res = ex.finish("q1");
assert_eq!(
res.row_count, 3,
"FROM-subquery + ORDER BY must not truncate to LIMIT in finish()"
);
}
#[test]
fn array_field_projection_is_null() {
let q = crate::query::parse::parse("SELECT n FROM char[]").unwrap();
let plan = pq(&q);
let sc = FieldSchema::with_fields(&[("n", 0, crate::types::HprofType::Int)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_array(1, "char[]", 5);
let res = ex.finish("q1");
assert_eq!(res.rows[0][0], QueryValue::Null);
}
struct AddrResolver {
names: std::collections::HashMap<u64, String>,
addrs: std::collections::HashMap<usize, u64>,
}
impl ClassResolver for AddrResolver {
fn class_name(&self, class_id: u64) -> Option<&str> {
self.names.get(&class_id).map(|s| s.as_str())
}
fn addr_of(&self, src_idx: usize) -> Option<u64> {
self.addrs.get(&src_idx).copied()
}
}
#[test]
fn in_subquery_keeps_only_members() {
let q = crate::query::parse::parse(
"SELECT @objectAddress FROM C WHERE @objectAddress IN \
(SELECT @objectAddress FROM D)",
)
.unwrap();
let plan = pq(&q);
assert_eq!(plan.in_subplans.len(), 1);
let sc = AddrResolver {
names: std::iter::once((10u64, "C".to_string())).collect(),
addrs: [(1usize, 0x100u64), (2, 0x200), (3, 0x400), (4, 0x800)]
.into_iter()
.collect(),
};
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
let set: std::collections::HashSet<u64> = [0x200u64, 0x400].into_iter().collect();
ex.set_in_subquery_sets(vec![InSet {
lhs: Attr::ObjectAddress,
set,
truncated: false,
}]);
for i in 1..=4usize {
ex.visit_instance(i, 10, &[]);
}
let res = ex.finish("q1");
assert_eq!(res.row_count, 2, "only the two member addresses survive");
assert_eq!(res.rows[0][0], QueryValue::Int(0x200));
assert_eq!(res.rows[1][0], QueryValue::Int(0x400));
assert!(!res.truncated);
}
#[test]
fn in_subquery_truncated_set_marks_result_truncated() {
let q = crate::query::parse::parse(
"SELECT @objectAddress FROM C WHERE @objectAddress IN \
(SELECT @objectAddress FROM D)",
)
.unwrap();
let plan = pq(&q);
let sc = AddrResolver {
names: std::iter::once((10u64, "C".to_string())).collect(),
addrs: std::iter::once((1usize, 0x200u64)).collect(),
};
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.set_in_subquery_sets(vec![InSet {
lhs: Attr::ObjectAddress,
set: std::iter::once(0x200u64).collect(),
truncated: true,
}]);
ex.visit_instance(1, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert!(
res.truncated,
"a truncated membership set taints the outer result"
);
}
#[test]
fn from_subquery_matches_all_classes() {
let q = crate::query::parse::parse("SELECT @objectId FROM (SELECT * FROM C c) x").unwrap();
let plan = pq(&q);
assert!(plan.from_subplan.is_some());
let sc = schema(&[(10, "C"), (20, "D")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 20, &[]);
let res = ex.finish("q1");
assert_eq!(
res.row_count, 2,
"FROM-subquery outer matches all objects pre-semijoin"
);
}
#[test]
fn alias_overrides_column_name_in_finish() {
let q = crate::query::parse::parse("SELECT @objectId AS myid FROM com.acme.Foo").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(5, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.columns.len(), 1);
assert_eq!(
res.columns[0].name, "myid",
"alias must override derived @objectId name"
);
}
#[test]
fn no_alias_preserves_derived_column_name() {
let q = crate::query::parse::parse("SELECT @usedHeapSize FROM com.acme.Foo").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(5, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.columns[0].name, "@usedHeapSize");
}
#[test]
fn multiple_aliases_applied_per_column() {
let q = crate::query::parse::parse(
"SELECT @objectId AS id, @usedHeapSize AS bytes FROM com.acme.Foo",
)
.unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(5, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.columns.len(), 2);
assert_eq!(res.columns[0].name, "id");
assert_eq!(res.columns[1].name, "bytes");
}
#[test]
fn quoted_alias_applied_to_column() {
let q = crate::query::parse::parse(r#"SELECT @usedHeapSize AS "size" FROM com.acme.Foo"#)
.unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(5, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.columns[0].name, "size");
}
#[test]
fn count_aggregate_alias() {
let q = crate::query::parse::parse("SELECT COUNT(*) AS n FROM com.acme.Foo").unwrap();
let plan = pq(&q);
let sc = schema(&[(10, "com.acme.Foo")]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(5, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.columns[0].name, "n");
}
#[test]
fn query_columns_helper_respects_aliases() {
use crate::query::ast::{Attr, Query, SelectItem};
use crate::query::ast::{ClassSpec, FromSource};
let q = Query {
distinct: false,
select: vec![SelectItem::Attr(Attr::ObjectId), SelectItem::Star],
select_aliases: vec![Some("myid".to_string()), None],
retained_set: false,
from: FromSource::Class(ClassSpec {
instanceof: false,
class_name: "C".into(),
is_regex: false,
}),
alias: None,
where_: None,
order_by: None,
limit: None,
offset: None,
union_branches: Vec::new(),
union_limit: None,
group_by: Vec::new(),
having: None,
intersect_branches: Vec::new(),
except_branches: Vec::new(),
};
let cols = query_columns(&q);
assert_eq!(cols.len(), 2);
assert_eq!(cols[0].name, "myid");
assert_eq!(cols[1].name, "*");
}
struct ShallowSchema {
name: &'static str,
class_id: u64,
sizes: std::collections::HashMap<usize, u32>,
}
impl ShallowSchema {
fn new(name: &'static str, sizes: &[(usize, u32)]) -> Self {
ShallowSchema {
name,
class_id: 10,
sizes: sizes.iter().copied().collect(),
}
}
}
impl ClassResolver for ShallowSchema {
fn class_name(&self, class_id: u64) -> Option<&str> {
if class_id == self.class_id {
Some(self.name)
} else {
None
}
}
fn shallow_of(&self, src_idx: usize) -> Option<u32> {
self.sizes.get(&src_idx).copied()
}
}
#[test]
fn scan_acc_count_star_with_where() {
let q =
crate::query::parse::parse("SELECT COUNT(*) FROM C WHERE @usedHeapSize > 0").unwrap();
let plan = pq(&q);
let sc = ShallowSchema::new("C", &[(1, 24), (2, 24), (3, 24)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]);
ex.visit_instance(3, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1, "aggregate emits exactly 1 row");
assert_eq!(res.rows[0][0], QueryValue::Int(3), "COUNT(*) = 3");
}
#[test]
fn scan_acc_sum_over_expression() {
let q = crate::query::parse::parse(
"SELECT SUM(@usedHeapSize * 2) FROM C WHERE @usedHeapSize > 0",
)
.unwrap();
let plan = pq(&q);
let sc = ShallowSchema::new("C", &[(1, 10), (2, 20), (3, 30)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]);
ex.visit_instance(3, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(
res.rows[0][0],
QueryValue::Int(120),
"SUM(@usedHeapSize * 2) = 120"
);
}
#[test]
fn scan_acc_sum_ignores_null_values() {
let _q = crate::query::parse::parse(
"SELECT SUM(@usedHeapSize * 2) FROM C WHERE @usedHeapSize > 0 OR @usedHeapSize = 0",
)
.unwrap();
let q2 =
crate::query::parse::parse("SELECT SUM(@usedHeapSize) FROM C WHERE @objectId >= 0")
.unwrap();
let plan = pq(&q2);
let sc = ShallowSchema::new("C", &[(1, 100), (3, 50)]); let mut ex = SingleScanExecutor::new(&q2, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]); ex.visit_instance(3, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], QueryValue::Int(150), "SUM skips Null");
}
#[test]
fn scan_acc_avg_is_float() {
let q =
crate::query::parse::parse("SELECT AVG(@usedHeapSize) FROM C WHERE @usedHeapSize > 0")
.unwrap();
let plan = pq(&q);
let sc = ShallowSchema::new("C", &[(1, 10), (2, 20), (3, 30)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]);
ex.visit_instance(3, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], QueryValue::Float(20.0), "AVG = 20.0");
}
#[test]
fn scan_acc_avg_of_empty_is_null() {
let q = crate::query::parse::parse(
"SELECT AVG(@usedHeapSize) FROM Missing WHERE @usedHeapSize > 0",
)
.unwrap();
let plan = pq(&q);
let sc = ShallowSchema::new("C", &[(1, 10)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]); let res = ex.finish("q1");
assert_eq!(
res.row_count, 1,
"aggregate always emits 1 row even with 0 matches"
);
assert_eq!(
res.rows[0][0],
QueryValue::Null,
"AVG with no values → Null"
);
}
#[test]
fn scan_acc_min_max_correct() {
let q = crate::query::parse::parse(
"SELECT MIN(@usedHeapSize), MAX(@usedHeapSize) FROM C WHERE @usedHeapSize > 0",
)
.unwrap();
let plan = pq(&q);
let sc = ShallowSchema::new("C", &[(1, 8), (2, 32), (3, 16)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]);
ex.visit_instance(3, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(res.rows[0][0], QueryValue::Int(8), "MIN = 8");
assert_eq!(res.rows[0][1], QueryValue::Int(32), "MAX = 32");
}
#[test]
fn scan_acc_min_max_of_empty_is_null() {
let q = crate::query::parse::parse(
"SELECT MIN(@usedHeapSize), MAX(@usedHeapSize) FROM Missing WHERE @usedHeapSize > 0",
)
.unwrap();
let plan = pq(&q);
let sc = ShallowSchema::new("C", &[(1, 8)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]); let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(
res.rows[0][0],
QueryValue::Null,
"MIN with no values → Null"
);
assert_eq!(
res.rows[0][1],
QueryValue::Null,
"MAX with no values → Null"
);
}
#[test]
fn scan_acc_sum_promotes_to_float_when_float_present() {
let q = crate::query::parse::parse(
"SELECT SUM(@usedHeapSize / 2.0) FROM C WHERE @usedHeapSize > 0",
)
.unwrap();
let plan = pq(&q);
let sc = ShallowSchema::new("C", &[(1, 10), (2, 20)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
match &res.rows[0][0] {
QueryValue::Float(f) => assert!(
(f - 15.0).abs() < 1e-9,
"SUM of floats must be 15.0, got {f}"
),
other => panic!("expected Float(15.0), got {other:?}"),
}
}
#[test]
fn scan_acc_count_expr_skips_null() {
let q =
crate::query::parse::parse("SELECT COUNT(@usedHeapSize) FROM C WHERE @objectId >= 0")
.unwrap();
let plan = pq(&q);
let sc = ShallowSchema::new("C", &[(1, 24), (3, 24)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]); ex.visit_instance(3, 10, &[]);
let res = ex.finish("q1");
assert_eq!(res.row_count, 1);
assert_eq!(
res.rows[0][0],
QueryValue::Int(2),
"COUNT(expr) must skip Null values, expected 2"
);
}
#[test]
fn scan_no_aggregate_is_unaffected_by_accumulator() {
let q = crate::query::parse::parse("SELECT @usedHeapSize FROM C WHERE @usedHeapSize > 0")
.unwrap();
let plan = pq(&q);
let sc = ShallowSchema::new("C", &[(1, 10), (2, 20), (3, 30)]);
let mut ex = SingleScanExecutor::new(&q, &plan, &sc);
ex.visit_instance(1, 10, &[]);
ex.visit_instance(2, 10, &[]);
ex.visit_instance(3, 10, &[]);
let res = ex.finish("q1");
assert_eq!(
res.row_count, 3,
"non-aggregate must produce per-object rows"
);
assert_eq!(res.rows[0][0], QueryValue::Int(10));
assert_eq!(res.rows[1][0], QueryValue::Int(20));
assert_eq!(res.rows[2][0], QueryValue::Int(30));
}
}