Skip to content

Commit

Permalink
std/parsesql: Fix JOIN parsing (#22890)
Browse files Browse the repository at this point in the history
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 );"
```
  • Loading branch information
tuffnatty authored Nov 2, 2024
1 parent 08b82c9 commit 46bb47a
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 33 deletions.
114 changes: 87 additions & 27 deletions lib/pure/parsesql.nim
Original file line number Diff line number Diff line change
Expand Up @@ -507,12 +507,14 @@ type
nkAsgn,
nkFrom,
nkFromItemPair,
nkJoin,
nkNaturalJoin,
nkUsing,
nkGroup,
nkLimit,
nkOffset,
nkHaving,
nkOrder,
nkJoin,
nkDesc,
nkUnion,
nkIntersect,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down
71 changes: 65 additions & 6 deletions tests/stdlib/tparsesql.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down

0 comments on commit 46bb47a

Please sign in to comment.