WIP: converting to Hasql

This commit is contained in:
Joe Nelson
2014-12-06 17:42:19 -08:00
parent b74b91b1c0
commit 8ebfbccd08
5 changed files with 79 additions and 55 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ executable dbapi
default-extensions: OverloadedStrings default-extensions: OverloadedStrings
other-extensions: QuasiQuotes other-extensions: QuasiQuotes
build-depends: base >=4.6 && <5 build-depends: base >=4.6 && <5
, postgresql-simple >= 0.4.7.0 , hasql, hasql-backend, hasql-postgres
, warp >= 3.0.2, wai >= 3.0.1 , warp >= 3.0.2, wai >= 3.0.1
, wai-extra, wai-cors , wai-extra, wai-cors
, wai-middleware-static >= 0.6.0 , wai-middleware-static >= 0.6.0
@@ -55,7 +55,7 @@ Test-Suite spec
Other-Modules: App, Config, Spec, SpecHelper Other-Modules: App, Config, Spec, SpecHelper
Build-Depends: base, hspec2, QuickCheck Build-Depends: base, hspec2, QuickCheck
, hspec-wai >= 0.5.0, hspec-wai-json , hspec-wai >= 0.5.0, hspec-wai-json
, postgresql-simple >= 0.4.7.0 , hasql, hasql-backend, hasql-postgres
, warp >= 3.0.2, wai >= 3.0.1 , warp >= 3.0.2, wai >= 3.0.1
, HTTP, convertible , HTTP, convertible
, case-insensitive , case-insensitive
+2
View File
@@ -24,6 +24,8 @@ import Network.Wai
import Data.Aeson import Data.Aeson
import Database.PostgreSQL.Simple import Database.PostgreSQL.Simple
import qualified Hasql as H
import qualified Hasql.Postgres as H
import PgQuery import PgQuery
import RangeQuery import RangeQuery
+2 -1
View File
@@ -5,7 +5,8 @@ import qualified Data.ByteString.Char8 as BS
import Control.Monad (mzero) import Control.Monad (mzero)
import Control.Applicative ( (<*>), (<$>) ) import Control.Applicative ( (<*>), (<$>) )
import Crypto.BCrypt import Crypto.BCrypt
import Database.PostgreSQL.Simple import qualified Hasql as H
import qualified Hasql.Postgres as H
import GHC.Int import GHC.Int
data AuthUser = AuthUser { data AuthUser = AuthUser {
+2 -1
View File
@@ -16,7 +16,8 @@ import Network.Wai.Middleware.Static (staticPolicy, only)
import Data.Pool(createPool, destroyAllResources) import Data.Pool(createPool, destroyAllResources)
import Data.List (intercalate) import Data.List (intercalate)
import Data.Version (versionBranch) import Data.Version (versionBranch)
import Database.PostgreSQL.Simple import qualified Hasql as H
import qualified Hasql.Postgres as H
import Options.Applicative hiding (columns) import Options.Applicative hiding (columns)
import Config (AppConfig(..), argParser, corsPolicy) import Config (AppConfig(..), argParser, corsPolicy)
+71 -51
View File
@@ -1,9 +1,12 @@
module PgQuery where module PgQuery where
import RangeQuery import RangeQuery
import Database.PostgreSQL.Simple import qualified Hasql as H
import Database.PostgreSQL.Simple.ToField import qualified Hasql.Postgres as H
import Database.PostgreSQL.Simple.Types (Query(..)) import qualified Hasql.Backend as H
import Data.Text hiding (map)
import Text.Regex.TDFA
import Text.Regex.TDFA.Text
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import Data.ByteString.Search (split) import Data.ByteString.Search (split)
import qualified Network.HTTP.Types.URI as Net import qualified Network.HTTP.Types.URI as Net
@@ -16,8 +19,7 @@ import Data.String.Conversions (cs)
import Data.Aeson (Value(..), encode) import Data.Aeson (Value(..), encode)
import qualified Data.List as L import qualified Data.List as L
type CompleteQuery = (Query, [Action]) type StatementT = H.Statement H.Postgres -> H.Statement H.Postgres
type CompleteQueryT = CompleteQuery -> CompleteQuery
data QualifiedTable = QualifiedTable { data QualifiedTable = QualifiedTable {
qtSchema :: BS.ByteString qtSchema :: BS.ByteString
, qtName :: BS.ByteString , qtName :: BS.ByteString
@@ -28,14 +30,14 @@ data OrderTerm = OrderTerm {
, otDirection :: BS.ByteString , otDirection :: BS.ByteString
} }
limitT :: Maybe NonnegRange -> CompleteQueryT limitT :: Maybe NonnegRange -> StatementT
limitT r q = limitT r q =
q <> (" LIMIT ? OFFSET ? ", [Plain (fromByteString limit), toField offset]) q <> (" LIMIT " <> limit <> " OFFSET " <> (cs . show) offset <> " ", [])
where where
limit = cs $ fromMaybe "ALL" $ show . rangeLimit <$> r limit = cs $ fromMaybe "ALL" $ show . rangeLimit <$> r
offset = fromMaybe 0 $ rangeOffset <$> r offset = fromMaybe 0 $ rangeOffset <$> r
whereT :: Net.Query -> CompleteQueryT whereT :: Net.Query -> StatementT
whereT params q = whereT params q =
if L.null params if L.null params
then q then q
@@ -44,59 +46,54 @@ whereT params q =
cols = [ col | col <- params, fst col `notElem` ["order"] ] cols = [ col | col <- params, fst col `notElem` ["order"] ]
conjunction = mconcat $ L.intersperse andq (map wherePred cols) conjunction = mconcat $ L.intersperse andq (map wherePred cols)
orderT :: [OrderTerm] -> CompleteQueryT orderT :: [OrderTerm] -> StatementT
orderT ts q = orderT ts q =
if L.null ts if L.null ts
then q then q
else q <> (" order by ",[]) <> clause else q <> (" order by ",[]) <> clause
where where
clause = mconcat $ L.intersperse commaq (map queryTerm ts) clause = mconcat $ L.intersperse commaq (map queryTerm ts)
queryTerm :: OrderTerm -> CompleteQuery queryTerm :: OrderTerm -> H.Statement H.Postgres
queryTerm t = queryTerm t = (" " <> (pgFmtIdent $ otTerm t) <> " "
(" ? ? ", <> otDirection t <> " "
[EscapeIdentifier (otTerm t), Plain (fromByteString $ otDirection t)] , [])
)
parentheticT :: CompleteQueryT parentheticT :: StatementT
parentheticT (sql, params) = parentheticT (sql, params) =
(" (" <> sql <> ") ", params) (" (" <> sql <> ") ", params)
iffNotT :: CompleteQuery -> CompleteQueryT iffNotT :: H.Statement H.Postgres -> StatementT
iffNotT (aq, ap) (bq, bp) = iffNotT (aq, ap) (bq, bp) =
("WITH aaa AS (" <> aq <> " returning *) " <> ("WITH aaa AS (" <> aq <> " returning *) " <>
bq <> "WHERE NOT EXISTS (SELECT * FROM aaa)" bq <> "WHERE NOT EXISTS (SELECT * FROM aaa)"
, ap ++ bp , ap ++ bp
) )
countRows :: QualifiedTable -> CompleteQuery countRows :: QualifiedTable -> H.Statement H.Postgres
countRows t = countRows t =
("select count(1) from ?.?", ("select count(1) from " <> fromQt t, [])
[EscapeIdentifier (qtSchema t), EscapeIdentifier (qtName t)])
asJsonWithCount :: CompleteQueryT asJsonWithCount :: StatementT
asJsonWithCount (sql, params) = ( asJsonWithCount (sql, params) = (
"count(t), array_to_json(array_agg(row_to_json(t)))::character varying from (" <> sql <> ") t" "count(t), array_to_json(array_agg(row_to_json(t)))::character varying from (" <> sql <> ") t"
, params , params
) )
selectStar :: QualifiedTable -> CompleteQuery selectStar :: QualifiedTable -> H.Statement H.Postgres
selectStar t = selectStar t =
("select * from ?.?", ("select * from " <> fromQt t, [])
[EscapeIdentifier (qtSchema t), EscapeIdentifier (qtName t)])
insertInto :: QualifiedTable -> [BS.ByteString] -> [Value] -> insertInto :: QualifiedTable -> [BS.ByteString] -> [Value] ->
CompleteQuery H.Statement H.Postgres
insertInto t [] _ = insertInto t [] _ =
("insert into ?.? default values returning *", ("insert into " <> fromQt t <> " default values returning *", [])
[EscapeIdentifier (qtSchema t), EscapeIdentifier (qtName t)])
insertInto t cols vals = insertInto t cols vals =
("insert into ?.? (" <> ("insert into " <> fromQt t <> " (" <>
Query (BS.intercalate ", " (map (const "?") cols)) <> BS.intercalate ", " (map pgFmtIdent cols) <>
") values (" <> ") values (" <>
Query (BS.intercalate ", " (map (const "?") vals)) <> BS.intercalate ", " (map (const "?") vals) <>
") returning *" ") returning *"
, [EscapeIdentifier (qtSchema t), EscapeIdentifier (qtName t)] , vals
++ map EscapeIdentifier cols ++ map (Escape . rawJsonValue) vals
) )
rawJsonValue :: Value -> BS.ByteString rawJsonValue :: Value -> BS.ByteString
@@ -104,41 +101,40 @@ rawJsonValue (String s) = cs s
rawJsonValue v = cs $ encode v rawJsonValue v = cs $ encode v
update :: QualifiedTable -> [BS.ByteString] -> [Value] -> update :: QualifiedTable -> [BS.ByteString] -> [Value] ->
CompleteQuery H.Statement H.Postgres
update t cols vals = update t cols vals =
("update ?.? set (" <> ("update " <> fromQt t <> " set (" <>
Query (BS.intercalate ", " (map (const "?") cols)) <> BS.intercalate ", " (map pgFmtIdent cols) <>
") = (" <> ") = (" <>
Query (BS.intercalate ", " (map (const "?") vals)) <> ")" BS.intercalate ", " (map (const "?") vals) <> ")"
, [EscapeIdentifier (qtSchema t), EscapeIdentifier (qtName t)] , vals
++ map EscapeIdentifier cols ++ map toField vals
) )
wherePred :: Net.QueryItem -> CompleteQuery wherePred :: Net.QueryItem -> H.Statement H.Postgres
wherePred (col, predicate) = wherePred (col, predicate) =
(" ? ? ? ", [EscapeIdentifier col, Plain op, toField value]) (" " <> pgFmtIdent col <> " " <> op <> " ? ", [value])
where where
opCode:rest = BS.split '.' $ fromMaybe "." predicate opCode:rest = BS.split '.' $ fromMaybe "." predicate
value = BS.intercalate "." rest value = BS.intercalate "." rest
op = fromByteString $ case opCode of op = case opCode of
"eq" -> "=" "eq" -> "="
"gt" -> ">" "gt" -> ">"
"lt" -> "<" "lt" -> "<"
"gte" -> ">=" "gte" -> ">="
"lte" -> "<=" "lte" -> "<="
"neq" -> "<>" "neq" -> "<>"
_ -> "=" _ -> "="
orderParse :: Net.Query -> [OrderTerm] orderParse :: Net.Query -> [OrderTerm]
orderParse q = orderParse q =
mapMaybe orderParseTerm . split "," $ cs order mapMaybe orderParseTerm . BS.split "," $ cs order
where where
order = fromMaybe "" $ join (lookup "order" q) order = fromMaybe "" $ join (lookup "order" q)
orderParseTerm :: BS.ByteString -> Maybe OrderTerm orderParseTerm :: BS.ByteString -> Maybe OrderTerm
orderParseTerm s = orderParseTerm s =
case split "." s of case BS.split "." s of
[d,c] -> [d,c] ->
if d `elem` ["asc", "desc"] if d `elem` ["asc", "desc"]
then Just $ OrderTerm (cs c) $ then Just $ OrderTerm (cs c) $
@@ -146,8 +142,32 @@ orderParseTerm s =
else Nothing else Nothing
_ -> Nothing _ -> Nothing
commaq :: CompleteQuery commaq :: H.Statement H.Postgres
commaq = (", ", []) commaq = (", ", [])
andq :: CompleteQuery andq :: H.Statement H.Postgres
andq = (" and ", []) andq = (" and ", [])
pgFmtIdent :: BS.ByteString -> BS.ByteString
pgFmtIdent x =
let escaped = replace "\"" "\"\"" (trimNullChars $ cs x) in
cs $ if escaped =~ danger
then "\"" <> escaped <> "\""
else escaped
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: BS.ByteString
pgFmtLit :: Text -> Text
pgFmtLit x =
let trimmed = trimNullChars x
escaped = "'" <> replace "'" "''" trimmed <> "'"
slashed = replace "\\" "\\\\" escaped in
if escaped =~ ("\\\\" :: Text)
then "E" <> slashed
else slashed
trimNullChars :: Text -> Text
trimNullChars = Data.Text.takeWhile (/= '\x0')
fromQt :: QualifiedTable -> BS.ByteString
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)