@@ -0,0 +1,208 @@
1+{-# LANGUAGE DeriveDataTypeable, DeriveFunctor, FlexibleInstances #-}
2+
3+------------------------------------------------------------------------------
4+-- |
5+-- Module: Database.PostgreSQL.Simple.ToField
6+-- Copyright: (c) 2011 MailRank, Inc.
7+-- (c) 2011 Leon P Smith
8+-- License: BSD3
9+-- Maintainer: Leon P Smith <leon@melding-monads.com>
10+-- Stability: experimental
11+-- Portability: portable
12+--
13+-- The 'ToField' typeclass, for rendering a parameter to a SQL query.
14+--
15+------------------------------------------------------------------------------
16+
17+module Database.PostgreSQL.Simple.ToField
18+ (
19+ Action(..)
20+ , ToField(..)
21+ , inQuotes
22+ ) where
23+
24+import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString,
25+ toByteString)
26+import Blaze.ByteString.Builder.Char8 (fromChar)
27+import Blaze.Text (integral, double, float)
28+import Data.ByteString (ByteString)
29+import qualified Data.ByteString.Base16 as B16
30+import qualified Data.ByteString.Base16.Lazy as L16
31+import Data.Int (Int8, Int16, Int32, Int64)
32+import Data.List (intersperse)
33+import Data.Monoid (mappend)
34+import Data.Time.Calendar (Day, showGregorian)
35+import Data.Time.Clock (UTCTime)
36+import Data.Time.Format (formatTime)
37+import Data.Time.LocalTime (TimeOfDay)
38+import Data.Typeable (Typeable)
39+import Data.Word (Word, Word8, Word16, Word32, Word64)
40+import Database.PostgreSQL.Simple.Types (Binary(..), In(..), Null)
41+import System.Locale (defaultTimeLocale)
42+import qualified Blaze.ByteString.Builder.Char.Utf8 as Utf8
43+import qualified Data.ByteString as SB
44+import qualified Data.ByteString.Lazy as LB
45+import qualified Data.Text as ST
46+import qualified Data.Text.Encoding as ST
47+import qualified Data.Text.Lazy as LT
48+import qualified Database.PostgreSQL.LibPQ as PQ
49+
50+-- | How to render an element when substituting it into a query.
51+data Action =
52+ Plain Builder
53+ -- ^ Render without escaping or quoting. Use for non-text types
54+ -- such as numbers, when you are /certain/ that they will not
55+ -- introduce formatting vulnerabilities via use of characters such
56+ -- as spaces or \"@'@\".
57+ | Escape ByteString
58+ -- ^ Escape and enclose in quotes before substituting. Use for all
59+ -- text-like types, and anything else that may contain unsafe
60+ -- characters when rendered.
61+ | Many [Action]
62+ -- ^ Concatenate a series of rendering actions.
63+ deriving (Typeable)
64+
65+instance Show Action where
66+ show (Plain b) = "Plain " ++ show (toByteString b)
67+ show (Escape b) = "Escape " ++ show b
68+ show (Many b) = "Many " ++ show b
69+
70+-- | A type that may be used as a single parameter to a SQL query.
71+class ToField a where
72+ toField :: a -> Action
73+ -- ^ Prepare a value for substitution into a query string.
74+
75+instance ToField Action where
76+ toField a = a
77+ {-# INLINE toField #-}
78+
79+instance (ToField a) => ToField (Maybe a) where
80+ toField Nothing = renderNull
81+ toField (Just a) = toField a
82+ {-# INLINE toField #-}
83+
84+instance (ToField a) => ToField (In [a]) where
85+ toField (In []) = Plain $ fromByteString "(null)"
86+ toField (In xs) = Many $
87+ Plain (fromChar '(') :
88+ (intersperse (Plain (fromChar ',')) . map toField $ xs) ++
89+ [Plain (fromChar ')')]
90+
91+instance ToField (Binary SB.ByteString) where
92+ toField (Binary bs) = Plain $ fromByteString "'\\x" `mappend`
93+ fromByteString (B16.encode bs) `mappend`
94+ fromChar '\''
95+
96+instance ToField (Binary LB.ByteString) where
97+ toField (Binary bs) = Plain $ fromByteString "'\\x" `mappend`
98+ fromLazyByteString (L16.encode bs) `mappend`
99+ fromChar '\''
100+
101+renderNull :: Action
102+renderNull = Plain (fromByteString "null")
103+
104+instance ToField Null where
105+ toField _ = renderNull
106+ {-# INLINE toField #-}
107+
108+instance ToField Bool where
109+ toField True = Plain (fromByteString "true")
110+ toField False = Plain (fromByteString "false")
111+ {-# INLINE toField #-}
112+
113+instance ToField Int8 where
114+ toField = Plain . integral
115+ {-# INLINE toField #-}
116+
117+instance ToField Int16 where
118+ toField = Plain . integral
119+ {-# INLINE toField #-}
120+
121+instance ToField Int32 where
122+ toField = Plain . integral
123+ {-# INLINE toField #-}
124+
125+instance ToField Int where
126+ toField = Plain . integral
127+ {-# INLINE toField #-}
128+
129+instance ToField Int64 where
130+ toField = Plain . integral
131+ {-# INLINE toField #-}
132+
133+instance ToField Integer where
134+ toField = Plain . integral
135+ {-# INLINE toField #-}
136+
137+instance ToField Word8 where
138+ toField = Plain . integral
139+ {-# INLINE toField #-}
140+
141+instance ToField Word16 where
142+ toField = Plain . integral
143+ {-# INLINE toField #-}
144+
145+instance ToField Word32 where
146+ toField = Plain . integral
147+ {-# INLINE toField #-}
148+
149+instance ToField Word where
150+ toField = Plain . integral
151+ {-# INLINE toField #-}
152+
153+instance ToField Word64 where
154+ toField = Plain . integral
155+ {-# INLINE toField #-}
156+
157+instance ToField PQ.Oid where
158+ toField = Plain . integral . \(PQ.Oid x) -> x
159+ {-# INLINE toField #-}
160+
161+instance ToField Float where
162+ toField v | isNaN v || isInfinite v = Plain (inQuotes (float v))
163+ | otherwise = Plain (float v)
164+ {-# INLINE toField #-}
165+
166+instance ToField Double where
167+ toField v | isNaN v || isInfinite v = Plain (inQuotes (double v))
168+ | otherwise = Plain (double v)
169+ {-# INLINE toField #-}
170+
171+instance ToField SB.ByteString where
172+ toField = Escape
173+ {-# INLINE toField #-}
174+
175+instance ToField LB.ByteString where
176+ toField = toField . SB.concat . LB.toChunks
177+ {-# INLINE toField #-}
178+
179+instance ToField ST.Text where
180+ toField = Escape . ST.encodeUtf8
181+ {-# INLINE toField #-}
182+
183+instance ToField [Char] where
184+ toField = Escape . toByteString . Utf8.fromString
185+ {-# INLINE toField #-}
186+
187+instance ToField LT.Text where
188+ toField = toField . LT.toStrict
189+ {-# INLINE toField #-}
190+
191+instance ToField UTCTime where
192+ toField = Plain . Utf8.fromString . formatTime defaultTimeLocale "'%F %T%Q+00'"
193+ {-# INLINE toField #-}
194+
195+instance ToField Day where
196+ toField = Plain . inQuotes . Utf8.fromString . showGregorian
197+ {-# INLINE toField #-}
198+
199+instance ToField TimeOfDay where
200+ toField = Plain . inQuotes . Utf8.fromString . show
201+ {-# INLINE toField #-}
202+
203+-- | Surround a string with single-quote characters: \"@'@\"
204+--
205+-- This function /does not/ perform any other escaping.
206+inQuotes :: Builder -> Builder
207+inQuotes b = quote `mappend` b `mappend` quote
208+ where quote = Utf8.fromChar '\''