All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.googlecode.paradox.parser.SQLParser Maven / Gradle / Ivy

/*
 * Copyright (c) 2009 Leonardo Alves da Costa
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
 * License for more details. You should have received a copy of the GNU General Public License along with this
 * program. If not, see .
 */
package com.googlecode.paradox.parser;

import com.googlecode.paradox.exceptions.ParadoxSyntaxErrorException;
import com.googlecode.paradox.exceptions.SyntaxError;
import com.googlecode.paradox.function.FunctionFactory;
import com.googlecode.paradox.function.date.ExtractFunction;
import com.googlecode.paradox.function.general.CastFunction;
import com.googlecode.paradox.function.general.ConvertFunction;
import com.googlecode.paradox.function.string.PositionFunction;
import com.googlecode.paradox.function.string.SubstringFunction;
import com.googlecode.paradox.function.string.TrimFunction;
import com.googlecode.paradox.parser.nodes.*;
import com.googlecode.paradox.planner.nodes.FieldNode;
import com.googlecode.paradox.planner.nodes.FunctionNode;
import com.googlecode.paradox.planner.nodes.ParameterNode;
import com.googlecode.paradox.planner.nodes.ValueNode;
import com.googlecode.paradox.planner.nodes.comparable.*;
import com.googlecode.paradox.planner.nodes.join.ANDNode;
import com.googlecode.paradox.planner.nodes.join.ORNode;
import com.googlecode.paradox.planner.sorting.OrderType;
import com.googlecode.paradox.results.ParadoxType;

import java.sql.SQLException;

/**
 * Parses a SQL statement.
 *
 * @since 1.0
 */
@SuppressWarnings("java:S1448")
public final class SQLParser {

    /**
     * The scanner used to read tokens.
     */
    private final Scanner scanner;

    /**
     * The current token.
     */
    private Token token;

    /**
     * Parameter count.
     */
    private int parameterCount;

    /**
     * Creates a new instance.
     *
     * @param sql the SQL to parse.
     * @throws SQLException in case of parse errors.
     */
    public SQLParser(final String sql) throws SQLException {
        this.scanner = new Scanner(sql);
    }

    /**
     * Parses the function name alias.
     *
     * @param functionName the function name.
     * @param position     the current scanner position.
     * @return the function node with alias set.
     * @throws SQLException in case of failures.
     */
    private static FunctionNode parseFunctionAlias(final String functionName, final ScannerPosition position)
            throws SQLException {
        final FunctionNode functionNode = new FunctionNode(functionName, position);
        functionNode.validate(position);
        return functionNode;
    }

    /**
     * Parses the SQL statement.
     *
     * @return a statement node.
     * @throws SQLException in case of parse errors.
     */
    public StatementNode parse() throws SQLException {
        if (!this.scanner.hasNext()) {
            throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_END_OF_STATEMENT);
        }

        this.token = this.scanner.nextToken();

