radixdb-executor 1.1.0

SQL binding, planning, and execution engine for RadixDB
Documentation
use std::sync::Arc;

use radixdb_catalog::{CatalogObject, ObjectId, ObjectKind};
use radixdb_core::Value;
use radixdb_procedural::{PrincipalContext, ProceduralResult, RuntimeValue};
use radixdb_storage::mvcc::engine::MVCCEngine;

use crate::{ExecutionContext, Executor};

use super::ProceduralResultStage;

#[derive(Default)]
struct TestStage;

impl ProceduralResultStage for TestStage {
    fn begin(&mut self) -> ProceduralResult<()> {
        Ok(())
    }

    fn stage_row(&mut self, _row: Vec<RuntimeValue>) -> ProceduralResult<()> {
        Ok(())
    }

    fn publish(&mut self) {}

    fn discard(&mut self) {}
}

fn executor() -> Executor {
    let engine = MVCCEngine::in_memory();
    engine.open_engine().unwrap();
    Executor::new(Arc::new(engine))
}

fn routine(executor: &Executor, name: &str) -> Option<CatalogObject> {
    executor
        .engine
        .pin_catalog()
        .unwrap()
        .find_routine(
            ObjectId::BOOTSTRAP_NAMESPACE,
            ObjectKind::Procedure,
            name,
            &[],
        )
        .unwrap()
        .cloned()
}

fn principals() -> PrincipalContext {
    PrincipalContext {
        session_principal: ObjectId::BOOTSTRAP_OWNER,
        invoker_principal: ObjectId::BOOTSTRAP_OWNER,
        effective_principal: ObjectId::BOOTSTRAP_OWNER,
    }
}

fn output(executor: &Executor, caller: &str) -> Vec<RuntimeValue> {
    executor
        .execute_procedure(
            routine(executor, caller).unwrap().id(),
            Vec::new(),
            &ExecutionContext::new(),
            principals(),
            &mut TestStage,
        )
        .unwrap()
        .execution()
        .output_values
        .clone()
}

#[test]
fn nested_call_resolves_named_arguments_and_left_to_right_defaults() {
    let executor = executor();
    executor
        .execute(
            "CREATE PROCEDURE add_default( \
                 IN first_value INTEGER NOT NULL, \
                 IN second_value INTEGER NOT NULL DEFAULT first_value + 1, \
                 OUT output_value INTEGER NOT NULL \
             ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
                 output_value := first_value + second_value; \
             END;",
        )
        .unwrap();
    executor
        .execute(
            "CREATE PROCEDURE named_default_caller(OUT output_value INTEGER NOT NULL) \
             LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
                 CALL add_default(first_value => 10, output_value => output_value); \
             END;",
        )
        .unwrap();
    assert_eq!(
        output(&executor, "named_default_caller"),
        vec![RuntimeValue::scalar(Value::Integer(21))]
    );
}

#[test]
fn routine_default_cannot_reference_a_later_argument() {
    let executor = executor();
    let error = match executor.execute(
        "CREATE PROCEDURE invalid_default( \
             IN first_value INTEGER DEFAULT later_value, \
             IN later_value INTEGER DEFAULT 1 \
         ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN RETURN; END;",
    ) {
        Ok(_) => panic!("forward argument reference unexpectedly passed DDL admission"),
        Err(error) => error,
    };
    assert!(error.to_string().contains("procedural compilation failed"));
    assert!(routine(&executor, "invalid_default").is_none());
}

#[test]
fn procedure_overload_prefers_exact_match_over_lossless_conversion() {
    let executor = executor();
    executor
        .execute(
            "CREATE PROCEDURE choose_numeric( \
                 IN input_value DECIMAL NOT NULL, OUT output_value INTEGER NOT NULL \
             ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN output_value := 2; END;",
        )
        .unwrap();
    executor
        .execute(
            "CREATE PROCEDURE choose_numeric( \
                 IN input_value INTEGER NOT NULL, OUT output_value INTEGER NOT NULL \
             ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN output_value := 1; END;",
        )
        .unwrap();
    executor
        .execute(
            "CREATE PROCEDURE exact_numeric_caller(OUT output_value INTEGER NOT NULL) \
             LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
                 CALL choose_numeric(7, output_value); \
             END;",
        )
        .unwrap();
    assert_eq!(
        output(&executor, "exact_numeric_caller"),
        vec![RuntimeValue::scalar(Value::Integer(1))]
    );
}

