fraiseql-core 2.4.0

Core execution engine for FraiseQL v2 - Compiled GraphQL over SQL
Documentation
#![allow(clippy::unwrap_used)] // Reason: benchmark setup code, panics acceptable
#![allow(missing_docs)] // Reason: criterion_group!/criterion_main! macros generate undocumented items

//! SQL Projection Performance Benchmarks
//!
//! This benchmark suite measures the performance of the SQL projection optimization system.
//! Run `cargo bench --bench sql_projection_benchmark` to get hardware-specific numbers.
//! Cross-framework comparisons live in `../velocitybench`.
//!
//! # Metrics Measured
//!
//! 1. **Projection SQL Generation** - Time to generate projection SQL for varying field counts
//! 2. **Result Projection** - Time to project JSONB fields (database-side projection)
//! 3. **`ResultProjector` Operations** - Time for field filtering and __typename addition
//! 4. **Complete Pipeline** - End-to-end latency from raw DB result to GraphQL response
//! 5. **Payload Size** - Bytes transferred with and without projection
//!
//! # Running Benchmarks
//!
//! ```bash
//! # Run all projection benchmarks
//! cargo bench --bench sql_projection_benchmark
//!
//! # Run specific benchmark
//! cargo bench --bench sql_projection_benchmark -- "postgres_projection"
//!
//! # Run with baseline comparison
//! cargo bench --bench sql_projection_benchmark -- --baseline main
//! ```
//!
//! # Design Targets (run the benchmarks above for actual numbers on your hardware)
//!
//! | Operation | Design target |
//! |-----------|---------------|
//! | PostgreSQL projection SQL (5 fields) | ~2µs |
//! | PostgreSQL projection SQL (20 fields) | ~8µs |
//! | Result projection (1K rows) | ~50µs |
//! | __typename addition (1K rows) | ~100µs |
//! | Complete pipeline (100K rows) | ~5ms |

#![allow(clippy::missing_errors_doc)] // Reason: criterion_group! macro generates undocumented items
use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
use fraiseql_core::{
    db::{
        MySqlProjectionGenerator, PostgresProjectionGenerator, SqliteProjectionGenerator,
        types::JsonbValue,
    },
    runtime::ResultProjector,
};
use serde_json::json;

/// Generate sample JSONB data with varying field counts
fn generate_sample_data(field_count: usize, row_count: usize) -> Vec<JsonbValue> {
    let mut rows = Vec::with_capacity(row_count);

    for row_id in 0..row_count {
        let mut obj = serde_json::Map::new();

        // Add requested fields
        for field_id in 0..field_count {
            let key = format!("field_{}", field_id);
            obj.insert(key, json!("value"));
        }

        // Add some extra fields to simulate real data
        obj.insert("id".to_string(), json!(row_id.to_string()));
        obj.insert("name".to_string(), json!(format!("User {}", row_id)));
        obj.insert("email".to_string(), json!(format!("user{}@example.com", row_id)));
        obj.insert("status".to_string(), json!("active"));
        obj.insert("created_at".to_string(), json!("2024-01-14T00:00:00Z"));
        obj.insert("updated_at".to_string(), json!("2024-01-14T00:00:00Z"));
        obj.insert("metadata".to_string(), json!({"score": 100, "tags": ["a", "b", "c"]}));

        rows.push(JsonbValue::new(serde_json::Value::Object(obj)));
    }

    rows
}

/// Generate field list for projection
fn generate_field_list(count: usize) -> Vec<String> {
    (0..count).map(|i| format!("field_{}", i)).collect()
}

// ============================================================================
// BENCHMARK: Projection SQL Generation (Database-side)
// ============================================================================

fn postgres_projection_generation(c: &mut Criterion) {
    let mut group = c.benchmark_group("postgres_projection_generation");

    for field_count in &[5, 10, 20, 50] {
        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_fields", field_count)),
            field_count,
            |b, &field_count| {
                let generator = PostgresProjectionGenerator::new();
                let fields = generate_field_list(field_count);

                b.iter(|| generator.generate_projection_sql(black_box(&fields)).unwrap());
            },
        );
    }

    group.finish();
}

fn mysql_projection_generation(c: &mut Criterion) {
    let mut group = c.benchmark_group("mysql_projection_generation");

    for field_count in &[5, 10, 20, 50] {
        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_fields", field_count)),
            field_count,
            |b, &field_count| {
                let generator = MySqlProjectionGenerator::new();
                let fields = generate_field_list(field_count);

                b.iter(|| generator.generate_projection_sql(black_box(&fields)).unwrap());
            },
        );
    }

    group.finish();
}

fn sqlite_projection_generation(c: &mut Criterion) {
    let mut group = c.benchmark_group("sqlite_projection_generation");

    for field_count in &[5, 10, 20, 50] {
        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_fields", field_count)),
            field_count,
            |b, &field_count| {
                let generator = SqliteProjectionGenerator::new();
                let fields = generate_field_list(field_count);

                b.iter(|| generator.generate_projection_sql(black_box(&fields)).unwrap());
            },
        );
    }

    group.finish();
}

// ============================================================================
// BENCHMARK: Result Projection (Field Filtering via ResultProjector)
// ============================================================================

fn result_projection_field_filtering(c: &mut Criterion) {
    let mut group = c.benchmark_group("result_projection_field_filtering");

    // Test with varying row counts to measure linear scaling
    for row_count in &[10, 100, 1000] {
        let field_count = 5;
        let fields = generate_field_list(field_count);
        let data = generate_sample_data(field_count, *row_count);

        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_rows", row_count)),
            row_count,
            |b, _| {
                let projector = ResultProjector::new(fields.clone());

                b.iter(|| projector.project_results(black_box(&data), true).unwrap());
            },
        );
    }

    group.finish();
}

