icydb-core 0.199.31

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
//! SQL `DELETE` policy parsing, shape classification, and admission planning.
//! Does not own: public DTO definitions or delete execution.

use super::model::*;
use crate::db::{
    QueryError,
    session::sql::write_policy::{
        SqlGeneratedWritePolicyKind, SqlWriteBoundedPlanProof, SqlWriteExecutionBounds,
        SqlWritePlanCore, SqlWritePrimaryKeyPlanProof, SqlWriteShapePolicyRejection,
        SqlWriteStatementShape, SqlWriteStatementShapeInput, classify_write_statement_shape,
    },
    sql::parser::{SqlDeleteStatement, SqlStatement, parse_sql_with_attribution},
};

/// Classify one SQL statement under an explicit `DELETE` exposure policy.
///
/// This helper parses and inspects statement shape only. It does not execute
/// mutation work or validate field existence beyond the caller-provided primary
/// key context.
pub fn classify_sql_delete_policy(
    sql: &str,
    policy: SqlDeleteExposurePolicy,
    context: SqlDeletePolicyContext<'_>,
) -> Result<SqlDeletePolicyReport, QueryError> {
    let (statement, _) =
        parse_sql_with_attribution(sql).map_err(QueryError::from_sql_parse_error)?;

    Ok(classify_sql_delete_statement_policy(
        &statement, policy, context,
    ))
}

pub(in crate::db) fn classify_sql_delete_statement_policy(
    statement: &SqlStatement,
    policy: SqlDeleteExposurePolicy,
    context: SqlDeletePolicyContext<'_>,
) -> SqlDeletePolicyReport {
    let SqlStatement::Delete(statement) = statement else {
        return SqlDeletePolicyReport::rejected(SqlDeletePolicyRejection::NotDelete);
    };

    let classification = classify_delete_statement(statement, context);
    let rejection = delete_policy_rejection(policy, &classification, context);
    let plan = if rejection.is_none() {
        validated_delete_plan(statement, policy, &classification, context)
    } else {
        None
    };

    SqlDeletePolicyReport {
        classification: Some(classification),
        plan,
        rejection,
    }
}

fn classify_delete_statement(
    statement: &SqlDeleteStatement,
    context: SqlDeletePolicyContext<'_>,
) -> SqlDeleteStatementClassification {
    SqlDeleteStatementClassification {
        target_entity: statement.entity.clone(),
        write_shape: classify_write_shape(statement, context),
    }
}

const fn delete_policy_rejection(
    policy: SqlDeleteExposurePolicy,
    classification: &SqlDeleteStatementClassification,
    context: SqlDeletePolicyContext<'_>,
) -> Option<SqlDeletePolicyRejection> {
    if let Some(rejection) = generated_policy_rejection(policy) {
        return Some(rejection);
    }

    match policy {
        SqlDeleteExposurePolicy::SessionWriteCurrent | SqlDeleteExposurePolicy::AdminBulk => None,
        SqlDeleteExposurePolicy::PublicPrimaryKeyOnly => {
            write_shape_policy_rejection(classification.write_shape.primary_key_policy_rejection())
        }
        SqlDeleteExposurePolicy::PublicBoundedDeterministic => write_shape_policy_rejection(
            classification
                .write_shape
                .bounded_deterministic_policy_rejection(context.write_bounds()),
        ),
        SqlDeleteExposurePolicy::GeneratedQuery | SqlDeleteExposurePolicy::GeneratedDdl => {
            generated_policy_rejection(policy)
        }
    }
}

const fn write_shape_policy_rejection(
    rejection: Option<SqlWriteShapePolicyRejection>,
) -> Option<SqlDeletePolicyRejection> {
    match rejection {
        Some(rejection) => Some(SqlDeletePolicyRejection::from_write_shape_rejection(
            rejection,
        )),
        None => None,
    }
}

fn validated_delete_plan(
    statement: &SqlDeleteStatement,
    policy: SqlDeleteExposurePolicy,
    classification: &SqlDeleteStatementClassification,
    context: SqlDeletePolicyContext<'_>,
) -> Option<SqlValidatedDeletePlan> {
    let execution_bounds = execution_bounds(policy, classification, context)?;
    match policy {
        SqlDeleteExposurePolicy::SessionWriteCurrent => Some(
            SqlValidatedDeletePlan::SessionCurrent(SqlSessionCurrentDeletePlan {
                core: SqlWritePlanCore::from_borrowed(statement, classification, execution_bounds),
            }),
        ),
        SqlDeleteExposurePolicy::PublicPrimaryKeyOnly => Some(
            SqlValidatedDeletePlan::PublicPrimaryKeyOnly(SqlPublicPrimaryKeyDeletePlan {
                core: SqlWritePlanCore::from_borrowed(statement, classification, execution_bounds),
                primary_key_proof: SqlWritePrimaryKeyPlanProof::from_field_names(
                    context.primary_key_fields,
                ),
            }),
        ),
        SqlDeleteExposurePolicy::PublicBoundedDeterministic => Some(
            SqlValidatedDeletePlan::PublicBoundedDeterministic(SqlPublicBoundedDeletePlan {
                core: SqlWritePlanCore::from_borrowed(statement, classification, execution_bounds),
                bounded_proof: SqlWriteBoundedPlanProof::from_admitted_shape(
                    &classification.write_shape,
                    context.primary_key_fields,
                )?,
            }),
        ),
        SqlDeleteExposurePolicy::AdminBulk => {
            Some(SqlValidatedDeletePlan::AdminBulk(SqlAdminBulkDeletePlan {
                core: SqlWritePlanCore::from_borrowed(statement, classification, execution_bounds),
            }))
        }
        SqlDeleteExposurePolicy::GeneratedQuery | SqlDeleteExposurePolicy::GeneratedDdl => None,
    }
}

const fn execution_bounds(
    policy: SqlDeleteExposurePolicy,
    classification: &SqlDeleteStatementClassification,
    context: SqlDeletePolicyContext<'_>,
) -> Option<SqlWriteExecutionBounds> {
    classification
        .write_shape
        .execution_bounds_for_exposure_class(policy.exposure_class(), context.write_bounds())
}

const fn generated_policy_rejection(
    policy: SqlDeleteExposurePolicy,
) -> Option<SqlDeletePolicyRejection> {
    match policy.exposure_class().generated_policy_kind() {
        Some(SqlGeneratedWritePolicyKind::Query) => {
            Some(SqlDeletePolicyRejection::GeneratedQueryRejectsDelete)
        }
        Some(SqlGeneratedWritePolicyKind::Ddl) => {
            Some(SqlDeletePolicyRejection::GeneratedDdlRejectsDelete)
        }
        None => None,
    }
}

fn classify_write_shape(
    statement: &SqlDeleteStatement,
    context: SqlDeletePolicyContext<'_>,
) -> SqlWriteStatementShape {
    classify_write_statement_shape(SqlWriteStatementShapeInput {
        predicate: statement.predicate.as_ref(),
        entity: statement.entity.as_str(),
        table_alias: statement.table_alias.as_deref(),
        order_by: statement.order_by.as_slice(),
        limit: statement.limit,
        offset: statement.offset,
        returning: statement.returning.as_ref(),
        primary_key_fields: context.primary_key_fields,
    })
}