#[test]
fn procedure_call_applies_checked_lossless_conversions() {
    let executor = executor();
    executor
        .execute(
            "CREATE PROCEDURE decimal_only( \
                 IN input_value DECIMAL NOT NULL, OUT output_value INTEGER NOT NULL \
             ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN output_value := 7; END;",
        )
        .unwrap();
    executor
        .execute(
            "CREATE PROCEDURE timestamp_only( \
                 IN input_value TIMESTAMP NOT NULL, OUT output_value INTEGER NOT NULL \
             ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN output_value := 8; END;",
        )
        .unwrap();
    executor
        .execute(
            "CREATE PROCEDURE widening_caller(OUT output_value INTEGER NOT NULL) \
             LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
                 CALL decimal_only(11, output_value); \
             END;",
        )
        .unwrap();
    executor
        .execute(
            "CREATE PROCEDURE date_widening_caller(OUT output_value INTEGER NOT NULL) \
             LANGUAGE RADIX SECURITY INVOKER AS \
             DECLARE input_value DATE NOT NULL := DATE '2026-09-07'; \
             BEGIN CALL timestamp_only(input_value, output_value); END;",
        )
        .unwrap();
    assert_eq!(
        output(&executor, "widening_caller"),
        vec![RuntimeValue::scalar(Value::Integer(7))]
    );
    assert_eq!(
        output(&executor, "date_widening_caller"),
        vec![RuntimeValue::scalar(Value::Integer(8))]
    );
}

#[test]
fn procedure_call_rejects_non_lossless_implicit_conversion() {
    let executor = executor();
    executor
        .execute(
            "CREATE PROCEDURE float_only(IN input_value FLOAT) \
             LANGUAGE RADIX SECURITY INVOKER AS BEGIN RETURN; END;",
        )
        .unwrap();
    let error = match executor.execute(
        "CREATE PROCEDURE invalid_float_caller() \
         LANGUAGE RADIX SECURITY INVOKER AS BEGIN CALL float_only(1); END;",
    ) {
        Ok(_) => panic!("INTEGER to FLOAT call unexpectedly passed DDL admission"),
        Err(error) => error,
    };
    assert!(error.to_string().contains("procedural compilation failed"));
}

#[test]
fn equal_cost_procedure_overloads_are_ambiguous() {
    let executor = executor();
    for ddl in [
        "CREATE PROCEDURE equal_cost(IN left_value DECIMAL, IN right_value INTEGER) \
         LANGUAGE RADIX SECURITY INVOKER AS BEGIN RETURN; END;",
        "CREATE PROCEDURE equal_cost(IN left_value INTEGER, IN right_value DECIMAL) \
         LANGUAGE RADIX SECURITY INVOKER AS BEGIN RETURN; END;",
    ] {
        executor.execute(ddl).unwrap();
    }
    let error = match executor.execute(
        "CREATE PROCEDURE ambiguous_cost_caller() \
         LANGUAGE RADIX SECURITY INVOKER AS BEGIN CALL equal_cost(1, 2); END;",
    ) {
        Ok(_) => panic!("equal-cost overload call unexpectedly passed DDL admission"),
        Err(error) => error,
    };
    assert!(error.to_string().contains("procedural compilation failed"));
    assert!(routine(&executor, "ambiguous_cost_caller").is_none());
}

#[test]
fn untyped_null_is_ambiguous_but_typed_null_selects_an_overload() {
    let executor = executor();
    for ddl in [
        "CREATE PROCEDURE nullable_choice( \
             IN input_value INTEGER, OUT output_value INTEGER NOT NULL \
         ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN output_value := 1; END;",
        "CREATE PROCEDURE nullable_choice( \
             IN input_value TEXT, OUT output_value INTEGER NOT NULL \
         ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN output_value := 2; END;",
    ] {
        executor.execute(ddl).unwrap();
    }
    let error = match executor.execute(
        "CREATE PROCEDURE ambiguous_null_caller(OUT output_value INTEGER NOT NULL) \
         LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
             CALL nullable_choice(NULL, output_value); \
         END;",
    ) {
        Ok(_) => panic!("untyped NULL overload call unexpectedly passed DDL admission"),
        Err(error) => error,
    };
    assert!(error.to_string().contains("procedural compilation failed"));

    executor
        .execute(
            "CREATE PROCEDURE typed_null_caller(OUT output_value INTEGER NOT NULL) \
             LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
                 CALL nullable_choice(CAST(NULL AS INTEGER), output_value); \
             END;",
        )
        .unwrap();
    assert_eq!(
        output(&executor, "typed_null_caller"),
        vec![RuntimeValue::scalar(Value::Integer(1))]
    );
}

