{-# LANGUAGE Strict #-} {-| Module : Database.PostgreSQL.Entity.Internal Copyright : © Clément Delafargue, 2018 Théophile Choutri, 2021 License : MIT Maintainer : theophile@choutri.eu Stability : stable Internal helpers used to implement the high-level API and SQL combinators. You can re-use those building blocks freely to create your own wrappers. -} module Database.PostgreSQL.Entity.Internal ( -- * Helpers isNotNull , isNull , isIn , inParens , quoteName , literal , getTableName , getFieldName , getPrimaryKey , prefix , expandFields , expandQualifiedFields , expandQualifiedFields' , qualifyField , qualifyFields , placeholder , placeholder' , generatePlaceholders , textToQuery , queryToText , intercalateVector , renderSortExpression ) where import Data.String (fromString) import Data.Text (Text, unpack) import Data.Text.Encoding (decodeUtf8) import Data.Vector (Vector) import qualified Data.Vector as V import Database.PostgreSQL.Simple.Types (Query (..)) import Data.Foldable (fold) import qualified Data.Text as T import Data.Text.Display (display) import Database.PostgreSQL.Entity.Internal.Unsafe (Field (Field)) import Database.PostgreSQL.Entity.Types {- $setup >>> :set -XQuasiQuotes >>> :set -XOverloadedLists >>> :set -XTypeApplications >>> import Database.PostgreSQL.Entity >>> import Database.PostgreSQL.Entity.Types >>> import Database.PostgreSQL.Entity.Internal.BlogPost >>> import Database.PostgreSQL.Entity.Internal.QQ >>> import Database.PostgreSQL.Entity.Internal.Unsafe -} {-| Wrap the given text between parentheses __Examples__ >>> inParens "wrap me!" "(wrap me!)" @since 0.0.1.0 -} inParens :: Text -> Text inParens t = "(" <> t <> ")" {-| Wrap the given text between double quotes __Examples__ >>> quoteName "meow." "\"meow.\"" @since 0.0.1.0 -} quoteName :: Text -> Text quoteName n = "\"" <> n <> "\"" {-| Wrap the given text between single quotes, for literal text in an SQL query. __Examples__ >>> literal "meow." "'meow.'" @since 0.0.2.0 -} literal :: Text -> Text literal n = "\'" <> escapeSingleQuotes n <> "\'" where escapeSingleQuotes x = T.replace "'" "''" x {-| Safe getter that quotes a table name __Examples__ >>> getTableName @Author "\"authors\"" >>> getTableName @Tags "public.\"tags\"" @since 0.0.1.0 -} getTableName :: forall e. Entity e => Text getTableName = prefix (schema @e) <> quoteName (tableName @e) {-| Safe getter that quotes a table's primary key __Examples__ >>> getPrimaryKey @Author "\"author_id\"" >>> getPrimaryKey @Tags "\"category\"" @since 0.0.2.0 -} getPrimaryKey :: forall e. Entity e => Text getPrimaryKey = getFieldName $ primaryKey @e prefix :: Maybe Text -> Text prefix = maybe "" (<> ".") {-| Accessor to the name of a field, with quotation. >>> getFieldName ([field| author_id |]) "\"author_id\"" @since 0.0.2.0 -} getFieldName :: Field -> Text getFieldName = quoteName . fieldName {-| Produce a comma-separated list of an entity's fields. __Examples__ >>> expandFields @BlogPost "\"blogpost_id\", \"author_id\", \"uuid_list\", \"title\", \"content\", \"created_at\"" @since 0.0.1.0 -} expandFields :: forall e. Entity e => Text expandFields = V.foldl1' (\element acc -> element <> ", " <> acc) (getFieldName <$> fields @e) {-| Produce a comma-separated list of an entity's fields, qualified with the table name __Examples__ >>> expandQualifiedFields @BlogPost "blogposts.\"blogpost_id\", blogposts.\"author_id\", blogposts.\"uuid_list\", blogposts.\"title\", blogposts.\"content\", blogposts.\"created_at\"" @since 0.0.1.0 -} expandQualifiedFields :: forall e. Entity e => Text expandQualifiedFields = expandQualifiedFields' (fields @e) prefixName where prefixName = tableName @e {-| Produce a comma-separated list of an entity's 'fields', qualified with an arbitrary prefix __Examples__ >>> expandQualifiedFields' (fields @BlogPost) "legacy" "legacy.\"blogpost_id\", legacy.\"author_id\", legacy.\"uuid_list\", legacy.\"title\", legacy.\"content\", legacy.\"created_at\"" @since 0.0.1.0 -} expandQualifiedFields' :: Vector Field -> Text -> Text expandQualifiedFields' fs prefixName = V.foldl1' (\element acc -> element <> ", " <> acc) fs' where fs' = fieldName <$> qualifyFields prefixName fs -- {-| Take a prefix and a vector of fields, and qualifies each field with the prefix __Examples__ >>> qualifyField @Author [field| name |] "authors.\"name\"" @since 0.0.2.0 -} qualifyField :: forall e. Entity e => Field -> Text qualifyField f = (\(Field fName _) -> p <> "." <> quoteName fName) f where p = tableName @e {-| Take a prefix and a vector of fields, and qualifies each field with the prefix __Examples__ >>> qualifyFields "legacy" (fields @BlogPost) [Field "legacy.\"blogpost_id\"" Nothing,Field "legacy.\"author_id\"" Nothing,Field "legacy.\"uuid_list\"" Nothing,Field "legacy.\"title\"" Nothing,Field "legacy.\"content\"" Nothing,Field "legacy.\"created_at\"" Nothing] @since 0.0.1.0 -} qualifyFields :: Text -> Vector Field -> Vector Field qualifyFields p fs = fmap (\(Field f t) -> Field (p <> "." <> quoteName f) t) fs {-| Produce a placeholder of the form @\"field\" = ?@ with an optional type annotation. __Examples__ >>> placeholder [field| id |] "\"id\" = ?" >>> placeholder $ [field| ids |] "\"ids\" = ?" >>> fmap placeholder $ fields @BlogPost ["\"blogpost_id\" = ?","\"author_id\" = ?","\"uuid_list\" = ?","\"title\" = ?","\"content\" = ?","\"created_at\" = ?"] @since 0.0.1.0 -} placeholder :: Field -> Text placeholder (Field f Nothing) = quoteName f <> " = ?" placeholder (Field f (Just t)) = quoteName f <> " = ?::" <> t {-| Produce a placeholder of the form @table.\"field\" = ?@ with an optional type annotation. __Examples__ >>> placeholder' @BlogPost [field| id |] "blogposts.\"id\" = ?" >>> placeholder' @BlogPost $ [field| ids |] "blogposts.\"ids\" = ?" @since 0.0.2.0 -} placeholder' :: forall e. Entity e => Field -> Text placeholder' f@(Field _ (Just t)) = qualifyField @e f <> " = ?::" <> t placeholder' f = qualifyField @e f <> " = ?" {-| Generate an appropriate number of “?” placeholders given a vector of fields. Used to generate INSERT queries. __Examples__ >>> generatePlaceholders $ fields @BlogPost "?, ?, ?, ?, ?, ?" @since 0.0.1.0 -} generatePlaceholders :: Vector Field -> Text generatePlaceholders vf = fold $ intercalateVector ", " $ fmap ph vf where ph (Field _ t) = maybe "?" (\t' -> "?::" <> t') t {-| Produce an IS NOT NULL statement given a vector of fields >>> isNotNull [ [field| possibly_empty |] ] "\"possibly_empty\" IS NOT NULL" >>> isNotNull [[field| possibly_empty |], [field| that_one_too |]] "\"possibly_empty\" IS NOT NULL AND \"that_one_too\" IS NOT NULL" @since 0.0.1.0 -} isNotNull :: Vector Field -> Text isNotNull fs' = fold $ intercalateVector " AND " (fmap process fieldNames) where fieldNames = fmap fieldName fs' process f = quoteName f <> " IS NOT NULL" {-| Produce an IS NULL statement given a vector of fields >>> isNull [ [field| possibly_empty |] ] "\"possibly_empty\" IS NULL" >>> isNull [[field| possibly_empty |], [field| that_one_too |]] "\"possibly_empty\" IS NULL AND \"that_one_too\" IS NULL" @since 0.0.1.0 -} isNull :: Vector Field -> Text isNull fs' = fold $ intercalateVector " AND " (fmap process fieldNames) where fieldNames = fmap fieldName fs' process f = quoteName f <> " IS NULL" isIn :: Field -> Vector Text -> Text isIn f values = process f <> " IN (" <> fold (intercalateVector ", " vals) <> ")" where vals = fmap literal values process f' = quoteName $ fieldName f' {-| Since the 'Query' type has an 'IsString' instance, the process of converting from 'Text' to 'String' to 'Query' is factored into this function ⚠ This may be dangerous and an unregulated usage of this function may expose to you SQL injection attacks @since 0.0.1.0 -} textToQuery :: Text -> Query textToQuery = fromString . unpack {-| For cases where combinator composition is tricky, we can safely get back to a 'Text' string from a 'Query' ⚠ This may be dangerous and an unregulated usage of this function may expose to you SQL injection attacks @since 0.0.1.0 -} queryToText :: Query -> Text queryToText = decodeUtf8 . fromQuery {-| The 'intercalateVector' function takes a Text and a Vector Text and concatenates the vector after interspersing the first argument between each element of the list. __Examples__ >>> intercalateVector "~" [] [] >>> intercalateVector "~" ["nyan"] ["nyan"] >>> intercalateVector "~" ["nyan", "nyan", "nyan"] ["nyan","~","nyan","~","nyan"] @since 0.0.1.0 -} intercalateVector :: Text -> Vector Text -> Vector Text intercalateVector sep vt | V.null vt = vt | otherwise = V.cons x (go xs) where (x, xs) = (V.head vt, V.tail vt) go :: Vector Text -> Vector Text go ys | V.null ys = ys | otherwise = V.cons sep (V.cons (V.head ys) (go (V.tail ys))) {-| __Examples__ >>> renderSortExpression ([field| title |], ASC) "\"title\" ASC" @since 0.0.2.0 -} renderSortExpression :: (Field, SortKeyword) -> Text renderSortExpression (f, sort) = (quoteName . fieldName) f <> " " <> display sort