packages feed

preql-0.4: test/Test/Syntax/Parser.hs

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings        #-}

module Test.Syntax.Parser where

import Preql.QuasiQuoter.Syntax.Name
import Preql.QuasiQuoter.Syntax.Parser
import Preql.QuasiQuoter.Syntax.Syntax

import qualified Preql.QuasiQuoter.Syntax.Lex as L

import Data.Either (isRight)
import Data.List.NonEmpty (NonEmpty(..))
import Prelude hiding (Ordering(..), lex)
import Test.Tasty
import Test.Tasty.HUnit

lexer :: TestTree
lexer = testGroup "lexer"
    [ testLex "'foo''bar'" [L.String "foo'bar"]
    , testLex "SELECT 2 + 3;" [L.SELECT, L.Iconst 2, L.Add, L.Iconst 3, L.Semicolon]
    , testLex "5" [L.Iconst 5]
    , testLex "2.5" [L.Fconst 2.5]
    , testLex "2e-2" [L.Fconst 0.02]
    , testLex "2.5e-1" [L.Fconst 0.25]
    , testLex "1 2.3" [ L.Iconst 1, L.Fconst 2.3 ]
    , testLex "1 ." [ L.Iconst 1, L.Dot ]
    , testLex "SELECT 1 IS NULL" [ L.SELECT, L.Iconst 1, L.IS, L.NULL_P ]
    , testLex "SELECT 1 ISNULL" [ L.SELECT, L.Iconst 1, L.ISNULL ]
    ]

