jetro-core 0.5.12

jetro-core: parser, compiler, and VM for the Jetro JSON query language
Documentation
use super::report::{
    InspectContext, InspectLevel, InspectOptions, InspectionSummary, LogicalInspection,
    QueryInspection,
};
use crate::io::{NdjsonOptions, NdjsonSourceMode};
use crate::parse::ast::Expr;
use crate::plan::physical::PlanningContext;
use crate::{JetroEngine, JetroEngineError};

pub(crate) fn inspect_query(
    engine: &JetroEngine,
    query: &str,
    options: InspectOptions,
    ndjson_options: NdjsonOptions,
) -> Result<QueryInspection, JetroEngineError> {
    let plan = engine.cached_plan(query, planning_context(options.context));
    let (summary, physical) = super::physical::inspect_physical_plan(&plan);
    let logical = (options.level != InspectLevel::Summary).then(|| inspect_logical(query));
    let physical =
        matches!(options.level, InspectLevel::Plan | InspectLevel::Detailed).then_some(physical);
    let pipeline = (options.level == InspectLevel::Detailed)
        .then(|| super::pipeline::inspect_first_pipeline(&plan))
        .flatten();
    let ndjson = match options.context {
        InspectContext::NdjsonReader if options.level == InspectLevel::Detailed => {
            Some(super::ndjson::inspect_ndjson_query(
                engine,
                query,
                NdjsonSourceMode::Reader,
                ndjson_options,
            )?)
        }
        InspectContext::NdjsonFile if options.level == InspectLevel::Detailed => {
            Some(super::ndjson::inspect_ndjson_query(
                engine,
                query,
                NdjsonSourceMode::File,
                ndjson_options,
            )?)
        }
        _ => None,
    };

    Ok(QueryInspection {
        query: query.to_string(),
        context: options.context,
        level: options.level,
        summary: summary_with_context(summary, options.context),
        logical,
        physical,
        pipeline,
        ndjson,
        warnings: Vec::new(),
    })
}

fn planning_context(context: InspectContext) -> PlanningContext {
    match context {
        InspectContext::Value => PlanningContext::val(),
        InspectContext::Bytes | InspectContext::NdjsonReader | InspectContext::NdjsonFile => {
            PlanningContext::bytes()
        }
    }
}

fn summary_with_context(
    mut summary: InspectionSummary,
    context: InspectContext,
) -> InspectionSummary {
    if matches!(
        context,
        InspectContext::NdjsonReader | InspectContext::NdjsonFile
    ) {
        summary.materializes_root = false;
    }
    summary
}

fn inspect_logical(query: &str) -> LogicalInspection {
    match crate::parse::parser::parse(query) {
        Ok(expr) => LogicalInspection {
            ast_root: expr_label(&expr).to_string(),
            root_shape: root_shape(&expr),
            notes: Vec::new(),
        },
        Err(err) => LogicalInspection {
            ast_root: "parse-error".to_string(),
            root_shape: "source-vm".to_string(),
            notes: vec![err.to_string()],
        },
    }
}

fn expr_label(expr: &Expr) -> &'static str {
    match expr {
        Expr::Null => "null",
        Expr::Bool(_) => "bool",
        Expr::Int(_) => "int",
        Expr::Float(_) => "float",
        Expr::Str(_) => "string",
        Expr::FString(_) => "f-string",
        Expr::Root => "root",
        Expr::Current => "current",
        Expr::Ident(_) => "ident",
        Expr::Chain(_, _) => "chain",
        Expr::BinOp(_, _, _) => "binary",
        Expr::UnaryNeg(_) => "unary-neg",
        Expr::Not(_) => "not",
        Expr::Kind { .. } => "kind",
        Expr::Coalesce(_, _) => "coalesce",
        Expr::Object(_) => "object",
        Expr::Array(_) => "array",
        Expr::Pipeline { .. } => "pipeline",
        Expr::ListComp { .. } => "list-comp",
        Expr::DictComp { .. } => "dict-comp",
        Expr::SetComp { .. } => "set-comp",
        Expr::GenComp { .. } => "gen-comp",
        Expr::Lambda { .. } => "lambda",
        Expr::Let { .. } => "let",
        Expr::IfElse { .. } => "if-else",
        Expr::Try { .. } => "try",
        Expr::GlobalCall { .. } => "global-call",
        Expr::Cast { .. } => "cast",
        Expr::Patch { .. } => "patch",
        Expr::UpdateBatch { .. } => "update-batch",
        Expr::DeleteMark => "delete-mark",
        Expr::Match { .. } => "match",
    }
}

fn root_shape(expr: &Expr) -> String {
    match expr {
        Expr::Chain(base, steps) => format!("{}+{} steps", expr_label(base), steps.len()),
        Expr::Object(fields) => format!("object({} fields)", fields.len()),
        Expr::Array(items) => format!("array({} items)", items.len()),
        other => expr_label(other).to_string(),
    }
}

#[cfg(test)]
mod tests {
    use crate::introspect::{InspectContext, InspectLevel, InspectOptions};

    #[test]
    fn inspect_query_assembles_static_plan_report() {
        let engine = crate::JetroEngine::new();
        let report = engine
            .inspect_query(
                "$.books.filter(price > 10).map(title)",
                InspectOptions::detailed(InspectContext::Bytes),
            )
            .expect("inspection");

        assert_eq!(report.level, InspectLevel::Detailed);
        assert!(report.physical.is_some());
        assert!(report.pipeline.is_some());
        assert!(report.ndjson.is_none());
    }

    #[test]
    fn public_ndjson_inspection_reports_rows_route() {
        let engine = crate::JetroEngine::new();
        let report = engine
            .inspect_ndjson_query_with_options(
                r#"$.rows().reverse().find(@.custom_attributes.find(@.value == "z"))"#,
                crate::io::NdjsonSourceMode::File,
                crate::io::NdjsonOptions::default(),
                InspectLevel::Detailed,
            )
            .expect("inspection");

        assert!(report.physical.is_some());
        assert_eq!(
            report
                .ndjson
                .as_ref()
                .map(|ndjson| ndjson.route_kind.as_str()),
            Some("rows-stream")
        );
        assert!(report.format_tree().contains("ordered-partition-search"));
    }
}