This commit is contained in:
treeform 2017-12-13 23:34:44 +00:00
commit 08d7b5d031
2 changed files with 397 additions and 452 deletions

View file

@ -662,10 +662,12 @@ proc getPrecedence(p: SqlParser): int =
elif isOpr(p, "=") or isOpr(p, "<") or isOpr(p, ">") or isOpr(p, ">=") or elif isOpr(p, "=") or isOpr(p, "<") or isOpr(p, ">") or isOpr(p, ">=") or
isOpr(p, "<=") or isOpr(p, "<>") or isOpr(p, "!=") or isKeyw(p, "is") or isOpr(p, "<=") or isOpr(p, "<>") or isOpr(p, "!=") or isKeyw(p, "is") or
isKeyw(p, "like"): isKeyw(p, "like"):
result = 3 result = 4
elif isKeyw(p, "and"): elif isKeyw(p, "and"):
result = 2 result = 3
elif isKeyw(p, "or"): elif isKeyw(p, "or"):
result = 2
elif isKeyw(p, "between"):
result = 1 result = 1
elif p.tok.kind == tkOperator: elif p.tok.kind == tkOperator:
# user-defined operator: # user-defined operator:
@ -1015,6 +1017,8 @@ proc parseUpdate(p: var SqlParser): SqlNode =
proc parseDelete(p: var SqlParser): SqlNode = proc parseDelete(p: var SqlParser): SqlNode =
getTok(p) getTok(p)
if isOpr(p, "*"):
getTok(p)
result = newNode(nkDelete) result = newNode(nkDelete)
eat(p, "from") eat(p, "from")
result.add(primary(p)) result.add(primary(p))
@ -1156,7 +1160,7 @@ proc open(p: var SqlParser, input: Stream, filename: string) =
proc parse(p: var SqlParser): SqlNode = proc parse(p: var SqlParser): SqlNode =
## parses the content of `p`'s input stream and returns the SQL AST. ## parses the content of `p`'s input stream and returns the SQL AST.
## Syntax errors raise an `EInvalidSql` exception. ## Syntax errors raise an `SqlParseError` exception.
result = newNode(nkStmtList) result = newNode(nkStmtList)
while p.tok.kind != tkEof: while p.tok.kind != tkEof:
parseStmt(p, result) parseStmt(p, result)
@ -1173,7 +1177,7 @@ proc close(p: var SqlParser) =
proc parseSQL*(input: Stream, filename: string): SqlNode = proc parseSQL*(input: Stream, filename: string): SqlNode =
## parses the SQL from `input` into an AST and returns the AST. ## parses the SQL from `input` into an AST and returns the AST.
## `filename` is only used for error messages. ## `filename` is only used for error messages.
## Syntax errors raise an `EInvalidSql` exception. ## Syntax errors raise an `SqlParseError` exception.
var p: SqlParser var p: SqlParser
open(p, input, filename) open(p, input, filename)
try: try:
@ -1184,7 +1188,7 @@ proc parseSQL*(input: Stream, filename: string): SqlNode =
proc parseSQL*(input: string, filename=""): SqlNode = proc parseSQL*(input: string, filename=""): SqlNode =
## parses the SQL from `input` into an AST and returns the AST. ## parses the SQL from `input` into an AST and returns the AST.
## `filename` is only used for error messages. ## `filename` is only used for error messages.
## Syntax errors raise an `EInvalidSql` exception. ## Syntax errors raise an `SqlParseError` exception.
parseSQL(newStringStream(input), "") parseSQL(newStringStream(input), "")
@ -1210,7 +1214,7 @@ proc addKeyw(s: var SqlWriter, thing: string) =
s.buffer.add(" ") s.buffer.add(" ")
proc rm(s: var SqlWriter, chars = " \L,") = proc rm(s: var SqlWriter, chars = " \L,") =
while s.buffer[^1] in chars: while s.buffer.len > 0 and s.buffer[^1] in chars:
s.buffer = s.buffer[0..^2] s.buffer = s.buffer[0..^2]
proc newLine(s: var SqlWriter) = proc newLine(s: var SqlWriter) =
@ -1253,6 +1257,7 @@ proc ra(n: SqlNode, s: var SqlWriter) =
else: else:
s.add("\"" & replace(n.strVal, "\"", "\"\"") & "\"") s.add("\"" & replace(n.strVal, "\"", "\"\"") & "\"")
of nkStringLit: of nkStringLit:
# TODO add e'' as an option?
s.add(escape(n.strVal, "'", "'")) s.add(escape(n.strVal, "'", "'"))
of nkBitStringLit: of nkBitStringLit:
s.add("b'" & n.strVal & "'") s.add("b'" & n.strVal & "'")
@ -1329,23 +1334,25 @@ proc ra(n: SqlNode, s: var SqlWriter) =
assert n.len == 3 assert n.len == 3
s.addKeyw("insert into") s.addKeyw("insert into")
ra(n.sons[0], s) ra(n.sons[0], s)
s.add(" ")
ra(n.sons[1], s) ra(n.sons[1], s)
if n.sons[2].kind == nkDefault: if n.sons[2].kind == nkDefault:
s.addKeyw("default values") s.addKeyw("default values")
else: else:
s.newLine() s.newLine()
ra(n.sons[2], s) ra(n.sons[2], s)
s.rm(" ")
s.add(';') s.add(';')
of nkUpdate: of nkUpdate:
s.addKeyw("update") s.innerKeyw("update"):
ra(n.sons[0], s) ra(n.sons[0], s)
s.addKeyw("set") s.innerKeyw("set"):
var L = n.len var L = n.len
for i in 1 .. L-2: for i in 1 .. L-2:
if i > 1: s.add(", ") if i > 1: s.add(", ")
var it = n.sons[i] var it = n.sons[i]
assert it.kind == nkAsgn assert it.kind == nkAsgn
ra(it, s) ra(it, s)
ra(n.sons[L-1], s) ra(n.sons[L-1], s)
s.add(';') s.add(';')
of nkDelete: of nkDelete:
@ -1404,8 +1411,8 @@ proc ra(n: SqlNode, s: var SqlWriter) =
s.innerKeyw("having"): s.innerKeyw("having"):
rs(n, s, "", "", ", ") rs(n, s, "", "", ", ")
of nkOrder: of nkOrder:
s.addKeyw("order by") s.innerKeyw("order by"):
rs(n, s, "", "", ", ") rs(n, s, "", "", ", ")
of nkJoin: of nkJoin:
var joinType = n.sons[0].strVal var joinType = n.sons[0].strVal
if joinType == "": if joinType == "":

