packages feed

pg-query-0.0.1.0: test/PgQuery/ParamsSpec.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -fwarn-unused-binds -fwarn-unused-imports #-}

module PgQuery.ParamsSpec (spec) where

import Control.Lens
import Data.Either (Either (Left, Right))
import Data.Eq (Eq)
import Data.Foldable (concatMap)
import Data.Function (($), (.))
import Data.Functor (fmap)
import Data.Int (Int)
import Data.List (intercalate)
import Data.Maybe (Maybe (Just, Nothing), maybeToList)
import Data.String (String)
import Data.Text (unpack)
import GHC.Base (error)
import GHC.Real (fromIntegral)
import GHC.Show (Show)
import PgQuery hiding (String, view)
import Test.Hspec
  ( Spec,
    describe,
    it,
  )
import Test.Hspec.Expectations.Pretty (shouldBe)

spec :: Spec
spec = do
  describe "DML can be parsed" $ do
    it "param references can be determined" $ do
      eResult <- parseSql "select u.name, u.address from users u where u.id = $1 or u.email = $2;"
      case eResult of
        Left err ->
          error err
        Right result -> do
          let nodes = toListOf (stmts . traverse . stmt . selectStmt . whereClause) result
              params = concatMap nodeToColumnParam nodes

          params `shouldBe` [(1, "u.id"), (2, "u.email")]

nodeToColumnParam :: Node -> [(Int, String)]
nodeToColumnParam subNode =
  case view maybe'node subNode of
    Just (Node'BoolExpr expr) ->
      let boolArgs = view args expr
       in concatMap nodeToColumnParam boolArgs
    Just (Node'AExpr expr) ->
      maybeToList $ aExprToColumnParam expr
    _ -> []

aExprToColumnParam :: A_Expr -> Maybe (Int, String)
aExprToColumnParam aExpr =
  case (preview lexpr aExpr, preview rexpr aExpr) of
    (Just leftNode, Just rightNode) ->
      case (toRef leftNode, toRef rightNode) of
        (Just (Column str), Just (Param param)) -> Just (param, str)
        _ ->
          case (toRef rightNode, toRef leftNode) of
            (Just (Column str), Just (Param param)) -> Just (param, str)
            _ -> Nothing
    _ -> Nothing

data Ref
  = Column String
  | Param Int
  deriving (Show, Eq)

toRef :: Node -> Maybe Ref
toRef subNode =
  case view maybe'node subNode of
    Just (Node'ColumnRef expr) ->
      let allFields = view fields expr
          combined = intercalate "." (fmap fieldToString allFields)
       in Just $ Column combined
    Just (Node'ParamRef ref) ->
      Just . Param $ fromIntegral (view number ref)
    _ -> Nothing
  where
    fieldToString :: Node -> String
    fieldToString subFieldNode =
      case view maybe'node subFieldNode of
        Just (Node'String str) -> unpack $ view sval str
        _ -> ""

-- Reference for:
--   select u.name, u.address from users u where u.id = $1 or u.email = $2;
--
-- [{target_list { res_target { val { column_ref { fields { string { sval: "u" } } fields { string { sval: "name" } } location: 7 } } location: 7 } }
--   target_list { res_target { val { column_ref { fields { string { sval: "u" } } fields { string { sval: "address" } } location: 15 } } location: 15 } }
--   from_clause { range_var { relname: "users" inh: true relpersistence: "p" alias { aliasname: "u" } location: 30 } }
--   where_clause {
--     bool_expr { boolop: OR_EXPR
--         args {
--           a_expr {
--           kind: AEXPR_OP name { string { sval: "=" } }
--           lexpr { column_ref { fields { string { sval: "u" } } fields { string { sval: "id" } } location: 44 } }
--           rexpr { param_ref { number: 1 location: 51 } }
--           location: 49
--           }
--         }
--         args {
--           a_expr {
--            kind: AEXPR_OP name { string { sval: "=" } }
--            lexpr { column_ref { fields { string { sval: "u" } } fields { string { sval: "email" } } location: 57 } }
--            rexpr { param_ref { number: 2 location: 67 } }
--            location: 65
--          }
--        }
--       location: 52
--     }
--   }
--   limit_option: LIMIT_OPTION_DEFAULT op: SETOP_NONE}
--   ]