hamelin_sql 0.3.10

SQL generation utilities for Hamelin query language
Documentation
//! Translation implementations for string operators and functions

use crate::utils::{
    direct_function_translation, hamelin_string_index_to_sql_with_negatives,
    string_negative_index_to_positive, to_sql_concat,
};
use crate::TranslationRegistry;
use hamelin_lib::func::defs::{
    CidrContains, Contains, EndsWith, IsIpv4, IsIpv6, Lower, Replace2, Replace3, StartsWith,
    StringConcat, StringLen, Substr2, Substr3, Upper,
};
use hamelin_lib::sql::expression::apply::{BinaryOperatorApply, FunctionCallApply};
use hamelin_lib::sql::expression::literal::StringLiteral;
use hamelin_lib::sql::expression::operator::Operator;
use hamelin_lib::sql::expression::{Cast, TryCast};
use hamelin_lib::sql::types::{SQLBaseType, SQLType};

/// Register all string operator and function translations.
pub fn register(registry: &mut TranslationRegistry) {
    // StringConcat: string + string -> SQL CONCAT
    registry.register::<StringConcat>(to_sql_concat);

    // replace(string, pattern) - pass through
    registry.register::<Replace2>(direct_function_translation);

    // replace(string, pattern, replacement) - pass through
    registry.register::<Replace3>(direct_function_translation);

    // substr(string, start) - convert 0-based Hamelin index to 1-based SQL
    registry.register::<Substr2>(|_, mut bindings| {
        let string = bindings.take()?;
        let start = bindings.take()?;
        // Convert 0-based Hamelin index to 1-based SQL index
        let sql_start = hamelin_string_index_to_sql_with_negatives(start.sql, string.sql.clone());
        Ok(FunctionCallApply::with_two("substr", string.sql, sql_start).into())
    });

    // substr(string, start, end) - convert indices
    registry.register::<Substr3>(|_, mut bindings| {
        let string = bindings.take()?;
        let start = bindings.take()?;
        let end = bindings.take()?;

        // Convert negative indices to positive in Hamelin space
        let positive_end = string_negative_index_to_positive(end.sql.clone(), string.sql.clone());
        let positive_start =
            string_negative_index_to_positive(start.sql.clone(), string.sql.clone());

        // Calculate length as positive_end - positive_start (both in Hamelin zero-based space)
        let length = BinaryOperatorApply::new(Operator::Minus, positive_end, positive_start).into();

        // Convert start to SQL space: add 1 for 1-based indexing
        let sql_start = hamelin_string_index_to_sql_with_negatives(start.sql, string.sql.clone());

        Ok(FunctionCallApply::with_three("substr", string.sql, sql_start, length).into())
    });

    // starts_with(string, prefix) - pass through
    registry.register::<StartsWith>(direct_function_translation);

    // ends_with(string, suffix) - translate to LIKE '%suffix'
    registry.register::<EndsWith>(|_, mut bindings| {
        let string = bindings.take()?;
        let suffix = bindings.take()?;
        Ok(BinaryOperatorApply::new(
            Operator::Like,
            string.sql,
            BinaryOperatorApply::new(Operator::Concat, StringLiteral::new("%").into(), suffix.sql)
                .into(),
        )
        .into())
    });

    // contains(string, substring) - translate to LIKE '%substring%'
    registry.register::<Contains>(|_, mut bindings| {
        let string = bindings.take()?;
        let substring = bindings.take()?;
        Ok(BinaryOperatorApply::new(
            Operator::Like,
            string.sql,
            BinaryOperatorApply::new(
                Operator::Concat,
                StringLiteral::new("%").into(),
                BinaryOperatorApply::new(
                    Operator::Concat,
                    substring.sql,
                    StringLiteral::new("%").into(),
                )
                .into(),
            )
            .into(),
        )
        .into())
    });

    // cidr_contains(cidr, ip) - translate to try(contains(cidr, try_cast(ip as ipaddress)))
    registry.register::<CidrContains>(|_, mut bindings| {
        let cidr = bindings.take()?;
        let ip = bindings.take()?;

        Ok(FunctionCallApply::with_one(
            "try",
            FunctionCallApply::with_two(
                "contains",
                cidr.sql,
                TryCast::new(ip.sql, SQLType::SQLBaseType(SQLBaseType::IpAddress)).into(),
            )
            .into(),
        )
        .into())
    });

    // is_ipv4(ip) - cast to ipaddress then to varchar and check for '.'
    registry.register::<IsIpv4>(|_, mut bindings| {
        let ip = bindings.take()?;

        Ok(BinaryOperatorApply::new(
            Operator::Like,
            Cast::new(
                TryCast::new(ip.sql, SQLType::SQLBaseType(SQLBaseType::IpAddress)).into(),
                SQLType::SQLBaseType(SQLBaseType::VarChar),
            )
            .into(),
            StringLiteral::new("%.%").into(),
        )
        .into())
    });

    // is_ipv6(ip) - cast to ipaddress then to varchar and check for ':'
    registry.register::<IsIpv6>(|_, mut bindings| {
        let ip = bindings.take()?;

        Ok(BinaryOperatorApply::new(
            Operator::Like,
            Cast::new(
                TryCast::new(ip.sql, SQLType::SQLBaseType(SQLBaseType::IpAddress)).into(),
                SQLType::SQLBaseType(SQLBaseType::VarChar),
            )
            .into(),
            StringLiteral::new("%:%").into(),
        )
        .into())
    });

    // lower(string) - pass through
    registry.register::<Lower>(direct_function_translation);

    // upper(string) - pass through
    registry.register::<Upper>(direct_function_translation);

    // len(string) - translate to length()
    registry.register::<StringLen>(|_, mut bindings| {
        let x = bindings.take()?;
        Ok(FunctionCallApply::with_one("length", x.sql).into())
    });
}