        StatementNode statementNode;
        if (isToken(TokenType.SELECT)) {
            statementNode = this.parseSelect();
        } else {
            throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_TOKEN, token.getPosition());
        }

        statementNode.setParameterCount(parameterCount);
        return statementNode;
    }

    /**
     * Test for expected tokens.
     *
     * @param token the token to validate.
     * @throws SQLException in case of unexpected tokens.
     */
    private void expect(final TokenType token) throws SQLException {
        if (this.token == null) {
            throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_END_OF_STATEMENT);
        } else if (this.token.getType() != token) {
            throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_TOKEN, getPosition());
        }

        if (this.scanner.hasNext()) {
            this.token = this.scanner.nextToken();
        } else {
            this.token = null;
        }
    }

    /**
     * Test for expected COMMA if {@code enabled} is true.
     *
     * @param enabled true if the token can be checked.
     * @throws SQLException in case of unexpected tokens.
     */
    private void expectComma(final boolean enabled) throws SQLException {
        if (enabled) {
            expect(TokenType.COMMA);
        }
    }

    /**
     * Parse the asterisk token.
     *
     * @param tableName the table name.
     * @return the asterisk node.
     * @throws SQLException in case of parse errors.
     */
    private AsteriskNode parseAsterisk(final String tableName) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.ASTERISK);
        return new AsteriskNode(tableName, position);
    }

    /**
     * Parses between token.
     *
     * @param field the between field.
     * @return the between node.
     * @throws SQLException in case of parse errors.
     */
    private BetweenNode parseBetween(final FieldNode field) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.BETWEEN);
        final FieldNode left = this.parseField();
        this.expect(TokenType.AND);
        final FieldNode right = this.parseField();
        return new BetweenNode(field, left, right, position);
    }

    /**
     * Parse the character token.
     *
     * @param fieldName the field name.
     * @return the node value.
     * @throws SQLException in case of parse errors.
     */
    private ValueNode parseCharacter(final String fieldName) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.CHARACTER);

        return new ValueNode(fieldName, position, ParadoxType.VARCHAR);
    }

    private String getFieldAlias(String fieldName) throws SQLException {
        String fieldAlias = fieldName;

        if (isToken(TokenType.AS)) {
            // Field alias (with AS identifier)
            this.expect(TokenType.AS);
            fieldAlias = this.token.getValue();
            this.expect(TokenType.IDENTIFIER);
        } else if (isToken(TokenType.IDENTIFIER)) {
            // Field alias (without AS identifier)
            fieldAlias = this.token.getValue();
            this.expect(TokenType.IDENTIFIER);
        }

        return fieldAlias;
    }

    /**
     * Parses the conditional statements.
     *
     * @return the node.
     * @throws SQLException in case of parse errors.
     */
    private AbstractConditionalNode parseCondition() throws SQLException {
        AbstractConditionalNode ret = null;
        while (this.scanner.hasNext() && !this.token.isConditionBreak() && !this.token.isSelectBreak()) {
            ret = parseSubCondition(ret);
        }

        return ret;
    }

    /**
     * Parses the conditional statements.
     *
     * @return the node.
     * @throws SQLException in case of parse errors.
     */
    private AbstractConditionalNode parseSubCondition(final AbstractConditionalNode parent) throws SQLException {
        AbstractConditionalNode ret = parent;

        if (!this.token.isConditionBreak() && !this.token.isSelectBreak()) {
            if (ret != null && this.token.isOperator()) {
                // Not in first expression.
                ret = this.parseOperators(ret);
            } else if (isToken(TokenType.L_PAREN)) {
                this.expect(TokenType.L_PAREN);
                while (!isToken(TokenType.R_PAREN)) {
                    ret = parseSubCondition(ret);
                }

                this.expect(TokenType.R_PAREN);
            } else if (isToken(TokenType.NOT)) {
                // Token type NOT.
                final ScannerPosition position = this.token.getPosition();
                this.expect(TokenType.NOT);
                final NotNode node = new NotNode(position);
                node.addChild(parseSubCondition(null));

                if (parent == null) {
                    ret = node;
                } else {
                    parent.addChild(node);
                }
            } else {
                ret = this.parseFieldNode();
            }
        }

        return ret;
    }

    /**
     * Parses the equals tokens.
     *
     * @param field the left field token.
     * @return the EQUALS node.
     * @throws SQLException in case of parse errors.
     */
    private EqualsNode parseEquals(final FieldNode field) throws SQLException {
        final ScannerPosition position = this.token.getPosition();
        this.expect(TokenType.EQUALS);
        final FieldNode value = this.parseField();
        return new EqualsNode(field, value, position);
    }

    /**
     * Parses the table join fields.
     *
     * @return the field node.
     * @throws SQLException in case of errors.
     */
    private FieldNode parseField() throws SQLException {
        String fieldName = this.token.getValue();
        final ScannerPosition position = getPosition();

        FieldNode ret;
        switch (this.token.getType()) {
            case CHARACTER:
                // Found a String value.
                ret = parseCharacter(fieldName);
                break;
            case NUMERIC:
                // Found a numeric value.
                ret = parseNumeric(fieldName);
                break;
            case NULL:
                ret = parseNull();
                break;
            case TRUE:
                ret = parseTrue(fieldName);
                break;
            case FALSE:
                ret = parseFalse(fieldName);
                break;
            case QUESTION_MARK:
                ret = parseParameter();
                break;
            default:
                // Found a field.
                ret = getFieldNode(fieldName, position);
                break;
        }

        return ret;
    }

    private FieldNode getFieldNode(final String fieldName, final ScannerPosition position) throws SQLException {
        String tableName = null;
        String name = fieldName;
        this.expect(TokenType.IDENTIFIER);

        // If it has a Table Name
        if (isToken(TokenType.PERIOD)) {
            this.expect(TokenType.PERIOD);
            tableName = name;
            name = this.token.getValue();
            this.expect(TokenType.IDENTIFIER);
        } else if (isToken(TokenType.L_PAREN)) {
            // function
            final FunctionNode node = parseFunction(fieldName, position, false);
            if (node.isGrouping()) {
                throw new ParadoxSyntaxErrorException(SyntaxError.INVALID_AGGREGATE_FUNCTION, position, node.getName());
            }

            return node;
        }

        return new FieldNode(tableName, name, position);
    }

    /**
     * Parses the field node.
     *
     * @return the field node.
     * @throws SQLException in case of parse errors.
     */
    private AbstractConditionalNode parseFieldNode() throws SQLException {
        final FieldNode firstField = this.parseField();
        AbstractConditionalNode node;

        switch (this.token.getType()) {
            case BETWEEN:
                node = this.parseBetween(firstField);
                break;
            case EQUALS:
                node = this.parseEquals(firstField);
                break;
            case NOT_EQUALS:
                node = this.parseNotEquals(firstField);
                break;
            case LESS:
                node = this.parseLess(firstField);
                break;
            case MORE:
                node = this.parseMore(firstField);
                break;
            case IN:
                node = this.parseIn(firstField);
                break;
            case IS:
                node = this.parseNull(firstField);
                break;
            case LIKE:
                node = this.parseLike(firstField);
                break;
            case ILIKE:
                node = this.parseILike(firstField);
                break;
            case NOT:
                node = this.parseNot(firstField);
                break;
            default:
                throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_TOKEN,
                        this.token.getPosition());
        }

        return node;
    }

    /**
     * Parse the field list in SELECT statement.
     *
     * @param select the select node.
     * @throws SQLException in case of parse errors.
     */
    private void parseFields(final SelectNode select) throws SQLException {
        boolean firstField = true;
        do {
            expectComma(!firstField);
            firstField = false;

            // Field Name
            final String fieldName = this.token.getValue();

            SQLNode node;
            switch (this.token.getType()) {
                case CHARACTER:
                    node = this.parseCharacter(fieldName);
                    break;
                case NUMERIC:
                    node = this.parseNumeric(fieldName);
                    break;
                case NULL:
                    node = this.parseNull();
                    break;
                case TRUE:
                    node = this.parseTrue(fieldName);
                    break;
                case FALSE:
                    node = this.parseFalse(fieldName);
                    break;
                case ASTERISK:
                    node = this.parseAsterisk(null);
                    break;
                case QUESTION_MARK:
                    node = parseParameter();
                    break;
                default:
                    node = this.parseIdentifier(fieldName);
                    break;
            }

            node.setAlias(getFieldAlias(node.getAlias()));
            select.addField(node);
        } while (this.scanner.hasNext() && !isToken(TokenType.FROM));
    }

    /**
     * Parse table field names from WHERE.
     *
     * @param oldAlias the old alias name.
     * @return the new alias.
     * @throws SQLException in case of errors.
     */
    private String parseFields(final String oldAlias) throws SQLException {
        String tableAlias = oldAlias;

        if (isToken(TokenType.IDENTIFIER) || isToken(TokenType.AS)) {
            // Field alias (with AS identifier)
            testAndRemoveTokenType(TokenType.AS);

            // Field alias (without AS identifier)
            tableAlias = this.token.getValue();
            this.expect(TokenType.IDENTIFIER);
        }

        return tableAlias;
    }

    /**
     * Parse the FROM keyword.
     *
     * @param select the select node.
     * @throws SQLException in case of parse errors.
     */
    private void parseFrom(final SelectNode select) throws SQLException {
        ScannerPosition position = getPosition();
        this.expect(TokenType.FROM);
        boolean firstField = true;
        while (this.token != null && !this.token.isSelectBreak()) {
            expectComma(!firstField);
            firstField = false;

            this.parseJoinTable(select);
        }

        if (select.getTables().isEmpty()) {
            if (this.token != null) {
                position = getPosition();
            } else {
                addOffset(position, TokenType.FROM.name().length());
            }

            throw new ParadoxSyntaxErrorException(SyntaxError.EMPTY_TABLE_LIST, position);
        }
    }

    /**
     * Parse the identifier token associated with a field.
     *
     * @param fieldName the field name.
     * @return the field node.
     * @throws SQLException in case of parse errors.
     */
    private SQLNode parseIdentifier(final String fieldName) throws SQLException {
        String newTableName = null;
        String newFieldName = fieldName;

        final ScannerPosition position = getPosition();

        // Just change to next token because some functions have clash names with
        // reserved words.
        if (this.scanner.hasNext()) {
            this.token = this.scanner.nextToken();
        } else {
            this.token = null;
        }

        if (isToken(TokenType.L_PAREN)) {
            // function
            return parseFunction(fieldName, position, true);
        } else if (isToken(TokenType.PERIOD)) {
            this.expect(TokenType.PERIOD);
            newTableName = fieldName;
            newFieldName = this.token.getValue();

            if (isToken(TokenType.ASTERISK)) {
                return parseAsterisk(newTableName);
            }

            this.expect(TokenType.IDENTIFIER);
        } else if (FunctionFactory.isFunctionAlias(fieldName)) {
            // A field without table alias can be a function alias.
            return parseFunctionAlias(fieldName, position);
        }

        return new FieldNode(newTableName, newFieldName, position);
    }

    /**
     * Handles function specific separators (only for separators).
     *
     * @param functionName the function name to identify it.
     * @param node         the function node.
     * @return true if the separator was handled.
     * @throws SQLException in case of failures.
     */
    private boolean isFunctionSpecific(final String functionName, final FunctionNode node) throws SQLException {
        boolean ret = false;
        if (functionName.equalsIgnoreCase(PositionFunction.NAME)) {
            // POSITION(a in b).
            this.expect(TokenType.IN);
            ret = true;
        } else if (functionName.equalsIgnoreCase(ExtractFunction.NAME)) {
            // EXTRACT(a FROM b).
            this.expect(TokenType.FROM);
            ret = true;
        } else if (functionName.equalsIgnoreCase(TrimFunction.NAME) && this.token != null) {
            parseTrimFunction(node);

            // Do nothing, no separator here. TRIM(TYPE...
            ret = true;
        } else if (functionName.equalsIgnoreCase(SubstringFunction.NAME)) {
            // SUBSTRING(VALUE FROM start FOR length).

            if (isToken(TokenType.COMMA)) {
                this.expect(TokenType.COMMA);
            } else if (node.getParameters().size() == 1) {
                this.expect(TokenType.FROM);
            } else {
                this.expect(TokenType.FOR);
            }

            ret = true;
        } else if (functionName.equalsIgnoreCase(ConvertFunction.NAME)) {
            if (isToken(TokenType.USING)) {
                node.getParameters().add(
                        new ValueNode(this.token.getValue(), this.token.getPosition(), ParadoxType.VARCHAR));
                this.expect(TokenType.USING);
            } else {
                this.expect(TokenType.COMMA);
            }

            ret = true;
        } else if (functionName.equalsIgnoreCase(CastFunction.NAME)) {
            this.expect(TokenType.AS);
            ret = true;
        }

        return ret;
    }

    /**
     * Parses TRIM function.
     *
     * @param node the function node.
     * @throws SQLException in case of failures.
     */
    private void parseTrimFunction(FunctionNode node) throws SQLException {
        if (TrimFunction.isValidType(node.getParameters().get(0).getName())) {
            // TRIM(TYPE...
            if (node.getParameters().size() == 0x02) {
                // TRIM(TYPE 'CHARS'...
                this.expect(TokenType.FROM);
            } else if (node.getParameters().size() != 1) {
                // TRIM(TYPE 'CHARS' FROM 'X'...
                throw new ParadoxSyntaxErrorException(SyntaxError.INVALID_PARAMETER_VALUE,
                        this.token.getPosition(), this.token.getValue());
            }
        } else if (node.getParameters().size() == 1) {
            // TRIM('CHARS' ...).
            this.expect(TokenType.FROM);
        } else {
            // TRIM('CHARS' FROM 'TEXT).
            throw new ParadoxSyntaxErrorException(SyntaxError.INVALID_PARAMETER_VALUE, this.token.getPosition(),
                    this.token.getValue());
        }
    }

    /**
     * Parses a function node.
     *
     * @param functionName    the function name.
     * @param position        the current scanner position.
     * @param enableAggregate if this field can have an aggregate function.
     * @return the function node.
     * @throws SQLException in case of failures.
     */
    private FunctionNode parseFunction(final String functionName, final ScannerPosition position, final boolean enableAggregate) throws SQLException {
        final FunctionNode functionNode = new FunctionNode(functionName, position);
        this.expect(TokenType.L_PAREN);

        boolean first = true;
        while (!isToken(TokenType.R_PAREN)) {
            // Is a function specific separator?
            if (!first && !isFunctionSpecific(functionName, functionNode)) {
                this.expect(TokenType.COMMA);
            }

            first = false;

            switch (this.token.getType()) {
                case CHARACTER:
                    functionNode.addParameter(this.parseCharacter(this.token.getValue()));
                    break;
                case NUMERIC:
                    functionNode.addParameter(this.parseNumeric(this.token.getValue()));
                    break;
                case NULL:
                    functionNode.addParameter(this.parseNull());
                    break;
                case TRUE:
                    functionNode.addParameter(this.parseTrue(this.token.getValue()));
                    break;
                case FALSE:
                    functionNode.addParameter(this.parseFalse(this.token.getValue()));
                    break;
                case ASTERISK:
                    functionNode.addParameter(this.parseAsterisk(null));
                    break;
                case QUESTION_MARK:
                    functionNode.addParameter(this.parseParameter());
                    break;
                default:
                    functionNode.addParameter(this.parseIdentifierFieldFunction(this.token.getValue(),
                            enableAggregate));
                    break;
            }
        }

        final ScannerPosition endPosition = getPosition();
        this.expect(TokenType.R_PAREN);

        functionNode.validate(endPosition);
        return functionNode;
    }

    private ParameterNode parseParameter() throws SQLException {
        final ScannerPosition position = this.token.getPosition();
        this.expect(TokenType.QUESTION_MARK);
        final ParameterNode node = new ParameterNode(parameterCount, position);
        parameterCount++;
        return node;
    }

    private FieldNode parseIdentifierFieldFunction(final String fieldName, final boolean enableAggregate)
            throws SQLException {
        String newTableName = null;
        String newFieldName = fieldName;

        final ScannerPosition position = getPosition();
        this.expect(TokenType.IDENTIFIER);

        if (isToken(TokenType.L_PAREN)) {
            // function
            final FunctionNode node = parseFunction(fieldName, position, enableAggregate);
            if (node.isGrouping() && !enableAggregate) {
                throw new ParadoxSyntaxErrorException(SyntaxError.INVALID_AGGREGATE_FUNCTION, position, node.getName());
            }

            return node;
        } else if (isToken(TokenType.PERIOD)) {
            // If it has a Table Name.
            this.expect(TokenType.PERIOD);
            newTableName = fieldName;
            newFieldName = this.token.getValue();

            this.expect(TokenType.IDENTIFIER);
        } else if (FunctionFactory.isFunctionAlias(fieldName)) {
            // A field without table alias can be a function alias.
            return parseFunctionAlias(fieldName, position);
        }

        return new FieldNode(newTableName, newFieldName, position);
    }

    /**
     * Parses the join tokens.
     *
     * @param select the select node.
     * @throws SQLException in case of errors.
     */
    private void parseJoin(final SelectNode select) throws SQLException {
        while (this.scanner.hasNext() && !isToken(TokenType.COMMA) && !this.token.isSelectBreak()) {
            // Inner, right or cross join.
            final JoinType joinType = getJoinType();
            this.expect(TokenType.JOIN);

            String schemaName = null;
            String tableName = this.token.getValue();
            this.expect(TokenType.IDENTIFIER);

            // Have schema name.
            if (isToken(TokenType.PERIOD)) {
                expect(TokenType.PERIOD);
                schemaName = tableName;
                tableName = this.token.getValue();
                this.expect(TokenType.IDENTIFIER);
            }

            final String tableAlias = this.parseFields(tableName);

            final JoinNode joinTable = new JoinNode(schemaName, tableName, tableAlias, joinType, null);
            if (joinType != JoinType.CROSS) {
                // Cross join don't have join clause.
                this.expect(TokenType.ON);
                joinTable.setCondition(this.parseCondition());
            }

            select.addTable(joinTable);
        }
    }

    /**
     * Parses the join type.
     *
     * @return the join type.
     * @throws SQLException in case of failures.
     */
    private JoinType getJoinType() throws SQLException {
        JoinType joinType = JoinType.INNER;
        switch (this.token.getType()) {
            case FULL:
                joinType = JoinType.FULL;
                this.expect(TokenType.FULL);
                testAndRemoveTokenType(TokenType.OUTER);
                break;
            case LEFT:
                joinType = JoinType.LEFT;
                this.expect(TokenType.LEFT);
                testAndRemoveTokenType(TokenType.OUTER);
                break;
            case RIGHT:
                joinType = JoinType.RIGHT;
                this.expect(TokenType.RIGHT);
                testAndRemoveTokenType(TokenType.OUTER);
                break;
            case CROSS:
                joinType = JoinType.CROSS;
                this.expect(TokenType.CROSS);
                break;
            case INNER:
                this.expect(TokenType.INNER);
                break;
            default:
                // Nothing to do here.
        }

        return joinType;
    }

    /**
     * Test for a desired token and remove it if is the right token.
     *
     * @param token the token to test.
     * @throws SQLException in case of failures.
     */
    private void testAndRemoveTokenType(final TokenType token) throws SQLException {
        if (isToken(token)) {
            this.expect(token);
        }
    }

    /**
     * Parse the tables name after a from keyword.
     *
     * @param select the select node.
     * @throws SQLException in case of parse errors.
     */
    private void parseJoinTable(final SelectNode select) throws SQLException {
        String schemaName = null;
        String tableName = this.token.getValue();
        final ScannerPosition position = getPosition();
        this.expect(TokenType.IDENTIFIER);

        // Have schema name.
        if (isToken(TokenType.PERIOD)) {
            expect(TokenType.PERIOD);
            schemaName = tableName;
            tableName = this.token.getValue();
            this.expect(TokenType.IDENTIFIER);
        }

        final String tableAlias = this.parseFields(tableName);

        final TableNode table = new TableNode(schemaName, tableName, tableAlias, position);
        select.addTable(table);

        // Parse possible table joins.
        this.parseJoin(select);
    }

    /**
     * Parses less token.
     *
     * @param firstField the left token field.
     * @return the less token.
     * @throws SQLException in case of parse errors.
     */
    private AbstractComparableNode parseLess(final FieldNode firstField) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.LESS);

        if (isToken(TokenType.EQUALS)) {
            this.expect(TokenType.EQUALS);
            return new LessThanOrEqualsNode(firstField, this.parseField(), position);
        }

        return new LessThanNode(firstField, parseField(), position);
    }

    /**
     * Parses more token.
     *
     * @param firstField the left more token field.
     * @return the grater than node.
     * @throws SQLException in case of parse errors.
     */
    private AbstractComparableNode parseMore(final FieldNode firstField) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.MORE);

        if (isToken(TokenType.EQUALS)) {
            this.expect(TokenType.EQUALS);
            return new GreaterThanOrEqualsNode(firstField, this.parseField(), position);
        }

        return new GreaterThanNode(firstField, this.parseField(), position);
    }

    private InNode parseIn(final FieldNode firstField) throws SQLException {
        final ScannerPosition position = this.token.getPosition();
        this.expect(TokenType.IN);
        this.expect(TokenType.L_PAREN);

        final InNode in = new InNode(firstField, position);

        boolean first = true;
        do {
            expectComma(!first);
            first = false;

            if (isToken(TokenType.NUMERIC)) {
                in.addField(new ValueNode(token.getValue(), token.getPosition(), ParadoxType.NUMBER));
                this.expect(TokenType.NUMERIC);
            } else if (isToken(TokenType.CHARACTER)) {
                in.addField(new ValueNode(token.getValue(), token.getPosition(), ParadoxType.VARCHAR));
                this.expect(TokenType.CHARACTER);
            } else {
                throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_TOKEN, getPosition());
            }

        } while (!isToken(TokenType.R_PAREN));

        this.expect(TokenType.R_PAREN);

        return in;
    }

    /**
     * Parses null conditional token.
     *
     * @param firstField the left more token field.
     * @return the null than node.
     * @throws SQLException in case of parse errors.
     */
    private AbstractComparableNode parseNull(final FieldNode firstField) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.IS);
        AbstractComparableNode ret;
        if (isToken(TokenType.NOT)) {
            this.expect(TokenType.NOT);
            ret = new IsNotNullNode(firstField, position);
        } else {
            ret = new IsNullNode(firstField, position);
        }

        this.expect(TokenType.NULL);
        return ret;
    }

    /**
     * Parses not conditional.
     *
     * @param firstField the left more token field.
     * @return the null than node.
     * @throws SQLException in case of parse errors.
     */
    private NotNode parseNot(final FieldNode firstField) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.NOT);

        final NotNode not = new NotNode(position);
        if (isToken(TokenType.LIKE)) {
            not.addChild(this.parseLike(firstField));
        } else if (isToken(TokenType.ILIKE)) {
            not.addChild(this.parseILike(firstField));
        } else if (isToken(TokenType.IN)) {
            not.addChild(this.parseIn(firstField));
        } else {
            throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_TOKEN, getPosition());
        }

        return not;
    }

    /**
     * Parses like conditional.
     *
     * @param firstField the left more token field.
     * @return the null than node.
     * @throws SQLException in case of parse errors.
     */
    private LikeNode parseLike(final FieldNode firstField) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.LIKE);

        final LikeNode like = new LikeNode(firstField, parseField(), position);
        parseEscapeToken(like);
        return like;
    }

    /**
     * Parses insensitive like conditional.
     *
     * @param firstField the left more token field.
     * @return the null than node.
     * @throws SQLException in case of parse errors.
     */
    private ILikeNode parseILike(final FieldNode firstField) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.ILIKE);

        final ILikeNode iLikeNode = new ILikeNode(firstField, parseField(), position);
        parseEscapeToken(iLikeNode);
        return iLikeNode;
    }

    /**
     * Check and parses the ESCAPE node in like.
     *
     * @param likeNode the like node.
     * @throws SQLException in case of syntax errors.
     */
    private void parseEscapeToken(final LikeNode likeNode) throws SQLException {
        // Has an escape value?
        if (isToken(TokenType.ESCAPE)) {
            this.expect(TokenType.ESCAPE);
            FieldNode field = parseField();
            if (field instanceof ValueNode) {
                ValueNode value = (ValueNode) field;

                if (value.getType() != ParadoxType.VARCHAR || value.getName().length() != 1) {
                    throw new ParadoxSyntaxErrorException(SyntaxError.INVALID_CHAR);
                }

                likeNode.setEscape(value.getName().charAt(0));
            } else {
                throw new ParadoxSyntaxErrorException(SyntaxError.INVALID_CHAR);
            }
        }
    }

    /**
     * Parses a not equals token.
     *
     * @param firstField the left not equals field.
     * @return the not equals node.
     * @throws SQLException in case of parse errors.
     */
    private NotEqualsNode parseNotEquals(final FieldNode firstField) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.NOT_EQUALS);
        final FieldNode value = this.parseField();
        return new NotEqualsNode(firstField, value, position);
    }

    /**
     * Parse the numeric token.
     *
     * @param fieldName the field name.
     * @return the field value.
     * @throws SQLException in case of parse errors.
     */
    private ValueNode parseNumeric(final String fieldName) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.NUMERIC);

        return new ValueNode(fieldName, position, ParadoxType.NUMBER);
    }

    /**
     * Parse the true token.
     *
     * @param fieldName the field name.
     * @return the field value.
     * @throws SQLException in case of parse errors.
     */
    private ValueNode parseTrue(final String fieldName) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.TRUE);

        final ValueNode value = new ValueNode("true", position, ParadoxType.BOOLEAN);
        value.setAlias(fieldName);
        return value;
    }

    /**
     * Parse the false token.
     *
     * @param fieldName the field name.
     * @return the field value.
     * @throws SQLException in case of parse errors.
     */
    private ValueNode parseFalse(final String fieldName) throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.FALSE);

        final ValueNode value = new ValueNode("false", position, ParadoxType.BOOLEAN);
        value.setAlias(fieldName);
        return value;
    }

    private ValueNode parseNull() throws SQLException {
        final ScannerPosition position = getPosition();
        this.expect(TokenType.NULL);

        final ValueNode value = new ValueNode(null, position, ParadoxType.NULL);
        value.setAlias("null");
        return value;
    }

    /**
     * Parses the operators token.
     *
     * @param parent the node child.
     * @return the conditional operator node.
     * @throws SQLException in case or errors.
     */
    private AbstractConditionalNode parseOperators(final AbstractConditionalNode parent) throws SQLException {
        final ScannerPosition position = getPosition();
        AbstractConditionalNode ret;
        if (isToken(TokenType.AND)) {
            // Token type AND.
            this.expect(TokenType.AND);

            if (parent instanceof ANDNode) {
                ret = parent;
            } else {
                ret = new ANDNode(parent, position);
            }

            ret.addChild(this.parseSubCondition(null));
        } else {
            // Token type OR.
            this.expect(TokenType.OR);

            if (parent instanceof ORNode) {
                ret = parent;
            } else {
                ret = new ORNode(parent, position);
            }

            ret.addChild(this.parseSubCondition(null));
        }

        return ret;
    }

    /**
     * Parses ORDER BY node.
     *
     * @param select the select statement node.
     * @throws SQLException in case of failures.
     */
    private void parseOrderBy(final SelectNode select) throws SQLException {
        this.expect(TokenType.ORDER);

        ScannerPosition position = getPosition();
        this.expect(TokenType.BY);

        if (this.token == null) {
            addOffset(position, TokenType.BY.name().length());
            throw new ParadoxSyntaxErrorException(SyntaxError.EMPTY_COLUMN_LIST, position);
        }

        boolean firstField = true;
        while (this.token != null && !this.token.isSelectBreak()) {
            // Field Name
            expectComma(!firstField);
            firstField = false;

            final String fieldName = this.token.getValue();
            FieldNode fieldNode;
            position = getPosition();
            if (this.token.getType() == TokenType.NUMERIC) {
                fieldNode = parseNumeric(fieldName);
            } else if (this.token.getType() == TokenType.IDENTIFIER) {
                fieldNode = parseIdentifierFieldFunction(fieldName, true);
            } else {
                throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_TOKEN, position);
            }

            OrderType type = getOrderType();

            select.addOrderBy(fieldNode, type);
        }
    }

    /**
     * Parses the order by type.
     *
     * @return the order by type.
     * @throws SQLException in case of failures.
     */
    private OrderType getOrderType() throws SQLException {
        OrderType type = OrderType.ASC;
        if (this.isToken(TokenType.ASC)) {
            this.expect(TokenType.ASC);
            // Default order, nothing to change on it.
        } else if (this.isToken(TokenType.DESC)) {
            this.expect(TokenType.DESC);
            type = OrderType.DESC;
        }
        return type;
    }

    /**
     * Parses GROUP BY node.
     *
     * @param select the select statement node.
     * @throws SQLException in case of failures.
     */
    private void parseGroupBy(final SelectNode select) throws SQLException {
        this.expect(TokenType.GROUP);

        ScannerPosition position = getPosition();
        this.expect(TokenType.BY);

        if (this.token == null) {
            addOffset(position, TokenType.BY.name().length());
            throw new ParadoxSyntaxErrorException(SyntaxError.EMPTY_COLUMN_LIST, position);
        }

        boolean firstField = true;
        while (this.token != null && !this.token.isConditionBreak()) {
            // Field Name
            expectComma(!firstField);
            firstField = false;

            final String fieldName = this.token.getValue();
            FieldNode fieldNode;
            position = getPosition();
            if (isToken(TokenType.NUMERIC)) {
                fieldNode = parseNumeric(fieldName);
            } else if (isToken(TokenType.CHARACTER)) {
                fieldNode = parseCharacter(fieldName);
            } else if (isToken(TokenType.NULL)) {
                fieldNode = this.parseNull();
            } else if (isToken(TokenType.TRUE)) {
                fieldNode = this.parseTrue(this.token.getValue());
            } else if (isToken(TokenType.FALSE)) {
                fieldNode = this.parseFalse(this.token.getValue());
            } else if (isToken(TokenType.IDENTIFIER)) {
                fieldNode = parseIdentifierFieldFunction(fieldName, false);
            } else {
                throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_TOKEN, position);
            }

            select.addGroupBy(fieldNode);
        }
    }

    /**
     * Parse a Select Statement.
     *
     * @return a select statement node.
     * @throws SQLException in case of parse errors.
     */
    private SelectNode parseSelect() throws SQLException {
        ScannerPosition position = getPosition();
        final SelectNode select = new SelectNode(position);
        this.expect(TokenType.SELECT);

        // Allowed only in the beginning of Select Statement
        if (isToken(TokenType.DISTINCT)) {
            select.setDistinct(true);
            this.expect(TokenType.DISTINCT);
        }

        // Field loop
        if (this.token != null) {
            this.parseFields(select);
        }

        if (select.getFields().isEmpty()) {
            addOffset(position, TokenType.SELECT.name().length());
            throw new ParadoxSyntaxErrorException(SyntaxError.EMPTY_COLUMN_LIST, position);
        }

        if (isToken(TokenType.FROM)) {
            this.parseFrom(select);

            // Only SELECT with FROM can have WHERE, GROUP BY and ORDER BY clause.
            if (isToken(TokenType.WHERE)) {
                parseWhere(select);
            }

            if (isToken(TokenType.GROUP)) {
                this.parseGroupBy(select);
            }

            if (isToken(TokenType.ORDER)) {
                this.parseOrderBy(select);
            }
        }

        if (isToken(TokenType.LIMIT)) {
            this.parseLimit(select);
        }

        if (isToken(TokenType.OFFSET)) {
            this.parseOffset(select);
        }

        if (this.scanner.hasNext() || this.token != null) {
            throw new ParadoxSyntaxErrorException(SyntaxError.UNEXPECTED_TOKEN,
                    this.token.getPosition());
        }

        return select;
    }

    /**
     * Parses the limit token.
     *
     * @param select the select node.
     * @throws SQLException in case of failures.
     */
    private void parseLimit(final SelectNode select) throws SQLException {
        this.expect(TokenType.LIMIT);
        select.setLimit(Integer.valueOf(this.token.getValue()));
        if (select.getLimit() < 0) {
            throw new ParadoxSyntaxErrorException(SyntaxError.INVALID_PARAMETER_VALUE,
                    this.token.getPosition(), this.token.getValue());
        }

        this.expect(TokenType.NUMERIC);
    }

    /**
     * Parses the offset token.
     *
     * @param select the select node.
     * @throws SQLException in case of failures.
     */
    private void parseOffset(final SelectNode select) throws SQLException {
        this.expect(TokenType.OFFSET);
        select.setOffset(Integer.valueOf(this.token.getValue()));
        if (select.getOffset() < 0) {
            throw new ParadoxSyntaxErrorException(SyntaxError.INVALID_PARAMETER_VALUE,
                    this.token.getPosition(), this.token.getValue());
        }

        this.expect(TokenType.NUMERIC);
    }

    /**
     * Parses the WHERE clause.
     *
     * @param select the select node.
     * @throws SQLException in case of failures.
     */
    private void parseWhere(final SelectNode select) throws SQLException {
        ScannerPosition position = getPosition();
        this.expect(TokenType.WHERE);
        select.setCondition(this.parseCondition());

        if (select.getCondition() == null) {
            addOffset(position, TokenType.WHERE.name().length());
            throw new ParadoxSyntaxErrorException(SyntaxError.EMPTY_CONDITIONAL_LIST,
                    position);
        }
    }

    /**
     * Check if the current token is the desired type.
     *
     * @param type the token to check.
     * @return true if the current token is the desired type.
     */
    private boolean isToken(final TokenType type) {
        return this.token != null && this.token.getType() == type;
    }

    /**
     * Safe way to get current scanner position.
     *
     * @return the current scanner position.
     */
    private ScannerPosition getPosition() {
        if (this.token != null) {
            return this.token.getPosition();
        }

        return null;
    }

    /**
     * Safe way to add an offset to a scanner position.
     *
     * @param position the scanner position.
     * @param offset   the offset to add.
     */
    private static void addOffset(final ScannerPosition position, final int offset) {
        if (position != null) {
            position.addOffset(offset);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy