pub mod visit;
use crate::value::Value;
use globset::GlobMatcher;
use regex::Regex;
#[derive(Debug, Clone)]
pub struct Query {
pub correlations: Vec<Query>,
pub branches: Vec<Branch>,
pub pipeline: Vec<Command>,
pub join_at: usize,
pub outer: bool,
}
#[derive(Debug, Clone)]
pub enum Anchor {
Current,
Root,
Mark(String),
MarkIndex(usize),
MarkTop,
MarkBack(usize),
MarksAll,
MarksNamed(String),
Id(Box<Operand>),
}
impl PartialEq for Anchor {
fn eq(&self, other: &Anchor) -> bool {
match (self, other) {
(Anchor::Current, Anchor::Current)
| (Anchor::Root, Anchor::Root)
| (Anchor::MarkTop, Anchor::MarkTop)
| (Anchor::MarksAll, Anchor::MarksAll) => true,
(Anchor::Mark(a), Anchor::Mark(b)) | (Anchor::MarksNamed(a), Anchor::MarksNamed(b)) => {
a == b
}
(Anchor::MarkIndex(a), Anchor::MarkIndex(b)) => a == b,
(Anchor::Id(a), Anchor::Id(b)) => {
crate::syntax::unparse::operand_text(a) == crate::syntax::unparse::operand_text(b)
}
_ => false,
}
}
}
impl Eq for Anchor {}
#[derive(Debug, Clone)]
pub struct Branch {
pub steps: Vec<PathElem>,
pub projection: Option<Projection>,
pub anchor: Anchor,
}
#[derive(Debug, Clone)]
pub enum Command {
Func(FnCall),
Push(Option<String>),
Subcontext {
name: Option<String>,
body: Box<Query>,
},
Expr(Operand),
ExprPush { name: Option<String>, expr: Operand },
Agg(FnCall),
AggPush { name: Option<String>, body: Operand },
AggRecord { call: FnCall, enriched: bool },
AggExpr(Operand),
AggRecall(RegRef),
Select(Predicate),
Filter(PredExpr),
Recall(RegRef),
Splice(RegRef),
SessionPush(String),
RecordWith(FnCall),
RecordPush {
name: Option<String>,
call: FnCall,
enriched: bool,
},
Spread { outer: bool },
FieldsPush,
Nav(Branch),
Map(Box<Command>),
Repeat {
command: Box<Command>,
min: usize,
max: Option<usize>,
},
}
pub(crate) fn row_key(name: &str) -> (&str, bool) {
match name.strip_prefix('\u{0}') {
Some(text) => (text, true),
None => (name, false),
}
}
pub const CORE_KEYS: &[&str] = &[
"name",
"id",
"index",
"depth",
"parent-name",
"parent-id",
"parent-index",
"n-children",
"n-descendants",
"n-siblings",
"traits",
"is-leaf",
"is-root",
"is-first-child",
"is-last-child",
"is-top-level",
"is-non-tree",
"name-path",
"index-path",
"id-path",
"path",
"source",
"instant",
"dpid",
"provenance",
"origin",
"elided",
"%provenance",
"@provenance",
"%origin",
"@origin",
];
#[derive(Debug, Clone)]
pub enum RegRef {
Session(String),
SessionRecord,
Top,
Index(usize),
Back(usize),
Named(String),
Whole,
Record,
FullRecord,
NodeWhole,
NodeRecord,
NodeFullRecord,
}
#[derive(Debug, Clone)]
pub struct FnCall {
pub name: String,
pub args: Vec<Arg>,
}
impl FnCall {
pub fn of_expr(&self) -> Option<&Operand> {
self.args.iter().find_map(|a| match a {
Arg::Opt {
name,
value: Some(e),
} if name == "--of" || name == "-o" => Some(e),
_ => None,
})
}
pub fn without_of(&self) -> FnCall {
FnCall {
name: self.name.clone(),
args: self
.args
.iter()
.filter(|a| !matches!(a, Arg::Opt { name, .. } if name == "--of" || name == "-o"))
.cloned()
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub enum Arg {
Key(Value),
Lit(Value),
Expr(Operand),
Range(Option<i64>, Option<i64>),
Named {
name: String,
value: Value,
},
Opt {
name: String,
value: Option<Operand>,
},
Pairs {
keys: Operand,
values: Operand,
},
}
pub fn auto_field_name(op: &Operand) -> Option<&str> {
if let Operand::Invoking(inner) | Operand::Position(inner) | Operand::Driver(inner) = op {
return auto_field_name(inner);
}
if let Operand::Recall(RegRef::Named(n)) = op {
return Some(n);
}
if let Operand::Field { name, .. } = op {
return Some(name);
}
if let Operand::Typed { inner, .. } = op {
return auto_field_name(inner);
}
if let Operand::Lit(crate::Value::Projection { key, .. }) = op {
return Some(key);
}
if let Operand::ValueMeta { key, .. } = op {
return Some(key);
}
if let Operand::PeerReg {
reg: RegRef::Named(n),
..
} = op
{
return Some(n);
}
if matches!(op, Operand::Ordinal) {
return Some("ordinal");
}
if let Operand::Edge { projection } | Operand::Edges { projection } = op {
return Some(match projection {
Some(Projection::Property(Some(k))) => k,
_ => {
if matches!(op, Operand::Edge { .. }) {
"edge"
} else {
"edges"
}
}
});
}
let projection = match op {
Operand::Rel { projection, .. } | Operand::Ctx { projection, .. } => projection.as_ref(),
_ => None,
}?;
match projection {
Projection::Property(Some(k)) => Some(k),
Projection::CoreMeta(k) | Projection::AdapterMeta(k) => Some(k),
Projection::Property(None) => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Projection {
Property(Option<String>),
CoreMeta(String),
AdapterMeta(String),
}
#[derive(Debug, Clone)]
pub enum PathElem {
Mark(Option<String>),
Step(Step),
Group(Group),
Push {
name: Option<String>,
body: PushBody,
},
}
#[derive(Debug, Clone)]
pub enum PushBody {
Query(Box<Query>),
Expr(Operand),
}
#[derive(Debug, Clone)]
pub struct Group {
pub alts: Vec<Vec<PathElem>>,
pub quant: Quant,
pub predicates: Vec<Predicate>,
pub traits: Vec<TraitClause>,
pub reach: Reach,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Quant {
pub min: usize,
pub max: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct Step {
pub axis: Axis,
pub matcher: Matcher,
pub labels: Vec<LabelClause>,
pub traits: Vec<TraitClause>,
pub predicates: Vec<Predicate>,
pub leaf: bool,
}
#[derive(Debug, Clone)]
pub enum Predicate {
Index(i64),
Range(Option<i64>, Option<i64>),
Expr(PredExpr),
Positions(Vec<Predicate>),
Computed(Operand),
}
#[derive(Debug, Clone)]
pub enum PredExpr {
Or(Box<PredExpr>, Box<PredExpr>),
And(Box<PredExpr>, Box<PredExpr>),
Not(Box<PredExpr>),
Compare(Operand, CmpOp, Operand),
Chain(Vec<Operand>, Vec<CmpOp>),
Truthy(Operand),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatSeg {
Star,
Lit(String),
}
#[derive(Debug, Clone)]
pub enum IndexSpec {
At(i64),
Range(Option<i64>, Option<i64>),
Name(String),
List(Vec<IndexSpec>),
Expr(Box<Operand>),
Cond(Box<PredExpr>),
Coords(Vec<IndexSpec>),
}
#[derive(Debug, Clone)]
pub enum ProjShape {
Value(Box<Operand>),
Columns {
items: Vec<(Option<String>, Operand)>,
index: Option<Box<Operand>>,
},
}
#[derive(Debug, Clone)]
pub enum TableRow {
List(Vec<Operand>),
Record(Vec<(Option<String>, Operand)>),
}
#[derive(Debug, Clone)]
pub enum Operand {
Pattern(Vec<PatSeg>),
Rel {
steps: Vec<PathElem>,
projection: Option<Projection>,
anchor: Anchor,
},
Lit(Value),
Arith {
op: ArithOp,
left: Box<Operand>,
right: Box<Operand>,
},
Concat {
mode: ConcatMode,
left: Box<Operand>,
right: Box<Operand>,
},
Row(Vec<(Option<String>, Operand)>),
Neg(Box<Operand>),
Desc(Box<Operand>),
Projected { base: Box<Operand>, shape: ProjShape },
Spread(Box<Operand>),
Gathered(Box<Operand>),
Group(Box<PredExpr>),
Recall(RegRef),
Topic,
Cursor,
Field { base: Box<Operand>, name: String },
LabelIndex { base: Box<Operand>, labels: Vec<Operand> },
Typed { inner: Box<Operand>, typing: crate::value::Typing },
ValueMeta { base: Box<Operand>, key: String },
PeerReg { at: Box<Operand>, reg: RegRef },
NamedCaptures,
List(Vec<Operand>),
Table {
header: Option<Vec<String>>,
rows: Vec<TableRow>,
},
RecordArray {
header: Vec<String>,
rows: Vec<Vec<(Option<String>, Operand)>>,
},
Range {
from: Box<Operand>,
to: Box<Operand>,
step: Option<Box<Operand>>,
},
Index {
base: Box<Operand>,
rows: IndexSpec,
cols: Option<IndexSpec>,
},
Ordinal,
Param(String),
Capture(usize),
Invoking(Box<Operand>),
Position(Box<Operand>),
Driver(Box<Operand>),
Edge { projection: Option<Projection> },
Topics,
Positions { projection: Option<Projection> },
Matrix { node: bool, full: bool },
CtxTable,
Seed,
Piped {
expr: Box<Operand>,
commands: Vec<Command>,
},
Now,
Cond {
cond: Box<PredExpr>,
then: Box<Operand>,
other: Box<Operand>,
},
Coalesce {
left: Box<Operand>,
right: Box<Operand>,
},
Edges { projection: Option<Projection> },
Interp(Vec<InterpSeg>),
Match {
scrutinee: Box<Operand>,
arms: Vec<(Operand, bool, Operand)>,
other: Box<Operand>,
},
Ctx {
index: Option<usize>,
steps: Vec<PathElem>,
projection: Option<Projection>,
},
}
#[derive(Debug, Clone)]
pub enum InterpSeg {
Text(String),
Expr(Operand),
Strict(Operand, Option<String>),
Default(Operand, Operand),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArithOp {
Add,
Sub,
Mul,
Div,
IDiv,
Mod,
MatMul,
}
impl ArithOp {
pub fn symbol(self) -> &'static str {
match self {
ArithOp::Add => "+",
ArithOp::Sub => "-",
ArithOp::Mul => "*",
ArithOp::Div => "div",
ArithOp::IDiv => "idiv",
ArithOp::Mod => "mod",
ArithOp::MatMul => "x",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConcatMode {
Merge,
Overwrite,
Item,
}
impl ConcatMode {
pub fn symbol(self) -> &'static str {
match self {
ConcatMode::Merge => "++",
ConcatMode::Overwrite => "++!",
ConcatMode::Item => "+@+",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CmpOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
Match,
NotMatch,
Contains,
Within,
Encloses,
}
#[derive(Debug, Clone)]
pub struct TraitClause {
pub alts: Vec<String>,
}
pub fn trait_has_wildcard(alt: &str) -> bool {
alt.trim_start_matches('!')
.split(':')
.any(|seg| seg == "." || seg == "*")
}
pub fn trait_pattern_matches(pattern: &str, name: &str) -> bool {
fn go(pat: &[&str], name: &[&str]) -> bool {
match (pat.split_first(), name.split_first()) {
(None, None) => true,
(None, Some(_)) | (Some(_), None) => false,
(Some((&"*", rest)), Some(_)) => (1..=name.len()).any(|k| go(rest, &name[k..])),
(Some((&".", rest)), Some((_, tail))) => go(rest, tail),
(Some((p, rest)), Some((n, tail))) => p == n && go(rest, tail),
}
}
let pat: Vec<&str> = pattern.split(':').collect();
let segs: Vec<&str> = name.split(':').collect();
go(&pat, &segs)
}
impl TraitClause {
pub fn matches_with(&self, traits: &[String], has: impl Fn(&str) -> bool) -> bool {
self.alts.iter().any(|alt| {
let (neg, lit) = match alt.strip_prefix('!') {
Some(lit) => (true, lit),
None => (false, alt.as_str()),
};
let hit = if trait_has_wildcard(lit) {
traits.iter().any(|t| trait_pattern_matches(lit, t))
} else {
has(lit)
};
hit != neg
})
}
}
#[derive(Debug, Clone)]
pub struct LabelClause {
pub alts: Vec<LabelAlt>,
}
#[derive(Debug, Clone)]
pub struct LabelAlt {
pub neg: bool,
pub matcher: Matcher,
}
impl LabelClause {
pub fn matches(&self, labels: &[String]) -> bool {
self.alts.iter().any(|a| {
let hit = labels.iter().any(|l| a.matcher.matches(l));
hit != a.neg
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Axis {
Child,
Descendant(Reach),
Universal,
Parent,
Ancestor(Reach),
NextSibling,
PrevSibling,
FollowingSiblings(Reach),
PrecedingSiblings(Reach),
OutLink,
InLink,
BothLink,
Resolve {
property: Option<String>,
label: Option<String>,
},
ReverseResolve {
label: Option<String>,
scope: Option<ScopePath>,
property: RefProp,
},
ValueMatch {
property: String,
scope: Option<ScopePath>,
target: String,
},
}
#[derive(Debug, Clone)]
pub struct ScopePath(pub Vec<PathElem>);
impl ScopePath {
pub fn text(&self) -> String {
self.0.iter().map(crate::syntax::unparse::elem).collect()
}
}
impl PartialEq for ScopePath {
fn eq(&self, other: &ScopePath) -> bool {
self.text() == other.text()
}
}
impl Eq for ScopePath {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefProp {
Named(String),
Any,
Default,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reach {
All,
Proximal,
Distal,
}
#[derive(Clone)]
pub enum Matcher {
Name(String),
Glob(GlobMatcher),
Regex(Regex),
Any,
Dot,
}
impl Matcher {
pub fn matches(&self, name: &str) -> bool {
match self {
Matcher::Name(n) => n == name,
Matcher::Glob(g) => g.is_match(name),
Matcher::Regex(r) => r.is_match(name),
Matcher::Any | Matcher::Dot => true,
}
}
}
impl std::fmt::Debug for Matcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Matcher::Name(n) => write!(f, "Name({n:?})"),
Matcher::Glob(g) => write!(f, "Glob({:?})", g.glob().glob()),
Matcher::Regex(r) => write!(f, "Regex({:?})", r.as_str()),
Matcher::Any => write!(f, "Any"),
Matcher::Dot => write!(f, "Dot"),
}
}
}
#[cfg(test)]
mod trait_segment_tests {
use super::{TraitClause, trait_has_wildcard, trait_pattern_matches};
#[test]
fn whole_segments_are_the_only_wildcards() {
assert!(trait_has_wildcard("*"));
assert!(trait_has_wildcard("tag:."));
assert!(trait_has_wildcard("*:draft"));
assert!(trait_has_wildcard("!tag:*"));
assert!(!trait_has_wildcard("head*"));
assert!(!trait_has_wildcard("v1.2"));
assert!(!trait_has_wildcard(".draft"));
assert!(!trait_has_wildcard("tag:pandas"));
}
#[test]
fn dot_is_one_segment_star_a_run() {
assert!(trait_pattern_matches("tag:.", "tag:capsa"));
assert!(!trait_pattern_matches("tag:.", "tag"));
assert!(!trait_pattern_matches("tag:.", "tag:a:b"));
assert!(trait_pattern_matches("tag:*", "tag:capsa"));
assert!(trait_pattern_matches("tag:*", "tag:a:b"));
assert!(!trait_pattern_matches("tag:*", "tag"));
assert!(trait_pattern_matches(".:draft", "label:draft"));
assert!(!trait_pattern_matches(".:draft", "draft"));
assert!(trait_pattern_matches("*:draft", "a:b:draft"));
assert!(!trait_pattern_matches("*:draft", "draft"));
assert!(trait_pattern_matches(".", "draft"));
assert!(!trait_pattern_matches(".", "tag:draft"));
assert!(trait_pattern_matches("*", "draft"));
assert!(trait_pattern_matches("*", "tag:draft"));
assert!(trait_pattern_matches("a:*:b", "a:x:y:b"));
assert!(!trait_pattern_matches("a:*:b", "a:b"));
}
#[test]
fn clause_runs_literals_through_has_and_wildcards_over_the_list() {
let traits = vec!["tag:capsa".to_string(), "block".to_string()];
let has = |n: &str| traits.iter().any(|t| t == n) || n == "alias";
let c = |alts: &[&str]| TraitClause {
alts: alts.iter().map(|s| s.to_string()).collect(),
};
assert!(c(&["alias"]).matches_with(&traits, has));
assert!(c(&["tag:."]).matches_with(&traits, has));
assert!(c(&["."]).matches_with(&traits, has));
assert!(!c(&["*:draft"]).matches_with(&traits, has));
assert!(c(&["!tag:*"]).matches_with(&[], has));
assert!(!c(&["!tag:*"]).matches_with(&traits, has));
assert!(c(&["*"]).matches_with(&traits, has));
assert!(!c(&["*"]).matches_with(&[], has));
assert!(c(&["!*"]).matches_with(&[], has));
assert!(!c(&["!*"]).matches_with(&traits, has));
}
}