mirror of
https://github.com/nim-lang/Nim.git
synced 2026-04-19 05:50:30 +00:00
std/parsesql: Fix JOIN parsing (#22890)
This commit fixes/adds tests for and fixes several issues with `JOIN`
operator parsing:
- For OUTER joins, LEFT | RIGHT | FULL specifier is not optional
```nim
doAssertRaises(SqlParseError): discard parseSql("""
SELECT id FROM a
OUTER JOIN b
ON a.id = b.id
""")
```
- For NATURAL JOIN and CROSS JOIN, ON and USING clauses are forbidden
```nim
doAssertRaises(SqlParseError): discard parseSql("""
SELECT id FROM a
CROSS JOIN b
ON a.id = b.id
""")
```
- JOIN should parse as part of FROM, not after WHERE
```nim
doAssertRaises(SqlParseError): discard parseSql("""
SELECT id FROM a
WHERE a.id IS NOT NULL
INNER JOIN b
ON a.id = b.id
""")
```
- LEFT JOIN should parse
```nim
doAssert $parseSql("""
SELECT id FROM a
LEFT JOIN b
ON a.id = b.id
""") == "select id from a left join b on a.id = b.id;"
```
- NATURAL JOIN should parse
```nim
doAssert $parseSql("""
SELECT id FROM a
NATURAL JOIN b
""") == "select id from a natural join b;"
```
- USING should parse
```nim
doAssert $parseSql("""
SELECT id FROM a
JOIN b
USING (id)
""") == "select id from a join b using (id );"
```
- Multiple JOINs should parse
```nim
doAssert $parseSql("""
SELECT id FROM a
JOIN b
ON a.id = b.id
LEFT JOIN c
USING (id)
""") == "select id from a join b on a.id = b.id left join c using (id );"
```
This commit is contained in:
@@ -507,12 +507,14 @@ type
|
||||
nkAsgn,
|
||||
nkFrom,
|
||||
nkFromItemPair,
|
||||
nkJoin,
|
||||
nkNaturalJoin,
|
||||
nkUsing,
|
||||
nkGroup,
|
||||
nkLimit,
|
||||
nkOffset,
|
||||
nkHaving,
|
||||
nkOrder,
|
||||
nkJoin,
|
||||
nkDesc,
|
||||
nkUnion,
|
||||
nkIntersect,
|
||||
@@ -936,18 +938,75 @@ proc parseWhere(p: var SqlParser): SqlNode =
|
||||
result = newNode(nkWhere)
|
||||
result.add(parseExpr(p))
|
||||
|
||||
proc parseJoinType(p: var SqlParser): SqlNode =
|
||||
## parse [ INNER ] JOIN | ( LEFT | RIGHT | FULL ) [ OUTER ] JOIN
|
||||
if isKeyw(p, "inner"):
|
||||
getTok(p)
|
||||
eat(p, "join")
|
||||
return newNode(nkIdent, "inner")
|
||||
elif isKeyw(p, "join"):
|
||||
getTok(p)
|
||||
return newNode(nkIdent, "")
|
||||
elif isKeyw(p, "left") or isKeyw(p, "full") or isKeyw(p, "right"):
|
||||
var joinType = newNode(nkIdent, p.tok.literal.toLowerAscii())
|
||||
getTok(p)
|
||||
optKeyw(p, "outer")
|
||||
eat(p, "join")
|
||||
return joinType
|
||||
else:
|
||||
sqlError(p, "join type expected")
|
||||
|
||||
proc parseFromItem(p: var SqlParser): SqlNode =
|
||||
result = newNode(nkFromItemPair)
|
||||
var expectAs = true
|
||||
if p.tok.kind == tkParLe:
|
||||
getTok(p)
|
||||
var select = parseSelect(p)
|
||||
result.add(select)
|
||||
if isKeyw(p, "select"):
|
||||
result.add(parseSelect(p))
|
||||
else:
|
||||
result = parseFromItem(p)
|
||||
expectAs = false
|
||||
eat(p, tkParRi)
|
||||
else:
|
||||
result.add(parseExpr(p))
|
||||
if isKeyw(p, "as"):
|
||||
if expectAs and isKeyw(p, "as"):
|
||||
getTok(p)
|
||||
result.add(parseExpr(p))
|
||||
while true:
|
||||
if isKeyw(p, "cross"):
|
||||
var join = newNode(nkJoin)
|
||||
join.add(newNode(nkIdent, "cross"))
|
||||
join.add(result)
|
||||
getTok(p)
|
||||
eat(p, "join")
|
||||
join.add(parseFromItem(p))
|
||||
result = join
|
||||
elif isKeyw(p, "natural"):
|
||||
var join = newNode(nkNaturalJoin)
|
||||
getTok(p)
|
||||
join.add(parseJoinType(p))
|
||||
join.add(result)
|
||||
join.add(parseFromItem(p))
|
||||
result = join
|
||||
elif isKeyw(p, "inner") or isKeyw(p, "join") or isKeyw(p, "left") or
|
||||
iskeyw(p, "full") or isKeyw(p, "right"):
|
||||
var join = newNode(nkJoin)
|
||||
join.add(parseJoinType(p))
|
||||
join.add(result)
|
||||
join.add(parseFromItem(p))
|
||||
if isKeyw(p, "on"):
|
||||
getTok(p)
|
||||
join.add(parseExpr(p))
|
||||
elif isKeyw(p, "using"):
|
||||
getTok(p)
|
||||
var n = newNode(nkUsing)
|
||||
parseParIdentList(p, n)
|
||||
join.add n
|
||||
else:
|
||||
sqlError(p, "ON or USING expected")
|
||||
result = join
|
||||
else:
|
||||
break
|
||||
|
||||
proc parseIndexDef(p: var SqlParser): SqlNode =
|
||||
result = parseIfNotExists(p, nkCreateIndex)
|
||||
@@ -1109,19 +1168,6 @@ proc parseSelect(p: var SqlParser): SqlNode =
|
||||
elif isKeyw(p, "except"):
|
||||
result.add(newNode(nkExcept))
|
||||
getTok(p)
|
||||
if isKeyw(p, "join") or isKeyw(p, "inner") or isKeyw(p, "outer") or isKeyw(p, "cross"):
|
||||
var join = newNode(nkJoin)
|
||||
result.add(join)
|
||||
if isKeyw(p, "join"):
|
||||
join.add(newNode(nkIdent, ""))
|
||||
getTok(p)
|
||||
else:
|
||||
join.add(newNode(nkIdent, p.tok.literal.toLowerAscii()))
|
||||
getTok(p)
|
||||
eat(p, "join")
|
||||
join.add(parseFromItem(p))
|
||||
eat(p, "on")
|
||||
join.add(parseExpr(p))
|
||||
if isKeyw(p, "limit"):
|
||||
getTok(p)
|
||||
var l = newNode(nkLimit)
|
||||
@@ -1388,6 +1434,30 @@ proc ra(n: SqlNode, s: var SqlWriter) =
|
||||
of nkFrom:
|
||||
s.addKeyw("from")
|
||||
s.addMulti(n)
|
||||
of nkJoin, nkNaturalJoin:
|
||||
var joinType = n.sons[0].strVal
|
||||
if joinType == "":
|
||||
joinType = "join"
|
||||
else:
|
||||
joinType &= " " & "join"
|
||||
if n.kind == nkNaturalJoin:
|
||||
joinType = "natural " & joinType
|
||||
ra(n.sons[1], s)
|
||||
s.addKeyw(joinType)
|
||||
# If the right part of the join is not leaf, parenthesize it
|
||||
if n.sons[2].kind != nkFromItemPair:
|
||||
s.add('(')
|
||||
ra(n.sons[2], s)
|
||||
s.add(')')
|
||||
else:
|
||||
ra(n.sons[2], s)
|
||||
if n.sons.len > 3:
|
||||
if n.sons[3].kind != nkUsing:
|
||||
s.addKeyw("on")
|
||||
ra(n.sons[3], s)
|
||||
of nkUsing:
|
||||
s.addKeyw("using")
|
||||
rs(n, s)
|
||||
of nkGroup:
|
||||
s.addKeyw("group by")
|
||||
s.addMulti(n)
|
||||
@@ -1403,16 +1473,6 @@ proc ra(n: SqlNode, s: var SqlWriter) =
|
||||
of nkOrder:
|
||||
s.addKeyw("order by")
|
||||
s.addMulti(n)
|
||||
of nkJoin:
|
||||
var joinType = n.sons[0].strVal
|
||||
if joinType == "":
|
||||
joinType = "join"
|
||||
else:
|
||||
joinType &= " " & "join"
|
||||
s.addKeyw(joinType)
|
||||
ra(n.sons[1], s)
|
||||
s.addKeyw("on")
|
||||
ra(n.sons[2], s)
|
||||
of nkDesc:
|
||||
ra(n.sons[0], s)
|
||||
s.addKeyw("desc")
|
||||
|
||||
@@ -159,17 +159,76 @@ INNER JOIN b
|
||||
ON a.id == b.id
|
||||
""") == "select id from a inner join b on a.id == b.id;"
|
||||
|
||||
doAssert $parseSql("""
|
||||
# For OUTER joins, LEFT | RIGHT | FULL specifier is not optional
|
||||
doAssertRaises(SqlParseError): discard parseSql("""
|
||||
SELECT id FROM a
|
||||
OUTER JOIN b
|
||||
ON a.id == b.id
|
||||
""") == "select id from a outer join b on a.id == b.id;"
|
||||
ON a.id = b.id
|
||||
""")
|
||||
|
||||
doAssert $parseSql("""
|
||||
# For NATURAL JOIN and CROSS JOIN, ON and USING clauses are forbidden
|
||||
doAssertRaises(SqlParseError): discard parseSql("""
|
||||
SELECT id FROM a
|
||||
CROSS JOIN b
|
||||
ON a.id == b.id
|
||||
""") == "select id from a cross join b on a.id == b.id;"
|
||||
ON a.id = b.id
|
||||
""")
|
||||
|
||||
# JOIN should parse as part of FROM, not after WHERE
|
||||
doAssertRaises(SqlParseError): discard parseSql("""
|
||||
SELECT id FROM a
|
||||
WHERE a.id IS NOT NULL
|
||||
INNER JOIN b
|
||||
ON a.id = b.id
|
||||
""")
|
||||
|
||||
# JOIN should parse as part of FROM, other fromItems may follow
|
||||
doAssert $parseSql("""
|
||||
SELECT id
|
||||
FROM
|
||||
a JOIN b ON a.id = b.id,
|
||||
c
|
||||
""") == "select id from a join b on a.id = b.id, c;"
|
||||
|
||||
# LEFT JOIN should parse
|
||||
doAssert $parseSql("""
|
||||
SELECT id FROM a
|
||||
LEFT JOIN b
|
||||
ON a.id = b.id
|
||||
""") == "select id from a left join b on a.id = b.id;"
|
||||
|
||||
# NATURAL JOIN should parse
|
||||
doAssert $parseSql("""
|
||||
SELECT id FROM a
|
||||
NATURAL JOIN b
|
||||
""") == "select id from a natural join b;"
|
||||
|
||||
# USING should parse
|
||||
doAssert $parseSql("""
|
||||
SELECT id FROM a
|
||||
JOIN b
|
||||
USING (id)
|
||||
""") == "select id from a join b using (id );"
|
||||
|
||||
# Multiple JOINs should parse
|
||||
doAssert $parseSql("""
|
||||
SELECT id FROM a
|
||||
JOIN b
|
||||
ON a.id = b.id
|
||||
LEFT JOIN c
|
||||
USING (id)
|
||||
""") == "select id from a join b on a.id = b.id left join c using (id );"
|
||||
|
||||
# Parenthesized JOIN expressions should parse
|
||||
doAssert $parseSql("""
|
||||
SELECT id
|
||||
FROM a JOIN (b JOIN c USING (id)) ON a.id = b.id
|
||||
""") == "select id from a join(b join c using (id )) on a.id = b.id;"
|
||||
|
||||
# Left-side parenthesized JOIN expressions should parse
|
||||
doAssert $parseSql("""
|
||||
SELECT id
|
||||
FROM (b JOIN c USING (id)) JOIN a ON a.id = b.id
|
||||
""") == "select id from b join c using (id ) join a on a.id = b.id;"
|
||||
|
||||
doAssert $parseSql("""
|
||||
CREATE TYPE happiness AS ENUM ('happy', 'very happy', 'ecstatic');
|
||||
|
||||
Reference in New Issue
Block a user