use super::*;
pub(super) const CHECKPOINT_COLUMNS: [&str; 2] = ["success", "message"];
#[derive(Debug, PartialEq, Eq)]
pub(super) struct CheckpointCall {
pub(super) columns: Vec<&'static str>,
}
pub(super) fn strip_keyword_ci<'a>(input: &'a str, keyword: &str) -> Option<&'a str> {
if !input.get(..keyword.len())?.eq_ignore_ascii_case(keyword) {
return None;
}
let rest = &input[keyword.len()..];
match rest.chars().next() {
Some(c) if c.is_alphanumeric() || c == '_' => None,
_ => Some(rest.trim_start()),
}
}
pub(super) fn strip_cypher_prefix(input: &str) -> &str {
let Some(mut rest) = strip_keyword_ci(input, "cypher") else {
return input;
};
let mut first = true;
loop {
let word_end = rest.find(|c: char| c.is_whitespace()).unwrap_or(rest.len());
let word = &rest[..word_end];
let is_version = first && matches!(word, "5" | "25");
let is_option = word.split_once('=').is_some_and(|(name, value)| {
!name.is_empty()
&& !value.is_empty()
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
});
if !is_version && !is_option {
return rest;
}
first = false;
rest = rest[word_end..].trim_start();
}
}
pub(super) fn parse_checkpoint_call(query: &str) -> Option<CheckpointCall> {
parse_procedure_call(query, "db.checkpoint", &CHECKPOINT_COLUMNS)
.map(|columns| CheckpointCall { columns })
}
pub(super) fn checkpoint_stream(
call: &CheckpointCall,
message: String,
type_str: &str,
started: Instant,
) -> ResultStream {
let values: Vec<BoltValue> = call
.columns
.iter()
.map(|column| {
debug_assert!(
CHECKPOINT_COLUMNS.contains(column),
"parse_checkpoint_call yielded an unknown column: {column}"
);
if *column == CHECKPOINT_COLUMNS[0] {
BoltValue::Boolean(true)
} else {
BoltValue::String(message.clone())
}
})
.collect();
ResultStream {
metadata: ResultMetadata {
columns: call.columns.iter().map(|c| (*c).to_string()).collect(),
extra: BoltDict::new(),
},
records: vec![BoltRecord { values }],
summary: BoltDict::from([
("type".to_string(), BoltValue::String(type_str.to_string())),
(
"t_last".to_string(),
BoltValue::Integer(started.elapsed().as_millis() as i64),
),
]),
}
}
pub(super) const COMPONENTS_COLUMNS: [&str; 3] = ["name", "versions", "edition"];
pub(super) const SHOW_CURRENT_USER_COLUMNS: [&str; 3] = ["username", "roles", "flags"];
pub(super) const SHOW_DATABASES_COLUMNS: [&str; 13] = [
"name",
"type",
"aliases",
"access",
"address",
"role",
"writer",
"requestedStatus",
"currentStatus",
"statusMessage",
"default",
"home",
"constituents",
];
#[derive(Debug, PartialEq, Eq)]
pub(super) enum ServerFactsVerb {
DbmsComponents,
DbmsShowCurrentUser,
ShowDatabases,
}
#[derive(Debug, PartialEq, Eq)]
pub(super) struct ServerFactsCall {
pub(super) verb: ServerFactsVerb,
pub(super) columns: Vec<&'static str>,
}
pub(super) fn parse_procedure_call(
query: &str,
proc: &str,
declared: &'static [&'static str],
) -> Option<Vec<&'static str>> {
let mut body = query.trim();
if let Some(stripped) = body.strip_suffix(';') {
body = stripped.trim_end();
}
let rest = strip_keyword_ci(body, "call")?;
let rest = strip_keyword_ci(rest, proc)?;
let rest = rest.strip_prefix('(')?.trim_start();
let rest = rest.strip_prefix(')')?.trim_start();
parse_yield_tail(rest, declared)
}
fn parse_yield_tail(rest: &str, declared: &'static [&'static str]) -> Option<Vec<&'static str>> {
if rest.is_empty() {
return Some(declared.to_vec());
}
let yielded = strip_keyword_ci(rest, "yield")?;
let mut columns: Vec<&'static str> = Vec::with_capacity(declared.len());
for raw in yielded.split(',') {
let name = raw.trim();
let canonical = declared.iter().find(|column| **column == name)?;
if columns.contains(canonical) {
return None;
}
columns.push(*canonical);
}
Some(columns)
}
pub(super) const BACKUP_COLUMNS: [&str; 10] = [
"success",
"path",
"lsn",
"nodes",
"relationships",
"bytes",
"lock_hold_ms",
"elapsed_ms",
"graph_version",
"prepared_copy",
];
#[derive(Debug, PartialEq, Eq)]
pub(super) enum BackupArg {
Literal(String),
Param(String),
}
#[derive(Debug, PartialEq, Eq)]
pub(super) struct BackupCall {
pub(super) arg: BackupArg,
pub(super) columns: Vec<&'static str>,
}
pub(super) fn parse_backup_call(query: &str) -> Option<BackupCall> {
let mut body = query.trim();
if let Some(stripped) = body.strip_suffix(';') {
body = stripped.trim_end();
}
let rest = strip_keyword_ci(body, "call")?;
let rest = strip_keyword_ci(rest, "db.backup")?;
let rest = rest.strip_prefix('(')?.trim_start();
let (arg, rest) = if let Some(after) = rest.strip_prefix('$') {
let end = after
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
.unwrap_or(after.len());
if end == 0 {
return None;
}
(BackupArg::Param(after[..end].to_string()), &after[end..])
} else {
let quote = rest.chars().next().filter(|c| *c == '\'' || *c == '"')?;
let after = &rest[1..];
let end = after.find(quote)?;
let literal = &after[..end];
if literal.contains('\\') {
return None;
}
(BackupArg::Literal(literal.to_string()), &after[end + 1..])
};
let rest = rest.trim_start().strip_prefix(')')?.trim_start();
let columns = parse_yield_tail(rest, &BACKUP_COLUMNS)?;
Some(BackupCall { arg, columns })
}
pub(super) fn backup_stream(
call: &BackupCall,
report: &kglite::api::session::BackupReport,
started: Instant,
) -> ResultStream {
let values: Vec<BoltValue> = call
.columns
.iter()
.map(|column| match *column {
"success" => BoltValue::Boolean(true),
"path" => BoltValue::String(report.path.display().to_string()),
"lsn" => report
.lsn
.map_or(BoltValue::Null, |lsn| BoltValue::Integer(lsn as i64)),
"nodes" => BoltValue::Integer(report.nodes as i64),
"relationships" => BoltValue::Integer(report.relationships as i64),
"bytes" => BoltValue::Integer(report.bytes as i64),
"lock_hold_ms" => BoltValue::Integer(report.lock_hold.as_millis() as i64),
"elapsed_ms" => BoltValue::Integer(report.elapsed.as_millis() as i64),
"graph_version" => BoltValue::Integer(report.graph_version as i64),
"prepared_copy" => BoltValue::Boolean(report.prepared_copy),
other => unreachable!("parse_backup_call yielded an unknown column: {other}"),
})
.collect();
ResultStream {
metadata: ResultMetadata {
columns: call.columns.iter().map(|c| (*c).to_string()).collect(),
extra: BoltDict::new(),
},
records: vec![BoltRecord { values }],
summary: BoltDict::from([
("type".to_string(), BoltValue::String("r".to_string())),
(
"t_last".to_string(),
BoltValue::Integer(started.elapsed().as_millis() as i64),
),
]),
}
}
pub(super) fn parse_server_facts_call(query: &str) -> Option<ServerFactsCall> {
if let Some(columns) = parse_procedure_call(query, "dbms.components", &COMPONENTS_COLUMNS) {
return Some(ServerFactsCall {
verb: ServerFactsVerb::DbmsComponents,
columns,
});
}
if let Some(columns) =
parse_procedure_call(query, "dbms.showcurrentuser", &SHOW_CURRENT_USER_COLUMNS)
{
return Some(ServerFactsCall {
verb: ServerFactsVerb::DbmsShowCurrentUser,
columns,
});
}
let mut body = query.trim();
if let Some(stripped) = body.strip_suffix(';') {
body = stripped.trim_end();
}
let rest = strip_keyword_ci(body, "show")?;
let rest = strip_keyword_ci(rest, "databases")?;
if rest.is_empty() {
return Some(ServerFactsCall {
verb: ServerFactsVerb::ShowDatabases,
columns: SHOW_DATABASES_COLUMNS.to_vec(),
});
}
None
}
pub(super) fn components_pipeline_rewrite(
query: &str,
rows: &[(String, Vec<String>)],
edition: &str,
) -> Option<String> {
let mut body = query.trim();
if let Some(stripped) = body.strip_suffix(';') {
body = stripped.trim_end();
}
let rest = strip_keyword_ci(body, "call")?;
let rest = strip_keyword_ci(rest, "dbms.components")?;
let rest = rest.strip_prefix('(')?.trim_start();
let rest = rest.strip_prefix(')')?.trim_start();
let (yielded, tail) = match strip_keyword_ci(rest, "yield") {
Some(after) => {
let at = first_clause_keyword(after)?;
(Some(after[..at].trim()), &after[at..])
}
None => (None, rest),
};
let first_word = tail.split(|c: char| !c.is_ascii_alphabetic()).next()?;
if !["where", "return", "with"]
.iter()
.any(|k| first_word.eq_ignore_ascii_case(k))
{
return None;
}
let mut projected: Vec<(&str, &str)> = Vec::new();
match yielded {
None | Some("*") => {
projected.extend(COMPONENTS_COLUMNS.iter().map(|c| (*c, *c)));
}
Some(list) => {
for raw in list.split(',') {
let mut words = raw.split_whitespace();
let column = words.next()?;
COMPONENTS_COLUMNS.iter().find(|c| **c == column)?;
let alias = match (words.next(), words.next(), words.next()) {
(None, ..) => column,
(Some(kw), Some(alias), None) if kw.eq_ignore_ascii_case("as") => alias,
_ => return None,
};
if projected.iter().any(|(_, a)| *a == alias) {
return None;
}
projected.push((column, alias));
}
}
}
let quote = |text: &str| format!("'{}'", text.replace('\\', "\\\\").replace('\'', "\\'"));
let row_literals: Vec<String> = rows
.iter()
.map(|(name, versions)| {
let versions: Vec<String> = versions.iter().map(|v| quote(v)).collect();
format!(
"{{name: {}, versions: [{}], edition: {}}}",
quote(name),
versions.join(", "),
quote(edition)
)
})
.collect();
let with: Vec<String> = projected
.iter()
.map(|(column, alias)| format!("__components.{column} AS {alias}"))
.collect();
Some(format!(
"UNWIND [{}] AS __components WITH {} {}",
row_literals.join(", "),
with.join(", "),
tail
))
}
fn first_clause_keyword(text: &str) -> Option<usize> {
let bytes = text.as_bytes();
let mut start = 0;
while start < bytes.len() {
if bytes[start].is_ascii_alphabetic() && (start == 0 || !is_word_byte(bytes[start - 1])) {
let end = text[start..]
.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
.map_or(text.len(), |n| start + n);
let word = &text[start..end];
if ["where", "return", "with"]
.iter()
.any(|k| word.eq_ignore_ascii_case(k))
{
return Some(start);
}
start = end;
} else {
start += 1;
}
}
None
}
fn is_word_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
pub(super) fn plan_from_explain_rows(
columns: &[String],
rows: &[Vec<kglite::datatypes::values::Value>],
) -> Option<BoltValue> {
use kglite::datatypes::values::Value;
let step_idx = columns.iter().position(|c| c == "step")?;
let op_idx = columns.iter().position(|c| c == "operation")?;
let est_idx = columns.iter().position(|c| c == "estimated_rows")?;
let mut steps: Vec<(i64, String, Option<i64>)> = Vec::new();
let mut passes: Vec<BoltValue> = Vec::new();
for row in rows {
let step = match row.get(step_idx)? {
Value::Int64(i) => *i,
_ => return None,
};
let op = match row.get(op_idx)? {
Value::String(s) => s.clone(),
_ => return None,
};
let est = match row.get(est_idx) {
Some(Value::Int64(i)) => Some(*i),
_ => None,
};
if let Some(name) = op.strip_prefix("OptimizerPass ") {
passes.push(BoltValue::String(name.to_string()));
} else {
steps.push((step, op, est));
}
}
if steps.is_empty() {
return None;
}
steps.sort_by_key(|(step, _, _)| *step);
let text: String = {
let mut lines = Vec::with_capacity(steps.len() + passes.len());
for (_, op, est) in steps.iter().rev() {
match est {
Some(est) => lines.push(format!("+ {op} (estimated rows: {est})")),
None => lines.push(format!("+ {op}")),
}
}
if !passes.is_empty() {
let names: Vec<&str> = passes
.iter()
.filter_map(|p| match p {
BoltValue::String(s) => Some(s.as_str()),
_ => None,
})
.collect();
lines.push(format!("optimizer passes: {}", names.join(", ")));
}
lines.join("\n")
};
let mut node: Option<BoltValue> = None;
let last = steps.len() - 1;
for (i, (_, op, est)) in steps.into_iter().enumerate() {
let mut args = BoltDict::new();
if let Some(est) = est {
args.insert("EstimatedRows".to_string(), BoltValue::Float(est as f64));
}
if i == last {
args.insert(
"string-representation".to_string(),
BoltValue::String(text.clone()),
);
args.insert(
"runtime".to_string(),
BoltValue::String("kglite".to_string()),
);
if !passes.is_empty() {
args.insert(
"optimizer-passes".to_string(),
BoltValue::List(std::mem::take(&mut passes)),
);
}
}
let children = match node.take() {
Some(child) => vec![child],
None => Vec::new(),
};
node = Some(BoltValue::Dict(BoltDict::from([
("operatorType".to_string(), BoltValue::String(op)),
("args".to_string(), BoltValue::Dict(args)),
("identifiers".to_string(), BoltValue::List(Vec::new())),
("children".to_string(), BoltValue::List(children)),
])));
}
node
}
pub(super) fn server_facts_stream(
columns: &[&'static str],
values: &[(&'static str, BoltValue)],
started: Instant,
) -> ResultStream {
server_facts_rows_stream(columns, &[values.to_vec()], started)
}
pub(super) fn components_stream(
columns: &[&'static str],
rows: &[(String, Vec<String>)],
edition: &str,
started: Instant,
) -> ResultStream {
let rows: Vec<Vec<(&'static str, BoltValue)>> = rows
.iter()
.map(|(name, versions)| {
vec![
("name", BoltValue::String(name.clone())),
(
"versions",
BoltValue::List(versions.iter().cloned().map(BoltValue::String).collect()),
),
("edition", BoltValue::String(edition.to_string())),
]
})
.collect();
server_facts_rows_stream(columns, &rows, started)
}
fn server_facts_rows_stream(
columns: &[&'static str],
rows: &[Vec<(&'static str, BoltValue)>],
started: Instant,
) -> ResultStream {
let records: Vec<BoltRecord> = rows
.iter()
.map(|values| BoltRecord {
values: columns
.iter()
.map(|column| {
values
.iter()
.find(|(name, _)| name == column)
.map(|(_, value)| value.clone())
.unwrap_or(BoltValue::Null)
})
.collect(),
})
.collect();
ResultStream {
metadata: ResultMetadata {
columns: columns.iter().map(|c| (*c).to_string()).collect(),
extra: BoltDict::new(),
},
records,
summary: BoltDict::from([
("type".to_string(), BoltValue::String("r".to_string())),
(
"t_last".to_string(),
BoltValue::Integer(started.elapsed().as_millis() as i64),
),
]),
}
}