Skip to content
This repository was archived by the owner on Dec 21, 2025. It is now read-only.
This repository was archived by the owner on Dec 21, 2025. It is now read-only.

Help with reduce/reduce conflict while parsing SQL #108

@leandropls

Description

@leandropls

I'm trying parse some simple SQL clauses and build them into SQLAlchemy objects. While parsing the example expressions below seem to work fine, I can't solve the generated reduce/reduce conflict. Can someone help me elucidate this?

This is a very simplified version to exemplify the problem I'm having:

from secrets import token_hex
from typing import Any

import sqlalchemy as sa
from sly import Lexer, Parser
from sqlalchemy.sql.elements import BindParameter, ClauseElement, ColumnClause


# noinspection PyUnresolvedReferences,PyUnboundLocalVariable,PyPep8Naming
class SQLLexer(Lexer):
    tokens = {
        BETWEEN,
        NUMBER,
        BOOLEAN,
        AND,
        COLUMN,
    }

    ignore = " \n"

    BETWEEN = r"BETWEEN"
    NUMBER = r"(\d+([.]\d*)?([eE][+-]?\d+)?|[.]\d+([eE][+-]?\d+)?)"
    BOOLEAN = r"TRUE|FALSE"
    AND = r"AND"
    COLUMN = r'"(?:[^"]|"")*"|[a-zA-Z_][a-zA-Z0-9_]*'


# noinspection PyUnresolvedReferences,PyUnusedLocal
class SQLParser(Parser):
    debugfile = "parser.txt"
    tokens = SQLLexer.tokens

    precedence = (
        ("left", AND),
        ("nonassoc", BETWEEN),
    )

    # Schema Item
    @_("COLUMN")
    def expr(self, p: tuple[str]) -> ColumnClause:
        return sa.column(p[0])

    # Number
    @_("NUMBER")
    def expr(self, p: tuple[str]) -> BindParameter:
        try:
            return wrap_value(int(p[0]))
        except ValueError:
            return wrap_value(float(p[0]))

    # Boolean
    @_("BOOLEAN")
    def expr(self, p: tuple[str]) -> BindParameter:
        if p[0].lower() == "true":
            return wrap_value(True)
        elif p[0].lower() == "false":
            return wrap_value(False)
        else:
            raise NotImplementedError("unknown boolean value")

    # AND
    @_("expr AND expr")
    def expr(
        self, p: tuple[ClauseElement, str, ClauseElement]
    ) -> tuple[ClauseElement, ClauseElement]:
        return sa.and_(p[0], p[2])

    # BETWEEN
    @_("expr BETWEEN expr AND expr")
    def expr(
        self, p: tuple[ClauseElement, str, ClauseElement, str, ClauseElement]
    ) -> tuple[ClauseElement, ClauseElement]:
        return p[0].between(p[2], p[4])


def wrap_value(value: Any) -> BindParameter:
    return sa.bindparam("param_" + token_hex(4), value)


if __name__ == "__main__":
    samples = [
        "myFlag AND FALSE",
        "rowNumber BETWEEN 1 AND 10",
    ]
    lexer = SQLLexer()
    parser = SQLParser()
    for sample in samples:
        print(f"input: {sample}", flush=True)
        tokens = list(lexer.tokenize(sample))
        print(f"tokens: {[(t.type, t.value) for t in tokens]}", flush=True)
        generated = parser.parse(iter(tokens))
        print(f"generated: {generated}", flush=True)
        print(flush=True)

And here's the output:

WARNING: 2 reduce/reduce conflicts
Parser debugging for SQLParser written to parser.txt
input: myFlag AND FALSE
tokens: [('COLUMN', 'myFlag'), ('AND', 'AND'), ('BOOLEAN', 'FALSE')]
generated: "myFlag" AND :param_da45c008

input: rowNumber BETWEEN 1 AND 10
tokens: [('COLUMN', 'rowNumber'), ('BETWEEN', 'BETWEEN'), ('NUMBER', '1'), ('AND', 'AND'), ('NUMBER', '10')]
generated: "rowNumber" BETWEEN :param_199c82be AND :param_01991b92

The conflict info:

Conflicts:

reduce/reduce conflict in state 10 resolved using rule expr -> expr AND expr  [precedence=left, level=1]
rejected rule (expr -> expr BETWEEN expr AND expr  [precedence=left, level=1]) in state 10

And state 10 info:

state 10

    (1) expr -> expr BETWEEN expr AND expr .
    (2) expr -> expr AND expr .
    (1) expr -> expr . BETWEEN expr AND expr
    (2) expr -> expr . AND expr
  ! reduce/reduce conflict for AND resolved using rule 2 (expr -> expr AND expr .)
  ! reduce/reduce conflict for BETWEEN resolved using rule 2 (expr -> expr AND expr .)
    $end            reduce using rule 1 (expr -> expr BETWEEN expr AND expr .)
    AND             reduce using rule 2 (expr -> expr AND expr .)
    BETWEEN         shift and go to state 5

I've had similar issues with expr IS expr and expr IS NOT expr which I wanted to treat as a.is_(b) and a.isnot(b) respectively, which I tried to solve with %prec with no success. But I'm assuming that figuring out this more complex (above) case will also handle the simpler one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions