pub mod ast;
pub mod dynamic_labels;
pub mod executor;
mod explain;
pub mod parse_cache;
pub mod parser;
pub mod plan_cache;
pub mod planner;
pub mod result;
#[cfg(test)]
mod stack_probe;
pub mod tokenizer;
pub mod value_codec;
mod window;
pub use executor::{is_mutation_query, CypherExecutor};
pub use explain::generate_explain_result;
pub use parse_cache::parse_cypher_cached as parse_cypher;
pub use planner::mark_lazy_eligibility;
pub use planner::optimize;
pub use planner::schema_check::validate_schema;
pub(crate) use planner::schema_check::{
collect_query_warnings, emit_query_warnings, strict_read_error, strict_type_error,
};
pub use planner::simplification::rewrite_text_score;
use crate::datatypes::values::Value;
use ast::*;
#[allow(clippy::result_large_err)]
pub fn parameter_names(query: &str) -> Result<Vec<String>, crate::error::KgError> {
let positioned = tokenizer::tokenize_cypher_with_positions(query).map_err(|message| {
crate::error::KgError::CypherSyntax {
message,
line: None,
col: None,
}
})?;
let mut names = Vec::new();
for (token, _) in positioned.tokens {
if let tokenizer::CypherToken::Parameter(name) = token {
if !names.contains(&name) {
names.push(name);
}
}
}
Ok(names)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryFeatures {
pub is_mutation: bool,
pub explain: bool,
pub profile: bool,
pub format_csv: bool,
pub has_load_csv: bool,
pub literal_limits: Vec<i64>,
}
#[allow(clippy::result_large_err)]
pub fn query_features(query: &str) -> Result<QueryFeatures, crate::error::KgError> {
let parsed = parse_cypher(query)?;
let mut features = QueryFeatures {
is_mutation: is_mutation_query(&parsed),
explain: false,
profile: false,
format_csv: false,
has_load_csv: false,
literal_limits: Vec::new(),
};
collect_query_features(&parsed, &mut features);
Ok(features)
}
fn collect_query_features(query: &CypherQuery, features: &mut QueryFeatures) {
features.explain |= query.explain;
features.profile |= query.profile;
features.format_csv |= query.output_format == OutputFormat::Csv;
for clause in &query.clauses {
match clause {
Clause::LoadCsv(_) => features.has_load_csv = true,
Clause::Limit(limit) => {
if let Expression::Literal(Value::Int64(value)) = &limit.count {
features.literal_limits.push(*value);
}
}
Clause::CallSubquery { body, .. } => collect_query_features(body, features),
Clause::Union(union) => collect_query_features(&union.query, features),
_ => {}
}
}
}
#[allow(clippy::result_large_err)]
pub fn parse_with_mutation_check(
query: &str,
) -> Result<(ast::CypherQuery, bool), crate::error::KgError> {
let parsed = parse_cypher(query)?;
let is_mutation = is_mutation_query(&parsed);
Ok((parsed, is_mutation))
}
#[cfg(test)]
mod parameter_name_tests {
use crate::api::cypher::parameter_names;
use crate::error::{KgError, KgErrorCode};
#[test]
fn ignores_comments_and_string_literals() {
let query = r#"
MATCH (n)
WHERE n.name = $name
AND n.note = 'literal $ignored'
AND n.other = "$also_ignored"
// $commented_out
RETURN n
"#;
assert_eq!(parameter_names(query).unwrap(), ["name"]);
}
#[test]
fn deduplicates_in_first_appearance_order() {
let query = "RETURN $second, $first, $second, $third, $first";
assert_eq!(
parameter_names(query).unwrap(),
["second", "first", "third"]
);
}
#[test]
fn finds_parameters_in_nested_expressions() {
let query = "RETURN coalesce($fallback, {items: [$first, {value: $second}]})";
assert_eq!(
parameter_names(query).unwrap(),
["fallback", "first", "second"]
);
}
#[test]
fn invalid_parameter_syntax_is_a_typed_cypher_error() {
let error = parameter_names("RETURN $").unwrap_err();
assert!(matches!(error, KgError::CypherSyntax { .. }));
assert_eq!(error.code(), KgErrorCode::CypherSyntax);
}
}
#[cfg(test)]
mod query_feature_tests {
use crate::api::cypher::query_features;
#[test]
fn reports_top_level_modes_and_mutation() {
let explain = query_features("EXPLAIN RETURN 1 FORMAT CSV").unwrap();
assert!(explain.explain);
assert!(explain.format_csv);
assert!(!explain.profile);
assert!(!explain.is_mutation);
let mutation = query_features("CREATE (:Thing)").unwrap();
assert!(mutation.is_mutation);
}
#[test]
fn reports_load_csv_without_matching_string_or_comment_text() {
let load = query_features("LOAD CSV FROM 'rows.csv' AS row RETURN row").unwrap();
assert!(load.has_load_csv);
let ordinary = query_features("// LOAD CSV\nRETURN 'LOAD CSV' AS text").unwrap();
assert!(!ordinary.has_load_csv);
}
#[test]
fn collects_only_literal_limits_across_the_query_tree() {
let features = query_features(
"CALL { RETURN 1 AS n LIMIT 10 } RETURN n LIMIT $outer \
UNION ALL RETURN 2 AS n LIMIT 200",
)
.unwrap();
assert_eq!(features.literal_limits, [10, 200]);
let lookalikes = query_features("RETURN 'LIMIT 200' AS text").unwrap();
assert!(lookalikes.literal_limits.is_empty());
}
}