packages feed

hpgsql-0.1.0.0: src/Hpgsql/Query.hs

module Hpgsql.Query
  ( Query, -- Do not export constructor
    SingleQuery, -- Do not export constructor
    sql,
    sqlPrep,
    mkQuery,
    escapeIdentifier,
    vALUES,
    preparedStatement,
    nonPreparedStatement,

    -- * Internal
    -- $internal
    breakQueryIntoStatements,
    mkQueryInternal,
    encodeParam,
  )
where

import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.List as List
import Data.Proxy (Proxy (..))
import Hpgsql.Builder (BinaryField (..))
import Hpgsql.Encoding (RowEncoder (..), ToPgRow (..))
import Hpgsql.InternalTypes (Query (..), SingleQuery (..), SingleQueryFragment (..), breakQueryIntoStatements)
import Hpgsql.QueryInternal (encodeParam, mkQuery, mkQueryInternal, sql, sqlPrep)
import Hpgsql.TypeInfo (EncodingContext, Oid)

-- $internal
-- These functions are internal implementation details and are not intended for public use.
-- They may change or be removed without notice.

-- | Escapes a database object identifier like a table name or a column name,
-- so it can be embedded  inside a @[sql|...|]@ quasiquote using @^{expr}@ syntax:
--
-- > [sql| SELECT ^{escapeIdentifier "çolumñ"} FROM ^{escapeIdentifier "sChEmA"}.some_table |]
escapeIdentifier :: ByteString -> Query
escapeIdentifier v = Query {queryString = [FragmentOfStaticSql "\"", FragmentOfStaticSql (doubleQuotes v), FragmentOfStaticSql "\""], queryParams = [], isPrepared = False}
  where
    doubleQuotes = BS.intercalate "\"\"" . BS.split 0x22 {- '"' -}

-- | Generates a query like @VALUES ($1,$2), ($3,$4)@ from a list of rows.
-- Can be embedded inside a @[sql|...|]@ quasiquote using @^{expr}@ syntax:
--
-- > [sql| INSERT INTO emp(id,name) ^{vALUES rows} ON CONFLICT DO NOTHING |]
vALUES :: forall a. (ToPgRow a) => [a] -> Query
vALUES [] =
  let rowOids = rowEncoder.toTypeOids (Proxy @a)
      nullParams :: [EncodingContext -> (Maybe Oid, BinaryField)]
      nullParams = map (\f -> \encCtx -> (f encCtx, SqlNull)) rowOids
   in [sql|(SELECT * FROM (VALUES ^{commaSeparatedRowTuples [nullParams]}) _subq LIMIT 0)|]
vALUES rows =
  let allParams = map rowEncoder.toPgParams rows
   in "VALUES " <> commaSeparatedRowTuples allParams

commaSeparatedRowTuples :: [[EncodingContext -> (Maybe Oid, BinaryField)]] -> Query
commaSeparatedRowTuples rowTuples =
  let (_, queryFragsPerRow) =
        List.mapAccumR
          ( \(!maxArgSoFar) singleRow ->
              let numParams = length singleRow
                  numberedArgs = map (QueryArgumentPlaceHolder . (+ maxArgSoFar)) [1 .. numParams]
               in (maxArgSoFar + numParams, FragmentOfStaticSql "(" : (List.intersperse (FragmentOfStaticSql ",") numberedArgs ++ [FragmentOfStaticSql ")"]))
          )
          0
          rowTuples
   in Query {queryString = mconcat $ List.intersperse [FragmentOfStaticSql ","] queryFragsPerRow, queryParams = mconcat rowTuples, isPrepared = False}

-- | Turns a `Query` into a prepared `Query`, i.e. every SQL statement
-- in the `Query` will be a prepared statement.
preparedStatement :: Query -> Query
preparedStatement Query {..} = Query {isPrepared = True, ..}

-- | Turns a `Query` into a non-prepared `Query`, i.e. every SQL statement
-- in the `Query` will not be a prepared statement.
nonPreparedStatement :: Query -> Query
nonPreparedStatement Query {..} = Query {isPrepared = False, ..}