fn result_projection_large_fieldset(c: &mut Criterion) {
    let mut group = c.benchmark_group("result_projection_large_fieldset");

    // Test with many fields to measure field-count impact
    for field_count in &[10, 20, 50] {
        let row_count = 100;
        let fields = generate_field_list(*field_count);
        let data = generate_sample_data(*field_count, row_count);

        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_fields", field_count)),
            field_count,
            |b, _| {
                let projector = ResultProjector::new(fields.clone());

                b.iter(|| projector.project_results(black_box(&data), true).unwrap());
            },
        );
    }

    group.finish();
}

// ============================================================================
// BENCHMARK: ResultProjector __typename Addition
// ============================================================================

fn add_typename_single_object(c: &mut Criterion) {
    let mut group = c.benchmark_group("add_typename_single_object");

    for field_count in &[5, 20, 50] {
        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_fields", field_count)),
            field_count,
            |b, &field_count| {
                let fields = generate_field_list(field_count);
                let data = generate_sample_data(field_count, 1);
                let projector = ResultProjector::new(fields);

                b.iter(|| projector.add_typename_only(black_box(&data[0]), "User").unwrap());
            },
        );
    }

    group.finish();
}

fn add_typename_array(c: &mut Criterion) {
    let mut group = c.benchmark_group("add_typename_array");

    for row_count in &[10, 100, 1000] {
        let field_count = 10;
        let fields = generate_field_list(field_count);
        let data = generate_sample_data(field_count, *row_count);

        // Combine array results into a single JSONB array
        let array_data = JsonbValue::new(serde_json::Value::Array(
            data.iter().map(|v| v.as_value().clone()).collect(),
        ));

        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_rows", row_count)),
            row_count,
            |b, _| {
                let projector = ResultProjector::new(fields.clone());

                b.iter(|| projector.add_typename_only(black_box(&array_data), "User").unwrap());
            },
        );
    }

    group.finish();
}

// ============================================================================
// BENCHMARK: Complete Pipeline (DB Query → Projection → GraphQL Response)
// ============================================================================

fn complete_pipeline_single_row(c: &mut Criterion) {
    let mut group = c.benchmark_group("complete_pipeline_single_row");

    for field_count in &[5, 10, 20] {
        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_fields", field_count)),
            field_count,
            |b, &field_count| {
                let fields = generate_field_list(field_count);
                let data = generate_sample_data(field_count, 1);
                let projector = ResultProjector::new(fields);

                b.iter(|| {
                    // Step 1: Project fields (not used directly, but part of pipeline)
                    let _projected = projector.project_results(black_box(&data), false).unwrap();

                    // Step 2: Add __typename
                    let with_typename =
                        projector.add_typename_only(black_box(&data[0]), "User").unwrap();

                    // Step 3: Wrap in GraphQL envelope
                    let _response = ResultProjector::wrap_in_data_envelope(with_typename, "user");
                });
            },
        );
    }

    group.finish();
}

fn complete_pipeline_array(c: &mut Criterion) {
    let mut group = c.benchmark_group("complete_pipeline_array");

    for row_count in &[100, 1000, 10000] {
        let field_count = 10;
        let fields = generate_field_list(field_count);
        let data = generate_sample_data(field_count, *row_count);
        let projector = ResultProjector::new(fields.clone());

        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_rows", row_count)),
            row_count,
            |b, _| {
                b.iter(|| {
                    // Step 1: Project fields
                    let projected = projector.project_results(black_box(&data), true).unwrap();

                    // Step 2: Wrap in GraphQL envelope
                    let _response = ResultProjector::wrap_in_data_envelope(projected, "users");
                });
            },
        );
    }

    group.finish();
}

// ============================================================================
// BENCHMARK: Payload Size Comparison
// ============================================================================

fn payload_size_comparison(c: &mut Criterion) {
    let mut group = c.benchmark_group("payload_size_comparison");
    group.sample_size(10); // Smaller sample for data generation benchmarks

    for row_count in &[100, 1000, 10000] {
        let field_count = 10;
        let fields = generate_field_list(field_count);
        let data = generate_sample_data(field_count, *row_count);
        let projector = ResultProjector::new(fields);

        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_rows_unfiltered", row_count)),
            row_count,
            |b, _| {
                b.iter(|| {
                    // Measure size of unfiltered response
                    let response = ResultProjector::wrap_in_data_envelope(
                        serde_json::json!(data.iter().map(|v| v.as_value()).collect::<Vec<_>>()),
                        "users",
                    );
                    let size = serde_json::to_string(&response).unwrap().len();
                    black_box(size)
                });
            },
        );

        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_rows_projected", row_count)),
            row_count,
            |b, _| {
                b.iter(|| {
                    // Measure size of projected response
                    let projected = projector.project_results(black_box(&data), true).unwrap();
                    let response = ResultProjector::wrap_in_data_envelope(projected, "users");
                    let size = serde_json::to_string(&response).unwrap().len();
                    black_box(size)
                });
            },
        );
    }

    group.finish();
}

// ============================================================================
// Criterion Configuration
// ============================================================================

criterion_group!(
    benches,
    postgres_projection_generation,
    mysql_projection_generation,
    sqlite_projection_generation,
    result_projection_field_filtering,
    result_projection_large_fieldset,
    add_typename_single_object,
    add_typename_array,
    complete_pipeline_single_row,
    complete_pipeline_array,
    payload_size_comparison,
);

criterion_main!(benches);