module Hasql.Statement
(
  Statement(..),
  refineResult,
  -- * Recipies

  -- ** Insert many
  -- $insertMany

  -- ** IN and NOT IN
  -- $inAndNotIn
)
where

import Hasql.Private.Prelude
import qualified Hasql.Decoders as Decoders
import qualified Hasql.Encoders as Encoders
import qualified Hasql.Private.Decoders as Decoders

{-|
Specification of a strictly single-statement query, which can be parameterized and prepared.

Consists of the following:

* SQL template,
* params encoder,
* result decoder,
* a flag, determining whether it should be prepared.

The SQL template must be formatted according to Postgres' standard,
with any non-ASCII characters of the template encoded using UTF-8.
According to the format,
parameters must be referred to using a positional notation, as in the following:
@$1@, @$2@, @$3@ and etc.
Those references must be used in accordance to the order in which the according
value encoders are specified in 'Encoders.Params'.

Following is an example of a declaration of a prepared statement with its associated codecs.

@
selectSum :: 'Statement' (Int64, Int64) Int64
selectSum = 'Statement' sql encoder decoder True where
  sql = "select ($1 + $2)"
  encoder =
    ('fst' '>$<' Encoders.'Hasql.Encoders.param' (Encoders.'Hasql.Encoders.nonNullable' Encoders.'Hasql.Encoders.int8')) '<>'
    ('snd' '>$<' Encoders.'Hasql.Encoders.param' (Encoders.'Hasql.Encoders.nullable' Encoders.'Hasql.Encoders.text'))
  decoder = Decoders.'Hasql.Decoders.singleRow' (Decoders.'Hasql.Decoders.column' (Decoders.'Hasql.Decoders.nonNullable' Decoders.'Hasql.Decoders.int8'))
@

The statement above accepts a product of two parameters of type 'Int64'
and produces a single result of type 'Int64'.
-}
data Statement a b =
  Statement ByteString (Encoders.Params a) (Decoders.Result b) Bool

instance Functor (Statement a) where
  {-# INLINE fmap #-}
  fmap = rmap

instance Profunctor Statement where
  {-# INLINE dimap #-}
  dimap f1 f2 (Statement template encoder decoder preparable) =
    Statement template (contramap f1 encoder) (fmap f2 decoder) preparable

{-|
Refine a result of a statement,
causing the running session to fail with the `UnexpectedResult` error in case of refinement failure.

This function is especially useful for refining the results of statements produced with
<http://hackage.haskell.org/package/hasql-th the \"hasql-th\" library>.
-}
refineResult :: (a -> Either Text b) -> Statement params a -> Statement params b
refineResult refiner (Statement template encoder decoder preparable) =
  Statement template encoder (Decoders.refineResult refiner decoder) preparable


{- $insertMany

It is not currently possible to pass in an array of encodable values
to use in an insert many statement. Instead, PostgreSQL's
(9.4 or later) @unnest@ function can be used in an analogous way
to haskell's `zip` function by passing in multiple arrays of values
to be zipped into the rows we want to insert:

@
insertMultipleLocations :: 'Statement' (Vector (UUID, Double, Double)) ()
insertMultipleLocations = 'Statement' sql encoder decoder True where
  sql = "insert into location (id, x, y) select * from unnest ($1, $2, $3)"
  encoder =
    contramap Vector.'Data.Vector.unzip3' $
    contrazip3 (vector Encoders.'Encoders.uuid') (vector Encoders.'Encoders.float8') (vector Encoders.'Encoders.float8')
    where
      vector =
        Encoders.'Encoders.param' .
        Encoders.'Encoders.nonNullable' .
        Encoders.'Encoders.array' .
        Encoders.'Encoders.dimension' 'foldl'' .
        Encoders.'Encoders.element' .
        Encoders.'Encoders.nonNullable'
  decoder = Decoders.'Decoders.noResult'
@

This approach is much more efficient than executing a single-row Insert
statement multiple times.
-}

{- $inAndNotIn

There is a common misconception that Postgresql supports array
as a parameter for the @IN@ operator.
However Postgres only supports a syntactical list of values with it,
i.e., you have to specify each option as an individual parameter
(@something IN ($1, $2, $3)@).

Clearly it would be much more convenient to provide an array as a single parameter,
but the @IN@ operator does not support that.
Fortunately, Postgres does provide such functionality with other operators:

* Use @something = ANY($1)@ instead of @something IN ($1)@
* Use @something <> ALL($1)@ instead of @something NOT IN ($1)@

For details see
<https://www.postgresql.org/docs/9.6/static/functions-comparisons.html#AEN20944 the Postgresql docs>.
-}