// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

//! See `main.rs` for how to run it.

use arrow::array::{ArrayRef, Int32Array, RecordBatch, StringArray};
use datafusion::common::Result;
use datafusion::common::config::ConfigOptions;
use datafusion::common::tree_node::{Transformed, TreeNode};
use datafusion::logical_expr::{Expr, LogicalPlan, LogicalPlanBuilder, col, lit};
use datafusion::optimizer::analyzer::AnalyzerRule;
use datafusion::prelude::SessionContext;
use std::sync::{Arc, Mutex};

/// This example demonstrates how to add your own [`AnalyzerRule`] to
/// DataFusion.
///
/// [`AnalyzerRule`]s transform [`LogicalPlan`]s prior to the DataFusion
/// optimization process, and can be used to change the plan's semantics (e.g.
/// output types).
///
/// This example shows an `AnalyzerRule` which implements a simplistic of row
/// level access control scheme by introducing a filter to the query.
///
/// See [optimizer_rule.rs] for an example of a optimizer rule
pub async fn analyzer_rule() -> Result<()> {
    // AnalyzerRules run before OptimizerRules.
    //
    // DataFusion includes several built in AnalyzerRules for tasks such as type
    // coercion which change the types of expressions in the plan. Add our new
    // rule to the context to run it during the analysis phase.
    let rule = Arc::new(RowLevelAccessControl::new());
    let ctx = SessionContext::new();
    ctx.add_analyzer_rule(Arc::clone(&rule) as _);

    ctx.register_batch("employee", employee_batch())?;

    // Now, planning any SQL statement also invokes the AnalyzerRule
    let plan = ctx
        .sql("SELECT * FROM employee")
        .await?
        .into_optimized_plan()?;

    // Printing the query plan shows a filter has been added
    //
    // Filter: employee.position = Utf8("Engineer")
    //   TableScan: employee projection=[name, age, position]
    println!("Logical Plan:\n\n{}\n", plan.display_indent());

    // Execute the query, and indeed no Manager's are returned
    //
    // +-----------+-----+----------+
    // | name      | age | position |
    // +-----------+-----+----------+
    // | Andy      | 11  | Engineer |
    // | Oleks     | 33  | Engineer |
    // | Xiangpeng | 55  | Engineer |
    // +-----------+-----+----------+
    ctx.sql("SELECT * FROM employee").await?.show().await?;

    // We can now change the access level to "Manager" and see the results
    //
    // +----------+-----+----------+
    // | name     | age | position |
    // +----------+-----+----------+
    // | Andrew   | 22  | Manager  |
    // | Chunchun | 44  | Manager  |
    // +----------+-----+----------+
    rule.set_show_position("Manager");
    ctx.sql("SELECT * FROM employee").await?.show().await?;

    // The filters introduced by our AnalyzerRule are treated the same as any
    // other filter by the DataFusion optimizer, including predicate push down
    // (including into scans), simplifications, and similar optimizations.
    //
    // For example adding another predicate to the query
    let plan = ctx
        .sql("SELECT * FROM employee WHERE age > 30")
        .await?
        .into_optimized_plan()?;

    // We can see the DataFusion Optimizer has combined the filters together
    // when we print out the plan
    //
    // Filter: employee.age > Int32(30) AND employee.position = Utf8("Manager")
    //   TableScan: employee projection=[name, age, position]
    println!("Logical Plan:\n\n{}\n", plan.display_indent());

    Ok(())
}

/// Example AnalyzerRule that implements a very basic "row level access
/// control"
///
/// In this case, it adds a filter to the plan that removes all managers from
/// the result set.
#[derive(Debug)]
struct RowLevelAccessControl {
    /// Models the current access level of the session
    ///
    /// This is value of the position column which should be included in the
    /// result set. It is wrapped in a `Mutex` so we can change it during query
    show_position: Mutex<String>,
}

impl RowLevelAccessControl {
    fn new() -> Self {
        Self {
            show_position: Mutex::new("Engineer".to_string()),
        }
    }

    /// return the current position to show, as an expression
    fn show_position(&self) -> Expr {
        lit(self.show_position.lock().unwrap().clone())
    }

    /// specifies a different position to show in the result set
    fn set_show_position(&self, access_level: impl Into<String>) {
        *self.show_position.lock().unwrap() = access_level.into();
    }
}

impl AnalyzerRule for RowLevelAccessControl {
    fn analyze(&self, plan: LogicalPlan, _config: &ConfigOptions) -> Result<LogicalPlan> {
        // use the TreeNode API to recursively walk the LogicalPlan tree
        // and all of its children (inputs)
        let transformed_plan = plan.transform(|plan| {
            // This closure is called for each LogicalPlan node
            // if it is a Scan node, add a filter to remove all managers
            if is_employee_table_scan(&plan) {
                // Use the LogicalPlanBuilder to add a filter to the plan
                let filter = LogicalPlanBuilder::from(plan)
                    // Filter Expression: position = <access level>
                    .filter(col("position").eq(self.show_position()))?
                    .build()?;

                // `Transformed::yes` signals the plan was changed
                Ok(Transformed::yes(filter))
            } else {
                // `Transformed::no`
                // signals the plan was not changed
                Ok(Transformed::no(plan))
            }
        })?;

        // the result of calling transform is a `Transformed` structure which
        // contains
        //
        // 1. a flag signaling if any rewrite took place
        // 2. a flag if the recursion stopped early
        // 3. The actual transformed data (a LogicalPlan in this case)
        //
        // This example does not need the value of either flag, so simply
        // extract the LogicalPlan "data"
        Ok(transformed_plan.data)
    }

    fn name(&self) -> &str {
        "table_access"
    }
}

fn is_employee_table_scan(plan: &LogicalPlan) -> bool {
    if let LogicalPlan::TableScan(scan) = plan {
        scan.table_name.table() == "employee"
    } else {
        false
    }
}

/// Return a RecordBatch with made up data about fictional employees
fn employee_batch() -> RecordBatch {
    let name: ArrayRef = Arc::new(StringArray::from_iter_values([
        "Andy",
        "Andrew",
        "Oleks",
        "Chunchun",
        "Xiangpeng",
    ]));
    let age: ArrayRef = Arc::new(Int32Array::from(vec![11, 22, 33, 44, 55]));
    let position = Arc::new(StringArray::from_iter_values([
        "Engineer", "Manager", "Engineer", "Manager", "Engineer",
    ]));
    RecordBatch::try_from_iter(vec![("name", name), ("age", age), ("position", position)])
        .unwrap()
}