#[test]
fn outer_call_resolves_named_defaults_and_returns_out_values() {
    let executor = executor();
    executor
        .execute(
            "CREATE PROCEDURE add_default( \
                 IN first_value INTEGER NOT NULL, \
                 IN second_value INTEGER NOT NULL DEFAULT first_value + 1, \
                 OUT output_value INTEGER NOT NULL \
             ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
                 output_value := first_value + second_value; \
             END;",
        )
        .unwrap();

    let mut result = executor
        .execute("CALL add_default(first_value => 10)")
        .unwrap();
    assert_eq!(result.columns(), &["output_value"]);
    assert!(result.next());
    assert_eq!(result.row()[0], Value::Integer(21));
    assert!(!result.next());
    assert!(result.last_error().is_none());
}

#[test]
fn outer_call_prefers_exact_overload_and_dynamic_call_shares_transaction() {
    let executor = executor();
    executor
        .execute("CREATE TABLE call_effects (value INTEGER PRIMARY KEY)")
        .unwrap();
    executor
        .execute(
            "CREATE PROCEDURE write_choice( \
                 IN input_value DECIMAL NOT NULL, OUT output_value INTEGER NOT NULL \
             ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN output_value := 2; END;",
        )
        .unwrap();
    executor
        .execute(
            "CREATE PROCEDURE write_choice( \
                 IN input_value INTEGER NOT NULL, OUT output_value INTEGER NOT NULL \
             ) LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
                 INSERT INTO call_effects VALUES (:input_value); output_value := 1; \
             END;",
        )
        .unwrap();

    let mut exact = executor.execute("CALL write_choice(7)").unwrap();
    assert!(exact.next());
    assert_eq!(exact.row()[0], Value::Integer(1));
    assert!(!exact.next());

    executor
        .execute(
            "CREATE PROCEDURE dynamic_caller(input_value INTEGER NOT NULL) \
             LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
                 EXECUTE 'CALL write_choice(?)' USING input_value; \
             END;",
        )
        .unwrap();
    executor.execute("CALL dynamic_caller(8)").unwrap();

    let mut count = executor
        .execute("SELECT COUNT(*) FROM call_effects")
        .unwrap();
    assert!(count.next());
    assert_eq!(count.row()[0], Value::Integer(2));
}

#[test]
fn outer_call_rolls_back_argument_effects_when_procedure_fails() {
    let executor = executor();
    executor
        .execute("CREATE TABLE call_argument_effects (value INTEGER PRIMARY KEY)")
        .unwrap();
    executor
        .execute(
            "CREATE FUNCTION write_argument(input_value INTEGER NOT NULL) \
             RETURNS INTEGER NOT NULL LANGUAGE RADIX VOLATILE SECURITY INVOKER AS \
             BEGIN INSERT INTO call_argument_effects VALUES (:input_value); \
             RETURN input_value; END;",
        )
        .unwrap();
    executor
        .execute(
            "CREATE PROCEDURE duplicate_argument(input_value INTEGER NOT NULL) \
             LANGUAGE RADIX SECURITY INVOKER AS BEGIN \
             INSERT INTO call_argument_effects VALUES (:input_value); END;",
        )
        .unwrap();

    let error = match executor.execute("CALL duplicate_argument(write_argument(5))") {
        Ok(_) => panic!("failing CALL unexpectedly succeeded"),
        Err(error) => error,
    };
    assert!(!error.to_string().is_empty());

    let mut count = executor
        .execute("SELECT COUNT(*) FROM call_argument_effects")
        .unwrap();
    assert!(count.next());
    assert_eq!(count.row()[0], Value::Integer(0));
}