Merge branch 'like'

Fixes #132
This commit is contained in:
Joe Nelson
2015-02-15 17:37:40 -08:00
9 changed files with 81 additions and 55 deletions
+1 -1
View File
@@ -6,5 +6,5 @@ machine:
version: 7.8.3 version: 7.8.3
test: test:
post: post:
- cabal exec hlint src/*.hs test/**/*.hs - cabal exec hlint -- -X QuasiQuotes src/*.hs test/**/*.hs
- cabal exec packdeps postgrest.cabal - cabal exec packdeps postgrest.cabal
+2 -4
View File
@@ -17,8 +17,7 @@ executable postgrest
main-is: Main.hs main-is: Main.hs
ghc-options: -Wall -W -O2 ghc-options: -Wall -W -O2
default-language: Haskell2010 default-language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
other-extensions: QuasiQuotes
build-depends: base >=4.6 && <5 build-depends: base >=4.6 && <5
, hasql == 0.7.*, hasql-backend , hasql == 0.7.*, hasql-backend
, hasql-postgres == 0.10.* , hasql-postgres == 0.10.*
@@ -57,8 +56,7 @@ executable postgrest
Test-Suite spec Test-Suite spec
Type: exitcode-stdio-1.0 Type: exitcode-stdio-1.0
Default-Language: Haskell2010 Default-Language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
other-extensions: QuasiQuotes
Hs-Source-Dirs: test, src Hs-Source-Dirs: test, src
ghc-options: -Wall -W -Werror ghc-options: -Wall -W -Werror
Main-Is: Main.hs Main-Is: Main.hs
+34 -29
View File
@@ -9,7 +9,7 @@ import qualified Hasql as H
import qualified Hasql.Postgres as P import qualified Hasql.Postgres as P
import qualified Hasql.Backend as B import qualified Hasql.Backend as B
import Data.Text hiding (map, empty) import qualified Data.Text as T
import Text.Regex.TDFA ( (=~) ) import Text.Regex.TDFA ( (=~) )
import Text.Regex.TDFA.Text () import Text.Regex.TDFA.Text ()
import qualified Network.HTTP.Types.URI as Net import qualified Network.HTTP.Types.URI as Net
@@ -32,12 +32,12 @@ instance Monoid PStmt where
type StatementT = PStmt -> PStmt type StatementT = PStmt -> PStmt
data QualifiedTable = QualifiedTable { data QualifiedTable = QualifiedTable {
qtSchema :: Text qtSchema :: T.Text
, qtName :: Text , qtName :: T.Text
} deriving (Show) } deriving (Show)
data OrderTerm = OrderTerm { data OrderTerm = OrderTerm {
otTerm :: Text otTerm :: T.Text
, otDirection :: BS.ByteString , otDirection :: BS.ByteString
} }
@@ -106,33 +106,33 @@ returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
deleteFrom :: QualifiedTable -> PStmt deleteFrom :: QualifiedTable -> PStmt
deleteFrom t = B.Stmt ("delete from " <> fromQt t) empty True deleteFrom t = B.Stmt ("delete from " <> fromQt t) empty True
insertInto :: QualifiedTable -> [Text] -> [JSON.Value] -> PStmt insertInto :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
insertInto t [] _ = B.Stmt insertInto t [] _ = B.Stmt
("insert into " <> fromQt t <> " default values returning *") empty True ("insert into " <> fromQt t <> " default values returning *") empty True
insertInto t cols vals = B.Stmt insertInto t cols vals = B.Stmt
("insert into " <> fromQt t <> " (" <> ("insert into " <> fromQt t <> " (" <>
intercalate ", " (map pgFmtIdent cols) <> T.intercalate ", " (map pgFmtIdent cols) <>
") values (" ") values ("
<> intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals) <> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
<> ") returning row_to_json(" <> fromQt t <> ".*)") <> ") returning row_to_json(" <> fromQt t <> ".*)")
empty True empty True
insertSelect :: QualifiedTable -> [Text] -> [JSON.Value] -> PStmt insertSelect :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
insertSelect t [] _ = B.Stmt insertSelect t [] _ = B.Stmt
("insert into " <> fromQt t <> " default values returning *") empty True ("insert into " <> fromQt t <> " default values returning *") empty True
insertSelect t cols vals = B.Stmt insertSelect t cols vals = B.Stmt
("insert into " <> fromQt t <> " (" ("insert into " <> fromQt t <> " ("
<> intercalate ", " (map pgFmtIdent cols) <> T.intercalate ", " (map pgFmtIdent cols)
<> ") select " <> ") select "
<> intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)) <> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals))
empty True empty True
update :: QualifiedTable -> [Text] -> [JSON.Value] -> PStmt update :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
update t cols vals = B.Stmt update t cols vals = B.Stmt
("update " <> fromQt t <> " set (" ("update " <> fromQt t <> " set ("
<> intercalate ", " (map pgFmtIdent cols) <> T.intercalate ", " (map pgFmtIdent cols)
<> ") = (" <> ") = ("
<> intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals) <> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
<> ")") <> ")")
empty True empty True
@@ -142,8 +142,11 @@ wherePred (col, predicate) = B.Stmt
empty True empty True
where where
opCode:rest = split (=='.') $ cs $ fromMaybe "." predicate opCode:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
value = intercalate "." rest unStarredVal = T.intercalate "." rest
star c = if c == '*' then '%' else c
value = if opCode == "like" || opCode == "ilike"
then T.map star unStarredVal else unStarredVal
op = case opCode of op = case opCode of
"eq" -> "=" "eq" -> "="
"gt" -> ">" "gt" -> ">"
@@ -151,17 +154,19 @@ wherePred (col, predicate) = B.Stmt
"gte" -> ">=" "gte" -> ">="
"lte" -> "<=" "lte" -> "<="
"neq" -> "<>" "neq" -> "<>"
"like"-> "like"
"ilike"-> "ilike"
_ -> "=" _ -> "="
orderParse :: Net.Query -> [OrderTerm] orderParse :: Net.Query -> [OrderTerm]
orderParse q = orderParse q =
mapMaybe orderParseTerm . split (==',') $ cs order mapMaybe orderParseTerm . T.split (==',') $ cs order
where where
order = fromMaybe "" $ join (lookup "order" q) order = fromMaybe "" $ join (lookup "order" q)
orderParseTerm :: Text -> Maybe OrderTerm orderParseTerm :: T.Text -> Maybe OrderTerm
orderParseTerm s = orderParseTerm s =
case split (=='.') s of case T.split (=='.') s of
[c,d] -> [c,d] ->
if d `elem` ["asc", "desc"] if d `elem` ["asc", "desc"]
then Just $ OrderTerm c $ then Just $ OrderTerm c $
@@ -175,31 +180,31 @@ commaq = B.Stmt ", " empty True
andq :: PStmt andq :: PStmt
andq = B.Stmt " and " empty True andq = B.Stmt " and " empty True
pgFmtIdent :: Text -> Text pgFmtIdent :: T.Text -> T.Text
pgFmtIdent x = pgFmtIdent x =
let escaped = replace "\"" "\"\"" (trimNullChars $ cs x) in let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
if escaped =~ danger if escaped =~ danger
then "\"" <> escaped <> "\"" then "\"" <> escaped <> "\""
else escaped else escaped
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: Text where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: T.Text
pgFmtLit :: Text -> Text pgFmtLit :: T.Text -> T.Text
pgFmtLit x = pgFmtLit x =
let trimmed = trimNullChars x let trimmed = trimNullChars x
escaped = "'" <> replace "'" "''" trimmed <> "'" escaped = "'" <> T.replace "'" "''" trimmed <> "'"
slashed = replace "\\" "\\\\" escaped in slashed = T.replace "\\" "\\\\" escaped in
cs $ if escaped =~ ("\\\\" :: Text) cs $ if escaped =~ ("\\\\" :: T.Text)
then "E" <> slashed then "E" <> slashed
else slashed else slashed
trimNullChars :: Text -> Text trimNullChars :: T.Text -> T.Text
trimNullChars = Data.Text.takeWhile (/= '\x0') trimNullChars = T.takeWhile (/= '\x0')
fromQt :: QualifiedTable -> Text fromQt :: QualifiedTable -> T.Text
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t) fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
unquoted :: JSON.Value -> Text unquoted :: JSON.Value -> T.Text
unquoted (JSON.String t) = t unquoted (JSON.String t) = t
unquoted (JSON.Number n) = unquoted (JSON.Number n) =
cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
-1
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Feature.AuthSpec where module Feature.AuthSpec where
-- {{{ Imports -- {{{ Imports
-1
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Feature.InsertSpec where module Feature.InsertSpec where
import Test.Hspec import Test.Hspec
+44 -15
View File
@@ -2,39 +2,68 @@ module Feature.QuerySpec where
import Test.Hspec import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Hasql as H
import Hasql.Postgres as H
import Control.Monad (void)
import Data.Text(Text)
import SpecHelper import SpecHelper
testSet :: IO ()
testSet = do
clearTable "items" >> clearTable "no_pk"
createItems 15
pool <- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $ H.tx Nothing $ do
H.unitEx $ insertNoPk "xyyx" "u"
H.unitEx $ insertNoPk "xYYx" "v"
where
insertNoPk :: Text -> Text -> H.Stmt H.Postgres
insertNoPk = [H.stmt|insert into "1".no_pk (a, b) values (?,?)|]
spec :: Spec spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
. afterAll_ (clearTable "items") . around withApp $ do
describe "Querying a nonexistent table" $ describe "Querying a nonexistent table" $
it "causes a 404" $ it "causes a 404" $
get "/faketable" `shouldRespondWith` 404 get "/faketable" `shouldRespondWith` 404
describe "Filtering response" $ describe "Filtering response" $ do
context "column equality" $ it "matches with equality" $
get "/items?id=eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
it "matches the predicate" $ it "matches with like" $ do
get "/items?id=eq.5" get "/no_pk?a=like.*yx" `shouldRespondWith` [json|
`shouldRespondWith` ResponseMatcher { [{"a":"xyyx","b":"u"}]|]
matchBody = Just "[{\"id\":5}]" get "/no_pk?a=like.xy*" `shouldRespondWith` [json|
, matchStatus = 200 [{"a":"xyyx","b":"u"}]|]
, matchHeaders = ["Content-Range" <:> "0-0/1"] get "/no_pk?a=like.*YY*" `shouldRespondWith` [json|
} [{"a":"xYYx","b":"v"}]|]
it "matches with ilike" $ do
get "/no_pk?a=ilike.xy*&order=b.asc" `shouldRespondWith` [json|
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
get "/no_pk?a=ilike.*YY*&order=b.asc" `shouldRespondWith` [json|
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
describe "ordering response" $ do describe "ordering response" $ do
it "by a column asc" $ it "by a column asc" $
get "/items?id=lte.2&order=id.asc" get "/items?id=lte.2&order=id.asc"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":1},{\"id\":2}]" matchBody = Just [json| [{"id":1},{"id":2}] |]
, matchStatus = 200 , matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"] , matchHeaders = ["Content-Range" <:> "0-1/2"]
} }
it "by a column desc" $ it "by a column desc" $
get "/items?id=lte.2&order=id.desc" get "/items?id=lte.2&order=id.desc"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":2},{\"id\":1}]" matchBody = Just [json| [{"id":2},{"id":1}] |]
, matchStatus = 200 , matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"] , matchHeaders = ["Content-Range" <:> "0-1/2"]
} }
@@ -52,9 +81,9 @@ spec = beforeAll (clearTable "items" >> createItems 15)
} }
it "Omits question mark when there are no params" $ it "Omits question mark when there are no params" $
get "/no_pk" get "/simple_pk"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ResponseMatcher {
matchBody = Just "[]" matchBody = Just "[]"
, matchStatus = 200 , matchStatus = 200
, matchHeaders = ["Content-Location" <:> "/no_pk"] , matchHeaders = ["Content-Location" <:> "/simple_pk"]
} }
-1
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
module Feature.StructureSpec where module Feature.StructureSpec where
import Test.Hspec hiding (pendingWith) import Test.Hspec hiding (pendingWith)
-1
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Main where module Main where
import Test.Hspec import Test.Hspec
-2
View File
@@ -1,5 +1,3 @@
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
module SpecHelper where module SpecHelper where
import Network.Wai import Network.Wai