AstParameterBinder.php

PHP

Path: src/Parsing/SQL/AstParameterBinder.php

<?php

namespace mini\Parsing\SQL;

use mini\Parsing\SQL\AST\{
    ASTNode,
    PlaceholderNode,
    LiteralNode,
    BinaryOperation,
    UnaryOperation,
    InOperation,
    IsNullOperation,
    LikeOperation,
    BetweenOperation,
    ExistsOperation,
    SubqueryNode,
    FunctionCallNode,
    SelectStatement,
    InsertStatement,
    UpdateStatement,
    DeleteStatement,
    ColumnNode,
    JoinNode
};

/**
 * Binds parameters to AST by replacing placeholders with literal values
 *
 * This creates a new AST with all PlaceholderNode instances replaced
 * by LiteralNode instances containing the actual parameter values.
 *
 * Usage:
 * ```php
 * $binder = new AstParameterBinder(['Alice', 30]);
 * $boundAst = $binder->bind($ast);
 * // Now PlaceholderNode('?') are replaced with LiteralNode values
 * ```
 */
class AstParameterBinder
{
    private array $params;
    private int $positionalIndex = 0;

    public function __construct(array $params)
    {
        $this->params = $params;
    }

    /**
     * Bind parameters to AST
     *
     * @param ASTNode $node Root AST node
     * @return ASTNode New AST with placeholders replaced
     */
    public function bind(ASTNode $node): ASTNode
    {
        $this->positionalIndex = 0;
        return $this->bindNode($node);
    }

    private function bindNode(ASTNode $node): ASTNode
    {
        // Replace PlaceholderNode with LiteralNode
        if ($node instanceof PlaceholderNode) {
            return $this->bindPlaceholder($node);
        }

        // Recursively bind child nodes
        if ($node instanceof BinaryOperation) {
            $new = clone $node;
            $new->left = $this->bindNode($node->left);
            $new->right = $this->bindNode($node->right);
            return $new;
        }

        if ($node instanceof UnaryOperation) {
            $new = clone $node;
            $new->expression = $this->bindNode($node->expression);
            return $new;
        }

        if ($node instanceof InOperation) {
            $new = clone $node;
            $new->left = $this->bindNode($node->left);
            if ($node->isSubquery()) {
                // Bind params inside subquery (params flow through)
                $new->values = $this->bindNode($node->values);
            } else {
                $new->values = array_map(fn($v) => $this->bindNode($v), $node->values);
            }
            return $new;
        }

        if ($node instanceof ExistsOperation) {
            $new = clone $node;
            $new->subquery = $this->bindNode($node->subquery);
            return $new;
        }

        if ($node instanceof IsNullOperation) {
            $new = clone $node;
            $new->expression = $this->bindNode($node->expression);
            return $new;
        }

        if ($node instanceof LikeOperation) {
            $new = clone $node;
            $new->left = $this->bindNode($node->left);
            $new->pattern = $this->bindNode($node->pattern);
            return $new;
        }

        if ($node instanceof BetweenOperation) {
            $new = clone $node;
            $new->expression = $this->bindNode($node->expression);
            $new->low = $this->bindNode($node->low);
            $new->high = $this->bindNode($node->high);
            return $new;
        }

        // SubqueryNode - bind params inside the subquery
        if ($node instanceof SubqueryNode) {
            $new = clone $node;
            $new->query = $this->bindNode($node->query);
            return $new;
        }

        if ($node instanceof FunctionCallNode) {
            $new = clone $node;
            $new->arguments = array_map(fn($arg) => $this->bindNode($arg), $node->arguments);
            return $new;
        }

        if ($node instanceof SelectStatement) {
            $new = clone $node;

            // Bind FROM (may be a subquery/derived table)
            if ($node->from instanceof ASTNode) {
                $new->from = $this->bindNode($node->from);
            }

            // Bind columns (may contain placeholders in expressions)
            $new->columns = array_map(fn($col) => $this->bindNode($col), $node->columns);

            // Bind JOINs
            $new->joins = array_map(fn($join) => $this->bindNode($join), $node->joins);

            if ($node->where) {
                $new->where = $this->bindNode($node->where);
            }

            // Bind GROUP BY expressions
            if ($node->groupBy) {
                $new->groupBy = array_map(fn($expr) => $this->bindNode($expr), $node->groupBy);
            }

            // Bind HAVING
            if ($node->having) {
                $new->having = $this->bindNode($node->having);
            }

            // Bind ORDER BY expressions
            if ($node->orderBy) {
                $new->orderBy = array_map(function ($item) {
                    return [
                        'column' => $this->bindNode($item['column']),
                        'direction' => $item['direction']
                    ];
                }, $node->orderBy);
            }

            // Bind LIMIT (may be placeholder)
            if ($node->limit) {
                $new->limit = $this->bindNode($node->limit);
            }

            // Bind OFFSET (may be placeholder)
            if ($node->offset) {
                $new->offset = $this->bindNode($node->offset);
            }

            return $new;
        }

        if ($node instanceof JoinNode) {
            $new = clone $node;
            if ($node->condition) {
                $new->condition = $this->bindNode($node->condition);
            }
            return $new;
        }

        if ($node instanceof ColumnNode) {
            $new = clone $node;
            $new->expression = $this->bindNode($node->expression);
            return $new;
        }

        if ($node instanceof InsertStatement) {
            $new = clone $node;
            // Bind each row of values
            $new->values = array_map(function ($row) {
                return array_map(fn($v) => $this->bindNode($v), $row);
            }, $node->values);
            return $new;
        }

        if ($node instanceof UpdateStatement) {
            $new = clone $node;
            // Bind SET values
            $new->updates = array_map(function ($update) {
                return [
                    'column' => $update['column'],
                    'value' => $this->bindNode($update['value'])
                ];
            }, $node->updates);
            // Bind WHERE
            if ($node->where) {
                $new->where = $this->bindNode($node->where);
            }
            return $new;
        }

        if ($node instanceof DeleteStatement) {
            $new = clone $node;
            if ($node->where) {
                $new->where = $this->bindNode($node->where);
            }
            return $new;
        }

        // Return unchanged for other node types (identifiers, literals)
        return $node;
    }

    private function bindPlaceholder(PlaceholderNode $node): LiteralNode
    {
        $value = null;

        if ($node->token === '?') {
            // Positional placeholder
            $value = $this->params[$this->positionalIndex++] ?? null;
        } else {
            // Named placeholder (:name)
            $name = ltrim($node->token, ':');
            $value = $this->params[$name] ?? null;
        }

        // Create LiteralNode with appropriate type
        if ($value === null) {
            return new LiteralNode(null, 'null');
        }

        if (is_int($value) || is_float($value)) {
            return new LiteralNode((string)$value, 'number');
        }

        // String - need to represent as quoted string
        return new LiteralNode($value, 'string');
    }
}