hamelin_legacy 0.4.2

Legacy AST translation code for Hamelin (to be deprecated)
Documentation
use hamelin_lib::{
    antlr::hamelinparser::{ExplodeCommandContext, ExplodeCommandContextAttrs},
    err::{TranslationError, TranslationErrors},
    sql::{
        expression::{
            identifier::{CompoundIdentifier, Identifier, SimpleIdentifier},
            literal::{BooleanLiteral, ColumnReference},
            Leaf, SQLExpression,
        },
        projection_builder::ProjectionBuilder,
        query::{
            Join, JoinClause, JoinType, SQLQuery, SubQuery, TableAlias, TableExpression,
            TableFunctionApply,
        },
    },
    types::Type,
};

use crate::ast::{assignment_clause::HamelinAssignmentClause, pipeline::HamelinPipeline};
use crate::env::Environment;
use crate::translation::PendingQuery;

pub fn translate(
    ctx: &ExplodeCommandContext<'static>,
    pipeline: &HamelinPipeline,
    previous_command: &PendingQuery,
) -> Result<PendingQuery, TranslationErrors> {
    let assignment_ctx = TranslationErrors::expect(ctx, ctx.assignmentClause())?;
    let (identifier, translation) = HamelinAssignmentClause::new(
        assignment_ctx.clone(),
        pipeline
            .context
            .default_expression_translation_context(&previous_command.env),
    )
    .to_sql()?;

    let left_alias = SimpleIdentifier::new("_left");
    let right_alias = SimpleIdentifier::new("_right");
    let right_nested_alias = SimpleIdentifier::new("_nested");

    let left_sql = previous_command.query.clone();
    let left_query = SubQuery::new(left_sql.clone().into()).alias(left_alias.clone().into());

    match translation.typ {
        Type::Array(array) => {
            /*
            This is the alias for the right hand side of the join. We have to be careful in how
            we construct this. If the element type is struct, the database will automatically
            "lift" each subfield into a named field in the result set. If the element type is
            not a struct, we have to give that inner field a name, so that we can then refer to
            it later. We just pick "_nested" so we can find it later, when building the projections.
            */
            let table_alias = match array.element_type.as_ref() {
                Type::Struct(_) => TableAlias::new(right_alias.clone().into()),
                _ => TableAlias::new(right_alias.clone().into())
                    .with_field_alias(right_nested_alias.clone()),
            };

            let unnest = TableFunctionApply::new("unnest".to_string())
                .with_argument(translation.sql.clone())
                .with_alias(table_alias.into());

            let from: TableExpression = if left_sql == SQLQuery::default() {
                unnest.into()
            } else {
                Join::new(left_query.into())
                    .with_clause(JoinClause {
                        table: unnest.into(),
                        join_type: JoinType::LEFT,
                        condition: Some(BooleanLiteral::new(true).into()),
                    })
                    .into()
            };

            let mut pb = ProjectionBuilder::default();

            // Check if this is a canonical explode (identifier already exists in schema)
            // Use lookup_local to properly handle compound identifiers like data.arr
            let is_canonical = previous_command.env.lookup_local(&identifier).is_some();

            // Build the exploded column expression
            let (exploded_expr, exploded_type): (SQLExpression, Type) =
                match array.element_type.as_ref() {
                    Type::Struct(srct) => {
                        // Generate the cast expression that represents the inner struct type.
                        let cast: SQLExpression =
                            ProjectionBuilder::deep_initialize_from_struct(None, srct.clone())
                                .build_cast()
                                .map_err(|e| TranslationError::wrap_box(ctx, e.into()))?
                                .into();

                        // Prefix any inner references with the "_right" alias.
                        let cast_with_prefix = cast.map_leaves(|leaf| match leaf {
                            Leaf::ColumnReference(column_reference) => ColumnReference::new(
                                column_reference
                                    .identifier
                                    .prefix_from_simples(&[right_alias.clone()])
                                    .into(),
                            )
                            .into(),
                            l => l.into(),
                        });

                        (cast_with_prefix, srct.clone().into())
                    }
                    t => {
                        // No fancy business needed for the non-struct case. Just column ref _right.
                        let right_nested_identifier: Identifier = right_nested_alias.clone().into();
                        let cr = ColumnReference::new(
                            right_nested_identifier
                                .prefix_from_simples(&[right_alias])
                                .into(),
                        );

                        (cr.into(), t.clone())
                    }
                };

            // For compound identifiers (like data.arr or str.exploded), we need to handle
            // the parent struct specially: initialize it as a Node with column references
            // so we can modify a field inside while preserving other fields.
            let target_root: Option<SimpleIdentifier> = match &identifier {
                Identifier::Compound(c) => Some(c.first().clone()),
                Identifier::Simple(_) => None,
            };

            // Helper to bind a field from the environment, handling the case where
            // it's the parent struct of our compound target identifier
            let bind_env_field = |pb: &mut ProjectionBuilder, k: &SimpleIdentifier, v: &Type| {
                let ident: Identifier = k.clone().into();
                if target_root.as_ref() == Some(k) {
                    // This is the parent struct of our compound target.
                    // Initialize it as a Node with _left-prefixed column references
                    // so we can later bind into it without losing existing fields.
                    if let Type::Struct(struct_type) = v {
                        let prefixed_name: Identifier = CompoundIdentifier::from_idents(&[
                            left_alias.clone().into(),
                            k.clone().into(),
                        ])
                        .into();
                        let initialized = ProjectionBuilder::deep_initialize_from_struct(
                            Some(prefixed_name),
                            struct_type.clone(),
                        );
                        for (nested_k, nested_v) in initialized.build_hamelin_type().fields.iter() {
                            let nested_ident: Identifier = CompoundIdentifier::from_idents(&[
                                k.clone().into(),
                                nested_k.clone().into(),
                            ])
                            .into();
                            let nested_cr = ColumnReference::new(
                                CompoundIdentifier::from_idents(&[
                                    left_alias.clone().into(),
                                    k.clone().into(),
                                    nested_k.clone().into(),
                                ])
                                .into(),
                            );
                            pb.bind(nested_ident, nested_cr.into(), nested_v.clone());
                        }
                    }
                } else {
                    // Regular field: bind with _left prefix
                    let cr = ColumnReference::new(
                        ident.prefix_from_simples(&[left_alias.clone()]).into(),
                    );
                    pb.bind(ident, cr.into(), v.clone());
                }
            };

            if is_canonical {
                // Canonical: bind old fields first (preserving order), then overwrite in place
                for (k, v) in previous_command.env.fields.fields.iter() {
                    bind_env_field(&mut pb, k, v);
                }
                pb.bind(identifier, exploded_expr, exploded_type);
            } else {
                // Non-canonical: new fields are prepended in Hamelin
                // First bind the new exploded field
                pb.bind(identifier.clone(), exploded_expr, exploded_type);

                // For compound identifiers, bind remaining fields from the parent struct
                // AFTER the new field so they come after in the struct
                if let Some(ref root) = target_root {
                    if let Some(Type::Struct(struct_type)) =
                        previous_command.env.fields.fields.get(root)
                    {
                        for (nested_k, nested_v) in struct_type.fields.iter() {
                            let nested_ident: Identifier = CompoundIdentifier::from_idents(&[
                                root.clone().into(),
                                nested_k.clone().into(),
                            ])
                            .into();
                            // Skip if this is the field we just bound (canonical-like case within struct)
                            if nested_ident == identifier {
                                continue;
                            }
                            let nested_cr = ColumnReference::new(
                                CompoundIdentifier::from_idents(&[
                                    left_alias.clone().into(),
                                    root.clone().into(),
                                    nested_k.clone().into(),
                                ])
                                .into(),
                            );
                            pb.bind(nested_ident, nested_cr.into(), nested_v.clone());
                        }
                    }
                }

                // Then bind the rest of the environment fields
                for (k, v) in previous_command.env.fields.fields.iter() {
                    // Skip the target root - we already handled its fields above
                    if target_root.as_ref() == Some(k) {
                        continue;
                    }
                    bind_env_field(&mut pb, k, v);
                }
            }

            let projections = pb
                .clone()
                .build_projections()
                .map_err(|e| TranslationError::wrap_box(ctx, e.into()))?;

            let query = SQLQuery::default().from(from).select(projections);
            let merged_env = Environment::new(pb.build_hamelin_type());

            Ok(PendingQuery::new(query, merged_env))
        }
        t => {
            return Err(TranslationError::msg(
                assignment_ctx.as_ref(),
                &format!("Unsupported type {} for explode command. Must be array.", t),
            )
            .single())
        }
    }
}