parser :: TestTree
parser = testGroup "parser"
  [ testGroup "Expr"
    [ testParseExpr "2 * 3 + 1"
      (BinOp Add (BinOp Mul (Lit (I 2)) (Lit (I 3))) (Lit (I 1)))
    , testParseExpr "1 + 2 * 3"
      (BinOp Add (Lit (I 1)) (BinOp Mul (Lit (I 2)) (Lit (I 3))) )
    , testParseExpr "name = 'Daniel'"
      (BinOp Eq (CRef "name") (Lit (T "Daniel")))
    , testParseExpr "TRUE" (Lit (B True))
    , testParseExpr "true" (Lit (B True))
    , testParseExpr "false" (Lit (B False))
    ]
  , testGroup "Statement"
    [ testParse "DELETE FROM taffy"
        (QD (Delete (mkName "taffy") Nothing))
    , testParse "dEleTe FROM taffy WHERE flavor = 'blueberry'"
      (QD Delete
          { table = mkName "taffy"
          , conditions = Just (BinOp Eq (CRef "flavor") (Lit (T"blueberry")))
          })
    , testParse "DELETE FROM users WHERE email != 'bergey@teallabs.org'"
      (QD Delete
       { table = mkName "users"
       , conditions = Just (BinOp NEq (CRef "email") (Lit (T "bergey@teallabs.org")))
       })
    , testParse "INSERT INTO users (email) VALUES ('bergey@teallabs.org')"
        (QI Insert
            { table = mkName "users"
            , columns = mkName "email" :| []
            , values = Lit (T "bergey@teallabs.org") :| []
            })
    , testParse "INSERT INTO addresses (street, country) VALUES ('4 Ames St', 'USA')"
      (QI Insert
       { table = "addresses"
       , columns = "street" :| ["country" ]
       , values = Lit (T "4 Ames St") :| [ Lit (T "USA") ]
       })
    ]
  , testGroup "Select"
    [ testParseSelect "SELECT name FROM users"
      (Simple select
       { from = [ J (Table "users") ]
       , targetList = [ Column (CRef "name") Nothing ]
       })
    , testParse "SELECT name, email FROM users"
      (QS (Simple select
       { from = [ J (Table "users") ]
       , targetList = [ Column (CRef "name") Nothing, Column (CRef "email") Nothing ]
       }))
    , testParse "SELECT name, email FROM users WHERE name = 'Daniel'"
      (QS (Simple select
       { from = [ J (Table "users") ]
       , targetList = [ Column (CRef "name") Nothing, Column (CRef "email") Nothing ]
       , whereClause = Just (BinOp Eq (CRef "name") (Lit (T "Daniel")))
       }))
    , testParse "SELECT name, email FROM users WHERE name = 'Daniel' OR name = 'Bergey'"
      (QS (Simple select
       { from = [ J (Table "users") ]
       , targetList = [ Column (CRef "name") Nothing, Column (CRef "email") Nothing ]
       , whereClause = Just (BinOp Or (BinOp Eq (CRef "name") (Lit (T "Daniel"))) (BinOp Eq (CRef "name") (Lit (T "Bergey"))))
       }))
    , testParse "SELECT name FROM users WHERE age = 35"
        -- We currently parse integers & decimals all to Double
        -- Just test that both parser rules work
      (QS (Simple select
       { from = [ J (Table "users") ]
       , targetList = [ Column (CRef "name") Nothing ]
       , whereClause = Just (BinOp Eq (CRef "age") (Lit (I 35)))
       }))
    , testParse "SELECT name FROM users WHERE age = 35.5"
      (QS (Simple select
       { from = [ J (Table "users") ]
       , targetList = [ Column (CRef "name") Nothing ]
       , whereClause = Just (BinOp Eq (CRef "age") (Lit (F 35.5)))
       }))
    , testParse "SELECT foo FROM bar WHERE baz > -2"
      (QS (Simple select
       { from = [ J (Table "bar") ]
       , targetList = [ Column (CRef "foo") Nothing ]
       , whereClause = Just (BinOp GT (CRef "baz") (Unary Negate (Lit (I 2))))
       }))
    , testParse "SELECT foo FROM bar WHERE baz = 2e-2"
      (QS (Simple select
       { from = [ J (Table "bar") ]
       , targetList = [ Column (CRef "foo") Nothing ]
       , whereClause = Just (BinOp Eq (CRef "baz") (Lit (F 0.02)))
       }))
    , testParse "SELECT foo FROM bar WHERE baz = 2E-2"
      (QS (Simple select
       { from = [ J (Table "bar") ]
       , targetList = [ Column (CRef "foo") Nothing ]
       , whereClause = Just (BinOp Eq (CRef "baz") (Lit (F 0.02)))
       }))
    , testParse "SELECT * FROM foobar"
      (QS (Simple select
       { from = [ J (Table "foobar") ]
       , targetList = [ Star ]
       }))
    , testParse "SELECT * FROM foo ORDER BY bar"
      (QS (S (Simple select { from = [J (Table "foo") ], targetList = [Star] })
          selectOptions { sortBy = [ SortBy (CRef "bar") (SortOrder DefaultSortOrder) NullsOrderDefault ] }))
    , testParse "SELECT * FROM foo ORDER BY bar DESC"
      (QS (S (Simple select { from = [J (Table "foo") ], targetList = [Star] })
          selectOptions { sortBy = [ SortBy (CRef "bar") (SortOrder Descending) NullsOrderDefault ] }))
    , testParse "SELECT * FROM foo ORDER BY bar NULLS FIRST"
      (QS (S (Simple select { from = [J (Table "foo") ], targetList = [Star] })
          selectOptions { sortBy = [ SortBy (CRef "bar") (SortOrder DefaultSortOrder) NullsFirst ] }))
    , testParse "SELECT * FROM foo ORDER BY bar ASC NULLS LAST"
      (QS (S (Simple select { from = [J (Table "foo") ], targetList = [Star] })
          selectOptions { sortBy = [ SortBy (CRef "bar") (SortOrder Ascending) NullsLast ] }))
    , testParse "SELECT * FROM foobar LIMIT 5"
      (QS (S (Simple select { from = [J (Table "foobar") ], targetList = [Star] })
          selectOptions { limit = Just (Lit (I 5)) }))
    , testParse "SELECT * FROM foobar LIMIT 5 OFFSET 5"
      (QS (S (Simple select { from = [J (Table "foobar") ], targetList = [Star] })
          selectOptions { limit = Just (Lit (I 5)), offset = Just (Lit (I 5)) }))
    , testParse "SELECT * FROM foobar OFFSET 25"
      (QS (S (Simple select { from = [J (Table "foobar") ], targetList = [Star] })
          selectOptions { offset = Just (Lit (I 25)) }))
    , testParse "TABLE foo"
      (QS (Simple select { from = [J (Table "foo") ], targetList = [Star] }))
    , testParse "SELECT foo FROM bar union SELECT baz FROM quux"
      (QS (Set Union Distinct
              (Simple select { from = [ J (Table "bar") ], targetList = [ Column (CRef "foo") Nothing ] })
              (Simple select { from = [ J (Table "quux") ], targetList = [ Column (CRef "baz") Nothing ] })))
    , testParse "SELECT foo FROM bar Intersect ALL SELECT baz FROM quux"
      (QS (Set Intersect All
              (Simple select { from = [ J (Table "bar") ], targetList = [ Column (CRef "foo") Nothing ] })
              (Simple select { from = [ J (Table "quux") ], targetList = [ Column (CRef "baz") Nothing ] })))
    , testParse "SELECT foo FROM bar EXCEPT SELECT baz FROM quux"
      (QS (Set Except Distinct
              (Simple select { from = [ J (Table "bar") ], targetList = [ Column (CRef "foo") Nothing ] })
              (Simple select { from = [ J (Table "quux") ], targetList = [ Column (CRef "baz") Nothing ] })))
    , testParse "SELECT * FROM foo CROSS JOIN bar"
      (QS (Simple select { from = [ J (CrossJoin (J (Table "foo")) (J (Table "bar"))) ]
                         , targetList = [Star] }))
    , testParse "SELECT * FROM foo JOIN bar ON foo.f = bar.b"
      (QS (Simple select { from = [ J (Join Inner (On (BinOp Eq (Indirection (CRef "foo") ("f" :| [])) (Indirection (CRef "bar") ("b" :| [])))) (J (Table "foo")) (J (Table "bar"))) ]
                         , targetList = [Star]}))
    , testParse "SELECT * FROM foo JOIN bar USING (f, b)"
      (QS (Simple select { from = [ J (Join Inner (Using ["f", "b"]) (J (Table "foo")) (J (Table "bar"))) ]
                         , targetList = [Star]}))
    , testParse "SELECT * FROM foo LEFT JOIN bar ON bar.foo = f.id"
      (QS (Simple select { from = [ J (Join LeftJoin (On (BinOp Eq (Indirection (CRef "bar") ("foo" :| [])) (Indirection (CRef "f") ("id" :| [])))) (J (Table "foo")) (J (Table "bar"))) ]
                         , targetList = [Star]}))
    , testParse "SELECT * FROM foo NATURAL JOIN bar"
      (QS (Simple select { from = [ J (Join Inner Natural (J (Table "foo")) (J (Table "bar"))) ]
                         , targetList = [Star]}))
    , testParse "SELECT DISTINCT foo FROM bar"
      (QS (Simple select { from = [ J (Table "bar") ], targetList = [ Column (CRef "foo") Nothing ], distinct = Just DistinctAll }))
    , testParse "SELECT DISTINCT ON (foo) * FROM bar"
      (QS (Simple select { from = [ J (Table "bar") ], targetList = [ Star ], distinct = Just (DistinctOn (CRef "foo" :| [] )) } ))
    , testParse "SELECT DISTINCT ON (foo, baz) * FROM bar"
      (QS (Simple select { from = [ J (Table "bar") ], targetList = [ Star ], distinct = Just (DistinctOn (CRef "foo" :| [CRef "baz" ] )) } ))
    , testParse "SELECT * FROM (SELECT foo FROM bar) AS baz"
      (QS (Simple select { targetList = [Star]
                         , from = [ SubSelect (Simple select { targetList = [ Column (CRef "foo") Nothing ], from = [ J (Table "bar") ] }) (Alias "baz" []) ] } ))
  , testParse "SELECT * FROM foo GROUP BY bar"
    (QS (Simple select { targetList = [Star], from = [J (Table "foo")]
                       , groupBy = [ CRef "bar" ] } ))
  -- TODO add count(*) or something, once we can parse functions
  , testParse "SELECT bar FROM foo GROUP BY bar HAVING count > 1"
    (QS (Simple select { targetList = [ Column (CRef "bar") Nothing ]
                       , from = [J (Table "foo")]
                       , groupBy = [ CRef "bar" ]
                       , having = Just (BinOp GT (CRef "count") (Lit (I 1))) } ))
  , testParse "SELECT * FROM foo WHERE bar LIKE '%'"
    (QS (Simple select { targetList = [Star], from = [J (Table "foo")]
                       , whereClause = Just (L (like Like (CRef "bar") (Lit (T "%")))) }))
  , testParse "SELECT * FROM foo WHERE bar NOT LIKE '%'"
    (QS (Simple select { targetList = [Star], from = [J (Table "foo")]
                       , whereClause = Just (L (like Like (CRef "bar") (Lit (T "%"))) { invert = True }) }))
  , testParse "SELECT * FROM foo WHERE bar SIMILAR TO '%'"
    (QS (Simple select { targetList = [Star], from = [J (Table "foo")]
                       , whereClause = Just (L (like Similar (CRef "bar") (Lit (T "%")))) }))
  , testParse "SELECT coalesce(foo, bar) FROM baz"
    ( QS (Simple select
          { targetList = [ Column (Fun $ fapp1 "coalesce" [CRef "foo", CRef "bar"]) Nothing ]
          , from = [J (Table "baz")] }))
  , testParse "SELECT greatest(foo, bar) FROM baz"
    ( QS (Simple select
          { targetList = [ Column (Fun $ fapp1 "greatest" [CRef "foo", CRef "bar"]) Nothing ]
          , from = [J (Table "baz")] }))
  , testParse "SELECT least(foo, bar) FROM baz"
    ( QS (Simple select
          { targetList = [ Column (Fun $ fapp1 "least" [CRef "foo", CRef "bar"]) Nothing ]
          , from = [J (Table "baz")] }))
  , testParse "SELECT 1 IS NULL"
    ( QS (Simple select { targetList = [ Column (Unary IsNull (Lit (I 1))) Nothing ] } ))
  , testParse "SELECT 1 ISNULL"
    ( QS (Simple select { targetList = [ Column (Unary IsNull (Lit (I 1))) Nothing ] } ))
  , testParse "SELECT CASE WHEN true THEN 1 ELSE 2 END"
    ( QS (Simple select
         { targetList =
           [ Column (Cas (Case
                           { whenClause = [(Lit (B True), Lit (I 1))]
                           , elseClause = Just (Lit (I 2))
                           , implicitArg = Nothing }))
             Nothing ] } ))
  , testParse "SELECT CASE WHEN foo is null THEN 1 WHEN foo > 0 THEN 2 END FROM bar"
    ( QS (Simple select
         { targetList =
           [ Column (Cas (Case
                          { whenClause =
                            [ (Unary IsNull (CRef "foo"), Lit (I 1))
                            , (BinOp GT (CRef "foo") (Lit (I 0)), Lit (I 2))
                            ]
                          , implicitArg = Nothing
                          , elseClause = Nothing }))
             Nothing ]
         , from = [ J (Table "bar") ] } ))
  , testParse "SELECT CASE WHEN foo is null THEN 1 WHEN foo > 0 THEN 2 ELSE 3 END FROM bar"
    ( QS (Simple select
         { targetList =
           [ Column (Cas (Case
                          { whenClause =
                            [ (Unary IsNull (CRef "foo"), Lit (I 1))
                            , (BinOp GT (CRef "foo") (Lit (I 0)), Lit (I 2))
                            ]
                          , implicitArg = Nothing
                          , elseClause = Just (Lit (I 3))}))
             Nothing ]
         , from = [ J (Table "bar") ] } ))
  , testParse "SELECT CASE foo WHEN 'bar' THEN 1 ELSE 0 END FROM quux"
    ( QS (Simple select
         { targetList =
           [ Column (Cas (Case
                          { implicitArg = Just (CRef "foo")
                          , whenClause = [ (Lit (T "bar"), Lit (I 1)) ]
                          , elseClause = Just (Lit (I 0))}))
             Nothing ]
         , from = [ J (Table "quux") ] } ))
  , testParse "SELECT CASE foo WHEN 'bar' THEN 1 WHEN 'baz' THEN 2 ELSE 0 END FROM quux"
    ( QS (Simple select
          { targetList =
           [ Column (Cas (Case
                          { implicitArg = Just (CRef "foo")
                          , whenClause =
                            [ (Lit (T "bar"), Lit (I 1))
                            , (Lit (T "baz"), Lit (I 2))
                            ]
                          , elseClause = Just (Lit (I 0))}))
             Nothing ]
         , from = [ J (Table "quux") ] } ))
  , testParse "WITH regional_sales AS (\
\    SELECT region, SUM(amount) AS total_sales\
\    FROM orders\
\    GROUP BY region\
\)\
\SELECT region,\
\       product,\
\       SUM(quantity) AS product_units,\
\       SUM(amount) AS product_sales \
\FROM orders \
\WHERE region = 'Lemuria' \
\GROUP BY region, product;"
  ( QS (S (Simple select
           { targetList = [ Column (CRef "region") Nothing
                          , Column (CRef "product") Nothing
                          , Column (Fun $ fapp1 "SUM" [ CRef "quantity" ]) (Just "product_units")
                          , Column (Fun $ fapp1 "SUM" [ CRef "amount" ]) (Just "product_sales")
                          ]
           , from = [ J (Table "orders") ]
           , whereClause = Just ( BinOp Eq (CRef "region") (Lit (T "Lemuria")) )
           , groupBy = [ CRef "region", CRef "product" ]
           })
          selectOptions
            { withClause = Just With
              { recursive = NotRecursive
              , commonTables = [ CommonTableExpr
                               { name = "regional_sales"
                               , aliases = []
                               , materialized = MaterializeDefault
                               , query = QS (Simple select
                                            { targetList =
                                              [ Column (CRef "region") Nothing
                                              , Column (Fun $ fapp1 "SUM" [ CRef "amount" ]) (Just "total_sales")
                                              ]
                                            , from = [ J (Table "orders") ]
                                            , groupBy = [ CRef "region" ]
                                            })
                               } ]
              }
            }))
  , testParse "WITH RECURSIVE t(n) AS (\
\    VALUES (1)\
\  UNION ALL\
\    SELECT n+1 FROM t WHERE n < 100\
\ )\
\ SELECT sum(n) FROM t;"
  ( QS (S (Simple select
          { targetList = [ Column (Fun $ fapp1 "sum" [ CRef "n" ]) Nothing ]
          , from = [ J (Table "t") ]
          })
         selectOptions
            { withClause = Just With
              { recursive = Recursive
              , commonTables = [ CommonTableExpr
                               { name = "t"
                               , aliases = [ "n" ]
                               , materialized = MaterializeDefault
                               , query = QS (Set Union All
                                             (SelectValues ((Lit (I 1) :| []) :| []))
                                             ( Simple select
                                               { targetList = [ Column (BinOp Add (CRef "n") (Lit (I 1))) Nothing ]
                                               , from = [ J (Table "t") ]
                                               , whereClause = Just (BinOp LT (CRef "n") (Lit (I 100)))
                                               }))
                               } ]
              }
            }))
    ]
  , testGroup "locking"
    [ testParseSelect  "SELECT * FROM j FOR UPDATE NOWAIT"
      (S (Simple select { targetList = [Star], from = [J (Table "j")] }) selectOptions { locking = [Locking ForUpdate [] LockWaitError] })
    , testParseSelect  "SELECT * FROM j UNION ALL SELECT * FROM d"
      ( Set Union All
        (Simple select { targetList = [Star] , from = [ J (Table "j") ]})
        (Simple select { targetList = [Star] , from = [ J (Table "d") ]})
      )
    , canParse  "SELECT * FROM j UNION ALL SELECT * FROM d UNION ALL (SELECT * FROM z UNION ALL SELECT * FROM c) FOR UPDATE NOWAIT"
    ]
  ]

testParse :: TestName -> Statement -> TestTree
testParse queryStr expected = testCase queryStr $
    assertEqual "testParse" (Right expected) (parseStatement "<testcase>" queryStr)

testParseSelect :: TestName -> SelectStmt -> TestTree
testParseSelect queryStr expected = testCase queryStr $
    assertEqual "testParseSelect" (Right expected) (parseSelect "<testcase>" queryStr)

testParseExpr :: TestName -> Expr -> TestTree
testParseExpr queryStr expected = testCase queryStr $
    assertEqual "testParseExpr" (Right expected) (parseExpr "<testcase>" queryStr)

testLex :: TestName -> [L.Token] -> TestTree
testLex queryStr expected = testCase queryStr $
    assertEqual "testLex" (Right (expected ++ [L.EOF])) (L.testLex queryStr)

canParse :: TestName -> TestTree
canParse queryStr = testCase queryStr $
  assertBool "canParse" (isRight (parseSelect "<testcase>" queryStr))