Skip to content

Commit a1eb940

Browse files
committed
Add Values constructor
Closes #61 and #91. See #61 in particular for a discussion of the design
1 parent f0bfbb9 commit a1eb940

File tree

4 files changed

+124
-1
lines changed

4 files changed

+124
-1
lines changed

src/Database/PostgreSQL/Simple/ToField.hs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import Data.Monoid (mappend)
3333
import Data.Time (Day, TimeOfDay, LocalTime, UTCTime, ZonedTime)
3434
import Data.Typeable (Typeable)
3535
import Data.Word (Word, Word8, Word16, Word32, Word64)
36+
import {-# SOURCE #-} Database.PostgreSQL.Simple.ToRow
3637
import Database.PostgreSQL.Simple.Types
3738
import qualified Blaze.ByteString.Builder.Char.Utf8 as Utf8
3839
import qualified Data.ByteString as SB
@@ -279,3 +280,59 @@ toJSONField = toField . JSON.toJSON
279280
inQuotes :: Builder -> Builder
280281
inQuotes b = quote `mappend` b `mappend` quote
281282
where quote = Utf8.fromChar '\''
283+
284+
interleaveFoldr :: (a -> [b] -> [b]) -> b -> [b] -> [a] -> [b]
285+
interleaveFoldr f b bs as = foldr (\a bs -> b : f a bs) bs as
286+
{-# INLINE interleaveFoldr #-}
287+
288+
instance ToRow a => ToField (Values a) where
289+
toField (Values types rows) =
290+
case rows of
291+
[] -> case types of
292+
[] -> error err
293+
(_:_) -> values $ typedRow (repeat (lit "null"))
294+
types
295+
[lit " LIMIT 0)"]
296+
(_:_) -> case types of
297+
[] -> values $ untypedRows rows [litC ')']
298+
(_:_) -> values $ typedRows rows types [litC ')']
299+
where
300+
err = "Database.PostgreSQL.Simple.toField :: Values -> Action either values or types must be non-empty"
301+
lit = Plain . fromByteString
302+
litC = Plain . fromChar
303+
values x = Many (lit "(VALUES ": x)
304+
305+
typedField :: (Action, QualifiedIdentifier) -> [Action] -> [Action]
306+
typedField (val,typ) rest = val : lit "::" : toField typ : rest
307+
308+
typedRow :: [Action] -> [QualifiedIdentifier] -> [Action] -> [Action]
309+
typedRow (val:vals) (typ:typs) rest =
310+
litC '(' :
311+
typedField (val,typ) ( interleaveFoldr
312+
typedField
313+
(litC ',')
314+
(litC ')' : rest)
315+
(zip vals typs) )
316+
317+
untypedRow :: [Action] -> [Action] -> [Action]
318+
untypedRow (val:vals) rest =
319+
litC '(' : val :
320+
interleaveFoldr
321+
(:)
322+
(litC ',')
323+
(litC ')' : rest)
324+
vals
325+
326+
typedRows :: ToRow a => [a] -> [QualifiedIdentifier] -> [Action] -> [Action]
327+
typedRows (val:vals) types rest =
328+
typedRow (toRow val) types (litC ',' : untypedRows vals rest)
329+
330+
untypedRows :: ToRow a => [a] -> [Action] -> [Action]
331+
untypedRows [] rest = rest
332+
untypedRows (val:vals) rest =
333+
untypedRow (toRow val) $
334+
interleaveFoldr
335+
(untypedRow . toRow)
336+
(litC ',')
337+
rest
338+
vals

src/Database/PostgreSQL/Simple/ToField.hs-boot

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
module Database.PostgreSQL.Simple.ToField where
22

33
import Database.PostgreSQL.Simple.Types
4+
import Blaze.ByteString.Builder(Builder)
5+
import Data.ByteString(ByteString)
6+
7+
-- | How to render an element when substituting it into a query.
8+
data Action =
9+
Plain Builder
10+
-- ^ Render without escaping or quoting. Use for non-text types
11+
-- such as numbers, when you are /certain/ that they will not
12+
-- introduce formatting vulnerabilities via use of characters such
13+
-- as spaces or \"@'@\".
14+
| Escape ByteString
15+
-- ^ Escape and enclose in quotes before substituting. Use for all
16+
-- text-like types, and anything else that may contain unsafe
17+
-- characters when rendered.
18+
| EscapeByteA ByteString
19+
-- ^ Escape binary data for use as a @bytea@ literal. Include surrounding
20+
-- quotes. This is used by the 'Binary' newtype wrapper.
21+
| EscapeIdentifier ByteString
22+
-- ^ Escape before substituting. Use for all sql identifiers like
23+
-- table, column names, etc. This is used by the 'Identifier' newtype
24+
-- wrapper.
25+
| Many [Action]
26+
-- ^ Concatenate a series of rendering actions.
427

528
class ToField a
629

src/Database/PostgreSQL/Simple/ToRow.hs-boot

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Database.PostgreSQL.Simple.ToRow where
33
import Database.PostgreSQL.Simple.Types
44
import {-# SOURCE #-} Database.PostgreSQL.Simple.ToField
55

6-
class ToRow a
6+
class ToRow a where
7+
toRow :: a -> [Action]
78

89
instance ToField a => ToRow (Only a)

src/Database/PostgreSQL/Simple/Types.hs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ module Database.PostgreSQL.Simple.Types
2727
, (:.)(..)
2828
, Savepoint(..)
2929
, PGArray(..)
30+
, Values(..)
3031
) where
3132

3233
import Blaze.ByteString.Builder (toByteString)
@@ -173,3 +174,44 @@ infixr 3 :.
173174

174175
newtype Savepoint = Savepoint Query
175176
deriving (Eq, Ord, Show, Read, Typeable)
177+
178+
-- | Represents a @VALUES@ table literal, usable as an alternative
179+
-- to @executeMany@ and @returning@. For example:
180+
--
181+
-- > execute c "INSERT INTO table (key,val) ?"
182+
-- > (Only (Values ["int4","text"]
183+
-- > [(1,"hello"),(2,"world")]))
184+
--
185+
-- Issues the following query:
186+
--
187+
-- > INSERT INTO table (key,val) (VALUES (1::"int4",'hello'::"text"),(2,'world'))
188+
--
189+
-- When the list of values is empty, the following query will be issued:
190+
--
191+
-- > INSERT INTO table (key,val) (VALUES (null::"int4",null::"text") LIMIT 0)
192+
--
193+
-- By contrast, @executeMany@ and @returning@ don't issue the query
194+
-- in the empty case, and simply return @0@ and @[]@ respectively.
195+
--
196+
-- The advantage over @executeMany@ is in cases when you want to
197+
-- parameterize table literals in addition to other parameters, as can
198+
-- occur with writable common table expressions, for example.
199+
--
200+
-- The first argument is a list of postgresql type names. Because this
201+
-- is turned into a properly quoted identifier, the type name is case
202+
-- sensitive and must be as it appears in the @pg_type@ table. Thus,
203+
-- you must write @timestamptz@ instead of @timestamp with time zone@,
204+
-- @int4@ instead of @integer@, @_int8@ instead of @bigint[]@, etcetera.
205+
--
206+
-- You may omit the type names, however, if you do so the list
207+
-- of values must be non-empty, and postgresql must be able to infer
208+
-- the types of the columns from the surrounding context. If these
209+
-- conditions are not met, postgresql-simple will throw an exception
210+
-- without issuing the query in the former case, and in the latter
211+
-- the postgres server will return an error which will be turned into
212+
-- a @SqlError@ exception.
213+
--
214+
-- See <http://www.postgresql.org/docs/9.3/static/sql-values.html> for
215+
-- more information.
216+
data Values a = Values [QualifiedIdentifier] [a]
217+
deriving (Eq, Ord, Show, Read, Typeable)

0 commit comments

Comments
 (0)