View file

@ -1,438 +1,376 @@
import unittest discard """
file: "tparsesql.nim"
output: '''select
foo
from
table;
select
foo
from
table;
select
foo
from
table
limit
10;
select
foo,
bar,
baz
from
table
limit
10;
select
foo as bar
from
table;
select
foo as foo_prime,
bar as bar_prime,
baz as baz_prime
from
table;
select
*
from
table;
select
*
from
table
where
((a = b) and (c = d));
select
*
from
table
where
(not b);
select
*
from
table
where
(a and (not b));
select
*
from
table
where
(((a = b) and (c = d)) or ((n is null) and (((not b) + 1) = 3)));
select
*
from
table
having
((a = b) and (c = d));
select
a,
b
from
table
group by
a;
select
a,
b
from
table
group by
1, 2;
select
t.a
from
t as t;
select
a,
b
from
(
select
*
from
t
);
select
a,
b
from
(
select
*
from
t
) as foo;
select
a,
b
from
(
select
*
from
(
select
*
from
(
select
*
from
(
select
*
from
inner as inner1
) as inner2
) as inner3
) as inner4
) as inner5;
select
a,
b
from
(
select
*
from
a
), (
select
*
from
b
), (
select
*
from
c
);
select
*
from
Products
where
(Price BETWEEN (10 AND 20));
select
id
from
a
join
b
on
(a.id == b.id);
select
id
from
a
join
(
select
id
from
c
) as b
on
(a.id == b.id);
select
id
from
a
INNER join
b
on
(a.id == b.id);
select
id
from
a
OUTER join
b
on
(a.id == b.id);
select
id
from
a
CROSS join
b
on
(a.id == b.id);
create type happiness as enum ('happy', 'very happy', 'ecstatic');
create table holidays(
num_weeks int,
happiness happiness);
create index table1_attr1 on table1(attr1);
select
*
from
myTab
where
(col1 = 'happy');
insert into Customers (CustomerName, ContactName, Address, City, PostalCode, Country)
values ('Cardinal', 'Tom B. Erichsen', 'Skagen 21', 'Stavanger', '4006', 'Norway');
insert into TableName default values;
update
Customers
set
ContactName = 'Alfred Schmidt', City = 'Frankfurt'
where
(CustomerID = 1);
delete from table_name;
delete from table_name;
select
*
from
Customers;
select
*
from
Customers
where
((((CustomerName LIKE 'L%') OR (CustomerName LIKE 'R%')) OR (CustomerName LIKE 'W%')) AND (Country = 'USA'))
order by
CustomerName;
'''
"""
import sequtils
import strutils
import parsesql import parsesql
proc fold(str: string): string = echo $parseSQL "SELECT foo FROM table;"
var echo $parseSQL "SELECT foo FROM table"
lines = str.split("\L") echo $parseSQL "SELECT foo FROM table limit 10"
minCount = 1000 echo $parseSQL "SELECT foo, bar, baz FROM table limit 10"
while lines.len > 0 and lines[0].strip().len == 0: echo $parseSQL "SELECT foo AS bar FROM table"
lines.delete(0, 0) echo $parseSQL "SELECT foo AS foo_prime, bar AS bar_prime, baz AS baz_prime FROM table"
while lines.len > 0 and lines[lines.len-1].strip().len == 0: echo $parseSQL "SELECT * FROM table"
lines.delete(lines.len, lines.len) #TODO add count(*)
for line in lines: #echo $parseSQL "SELECT COUNT(*) FROM table"
var count = 0 echo $parseSQL """
while line[count] == ' ': SELECT * FROM table
inc count WHERE a = b and c = d
if minCount > count: """
minCount = count echo $parseSQL """
for i, line in lines: SELECT * FROM table
lines[i] = line[minCount..^1] WHERE not b
return lines.join("\L") """
echo $parseSQL """
proc parseCheck(have: string, need: string) = SELECT
var *
sql = parseSQL(have) FROM
sqlHave = renderSQL(sql, true).strip() table
sqlNeed = need.fold().strip() WHERE
var a and not b
haveLines = sqlHave.split("\L") """
needLines = sqlNeed.split("\L") echo $parseSQL """
for i in 0..<haveLines.len: SELECT * FROM table
if haveLines[i] != needLines[i]: WHERE a = b and c = d or n is null and not b + 1 = 3
echo "" """
echo " --- have --- " echo $parseSQL """
echo sqlHave SELECT * FROM table
#echo repr(sql) HAVING a = b and c = d
echo " --- need --- " """
echo sqlNeed echo $parseSQL """
echo " --- lines --- " SELECT a, b FROM table
echo repr haveLines[i] GROUP BY a
echo repr needLines[i] """
echo "line: ", i echo $parseSQL """
raise newException(Exception, "Two don't equal.") SELECT a, b FROM table
GROUP BY 1, 2
"""
suite "sql": echo $parseSQL "SELECT t.a FROM t as t"
echo $parseSQL """
test "basic": SELECT a, b FROM (
parseCheck "SELECT foo FROM table;", """ SELECT * FROM t
SELECT )
foo """
FROM echo $parseSQL """
table; SELECT a, b FROM (
""" SELECT * FROM t
) as foo
test "dont require ; at the end": """
parseCheck "SELECT foo FROM table", """ echo $parseSQL """
SELECT SELECT a, b FROM (
foo SELECT * FROM (
FROM SELECT * FROM (
table; SELECT * FROM (
""" SELECT * FROM inner as inner1
) as inner2
test "limit": ) as inner3
parseCheck "SELECT foo FROM table limit 10", """ ) as inner4
SELECT ) as inner5
foo """
FROM echo $parseSQL """
table SELECT a, b FROM
LIMIT (SELECT * FROM a),
10; (SELECT * FROM b),
""" (SELECT * FROM c)
"""
test "fields": echo $parseSQL """
parseCheck "SELECT foo, bar, baz FROM table limit 10", """ SELECT * FROM Products
SELECT WHERE Price BETWEEN 10 AND 20;
foo, """
bar, echo $parseSQL """
baz SELECT id FROM a
FROM JOIN b
table ON a.id == b.id
LIMIT """
10; echo $parseSQL """
""" SELECT id FROM a
JOIN (SELECT id from c) as b
test "as_field": ON a.id == b.id
parseCheck "SELECT foo AS bar FROM table", """ """
SELECT echo $parseSQL """
foo AS bar SELECT id FROM a
FROM INNER JOIN b
table; ON a.id == b.id
""" """
echo $parseSQL """
parseCheck "SELECT foo AS foo_prime, bar AS bar_prime, baz AS baz_prime FROM table", """ SELECT id FROM a
SELECT OUTER JOIN b
foo AS foo_prime, ON a.id == b.id
bar AS bar_prime, """
baz AS baz_prime echo $parseSQL """
FROM SELECT id FROM a
table; CROSS JOIN b
""" ON a.id == b.id
"""
test "select *": echo $parseSQL """
parseCheck "SELECT * FROM table", """ CREATE TYPE happiness AS ENUM ('happy', 'very happy', 'ecstatic');
SELECT CREATE TABLE holidays (
* num_weeks int,
FROM happiness happiness
table; );
""" CREATE INDEX table1_attr1 ON table1(attr1);
# TODO: COUNT(*) SELECT * FROM myTab WHERE col1 = 'happy';
#parseCheck "SELECT COUNT(*) FROM table", """ """
# SELECT * echo $parseSQL """
# FROM table; INSERT INTO Customers (CustomerName, ContactName, Address, City, PostalCode, Country)
#""" VALUES ('Cardinal', 'Tom B. Erichsen', 'Skagen 21', 'Stavanger', '4006', 'Norway');
"""
test "where": echo $parseSQL """
parseCheck """ INSERT INTO TableName DEFAULT VALUES
SELECT * FROM table """
WHERE a = b and c = d echo $parseSQL """
""", """ UPDATE Customers
SELECT SET ContactName = 'Alfred Schmidt', City= 'Frankfurt'
* WHERE CustomerID = 1;
FROM """
table echo $parseSQL "DELETE FROM table_name;"
WHERE echo $parseSQL "DELETE * FROM table_name;"
((a = b) and (c = d)); echo $parseSQL """
""" --Select all:
SELECT * FROM Customers;
parseCheck """ """
SELECT * FROM table echo $parseSQL """
WHERE not b SELECT * FROM Customers WHERE (CustomerName LIKE 'L%'
""", """ OR CustomerName LIKE 'R%' /*OR CustomerName LIKE 'S%'
SELECT OR CustomerName LIKE 'T%'*/ OR CustomerName LIKE 'W%')
* AND Country='USA'
FROM ORDER BY CustomerName;
table """
WHERE
(not b);
"""
parseCheck """
SELECT
*
FROM
table
WHERE
a and not b
""", """
SELECT
*
FROM
table
WHERE
(a and (not b));
"""
parseCheck """
SELECT *
FROM table
WHERE not a and not b
""", """
SELECT
*
FROM
table
WHERE
((not a) and (not b));
"""
parseCheck """
SELECT * FROM table
WHERE a = b and c = d or n is null and not b + 1 = 3
""", """
SELECT
*
FROM
table
WHERE
(((a = b) and (c = d)) or ((n is null) and (((not b) + 1) = 3)));
"""
test "having":
parseCheck """
SELECT * FROM table
HAVING a = b and c = d
""", """
SELECT
*
FROM
table
HAVING
((a = b) and (c = d));
"""
test "group by":
parseCheck """
SELECT a, b FROM table
GROUP BY a
""", """
SELECT
a,
b
FROM
table
GROUP BY
a;
"""
parseCheck """
SELECT a, b FROM table
GROUP BY 1, 2
""", """
SELECT
a,
b
FROM
table
GROUP BY
1, 2;
"""
test "as table":
parseCheck """
SELECT t.a FROM t as t
""", """
SELECT
t.a
FROM
t AS t;
"""
test "subselects":
parseCheck """
SELECT a, b FROM (
SELECT * FROM t
)
""", """
SELECT
a,
b
FROM
(
SELECT
*
FROM
t
);
"""
parseCheck """
SELECT a, b FROM (
SELECT * FROM t
) as foo
""", """
SELECT
a,
b
FROM
(
SELECT
*
FROM
t
) AS foo;
"""
parseCheck """
SELECT a, b FROM (
SELECT * FROM (
SELECT * FROM (
SELECT * FROM (
SELECT * FROM inner as inner1
) as inner2
) as inner3
) as inner4
) as inner5
""", """
SELECT
a,
b
FROM
(
SELECT
*
FROM
(
SELECT
*
FROM
(
SELECT
*
FROM
(
SELECT
*
FROM
inner AS inner1
) AS inner2
) AS inner3
) AS inner4
) AS inner5;
"""
parseCheck """
SELECT a, b FROM
(SELECT * FROM a),
(SELECT * FROM b),
(SELECT * FROM c)
""","""
SELECT
a,
b
FROM
(
SELECT
*
FROM
a
), (
SELECT
*
FROM
b
), (
SELECT
*
FROM
c
);
"""
test "original test":
parseCheck """
CREATE TYPE happiness AS ENUM ('happy', 'very happy', 'ecstatic');
CREATE TABLE holidays (
num_weeks int,
happiness happiness
);
CREATE INDEX table1_attr1 ON table1(attr1);
SELECT * FROM myTab WHERE col1 = 'happy';
""", """
CREATE TYPE happiness AS ENUM ('happy', 'very happy', 'ecstatic');
CREATE TABLE holidays(
num_weeks int,
happiness happiness);
CREATE INDEX table1_attr1 ON table1(attr1);
SELECT
*
FROM
myTab
WHERE
(col1 = 'happy');
"""
test "joins":
parseCheck """
SELECT id FROM a
JOIN b
ON a.id == b.id
""", """
SELECT
id
FROM
a
JOIN
b
ON
(a.id == b.id);
"""
parseCheck """
SELECT id FROM a
JOIN (SELECT id from c) as b
ON a.id == b.id
""", """
SELECT
id
FROM
a
JOIN
(
SELECT
id
FROM
c
) AS b
ON
(a.id == b.id);
"""
parseCheck """
SELECT id FROM a
INNER JOIN b
ON a.id == b.id
""", """
SELECT
id
FROM
a
INNER JOIN
b
ON
(a.id == b.id);
"""
parseCheck """
SELECT id FROM a
OUTER JOIN b
ON a.id == b.id
""", """
SELECT
id
FROM
a
OUTER JOIN
b
ON
(a.id == b.id);
"""
parseCheck """
SELECT id FROM a
CROSS JOIN b
ON a.id == b.id
""", """
SELECT
id
FROM
a
CROSS JOIN
b
ON
(a.id == b.id);
"""