This repository was archived by the owner on Dec 21, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 114
Help with reduce/reduce conflict while parsing SQL #108
Copy link
Copy link
Closed
Description
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.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels