orville-postgresql-1.1.0.0: src/Orville/PostgreSQL/Marshall/SyntheticField.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
Copyright : Flipstone Technology Partners 2023-2025
License : MIT
Stability : Stable
@since 1.0.0.0
-}
module Orville.PostgreSQL.Marshall.SyntheticField
( SyntheticField
, syntheticFieldExpression
, syntheticFieldAlias
, syntheticFieldName
, syntheticFieldValueFromSqlValue
, syntheticField
, nullableSyntheticField
, prefixSyntheticField
, orderBySyntheticField
)
where
import qualified Data.ByteString.Char8 as B8
import qualified Orville.PostgreSQL.Expr as Expr
import Orville.PostgreSQL.Internal.FieldName (FieldName, byteStringToFieldName, fieldNameToByteString, stringToFieldName)
import qualified Orville.PostgreSQL.Marshall.SqlComparable as SqlComparable
import qualified Orville.PostgreSQL.Marshall.SqlType as SqlType
import qualified Orville.PostgreSQL.Raw.SqlValue as SqlValue
{- | A 'SyntheticField' can be used to evaluate a SQL expression based on the
columns of a table when records are selected from the database. Synthetic
fields are inherently read-only.
@since 1.0.0.0
-}
data SyntheticField a = SyntheticField
{ i_syntheticFieldExpression :: Expr.ValueExpression
, i_syntheticFieldName :: FieldName
, i_syntheticFieldType :: SqlType.SqlType a
}
{- | Marshalls a Haskell value to be in the field to its 'SqlValue.SqlValue'
representation.
@since 1.1.0.0
-}
instance SqlComparable.SqlComparable (SyntheticField a) a where
toComparableSqlValue field = SqlType.sqlTypeToSql (syntheticFieldType field)
referenceValueExpression = syntheticFieldExpression
{- | Returns the SQL expression that should be used in select statements to
calculate the synthetic field.
@since 1.0.0.0
-}
syntheticFieldExpression :: SyntheticField a -> Expr.ValueExpression
syntheticFieldExpression =
i_syntheticFieldExpression
{- | Returns the alias that should be used in select statements to name the
synthetic field.
@since 1.0.0.0
-}
{-# DEPRECATED syntheticFieldAlias "Use syntheticFieldName instead" #-}
syntheticFieldAlias :: SyntheticField a -> FieldName
syntheticFieldAlias =
i_syntheticFieldName
{- | Returns the name that should be used in select statements to name the
synthetic field.
@since 1.1.0.0
-}
syntheticFieldName :: SyntheticField a -> FieldName
syntheticFieldName =
i_syntheticFieldName
{- | Decodes a calculated value selected from the database to its expected
Haskell type. Returns a 'Left' with an error message if the decoding fails.
@since 1.0.0.0
-}
syntheticFieldValueFromSqlValue :: SyntheticField a -> SqlValue.SqlValue -> Either String a
syntheticFieldValueFromSqlValue =
SqlType.sqlTypeFromSql . syntheticFieldType
{- | The 'SqlType.SqlType' for the 'SyntheticField' determines the PostgreSQL
data type used to define the field as well as how to marshall Haskell values
from the database.
@since 1.1.0.0
-}
syntheticFieldType :: SyntheticField a -> SqlType.SqlType a
syntheticFieldType = i_syntheticFieldType
{- | Constructs a 'SyntheticField' that will select a SQL expression using
the given alias.
@since 1.0.0.0
-}
syntheticField ::
-- | The SQL expression to be selected.
Expr.ValueExpression ->
-- | The alias to be used to name the calculation in SQL expressions.
String ->
-- | The 'SqlType.SqlType' to be used for comparisons
SqlType.SqlType a ->
SyntheticField a
syntheticField expression alias sqlType =
SyntheticField
{ i_syntheticFieldExpression = expression
, i_syntheticFieldName = stringToFieldName alias
, i_syntheticFieldType = sqlType
}
{- | Modifies a 'SyntheticField' to allow it to decode @NULL@ values.
@since 1.0.0.0
-}
nullableSyntheticField :: SyntheticField a -> SyntheticField (Maybe a)
nullableSyntheticField synthField =
let
nullableType :: SqlType.SqlType a -> SqlType.SqlType (Maybe a)
nullableType sqlType =
sqlType
{ SqlType.sqlTypeToSql = maybe SqlValue.sqlNull (SqlType.sqlTypeToSql sqlType)
, SqlType.sqlTypeFromSql =
\sqlValue ->
if SqlValue.isSqlNull sqlValue
then Right Nothing
else fmap Just $ SqlType.sqlTypeFromSql sqlType sqlValue
}
in
synthField
{ i_syntheticFieldType = nullableType (syntheticFieldType synthField)
}
{- | Adds a prefix, followed by an underscore, to the alias used to name the
synthetic field.
@since 1.0.0.0
-}
prefixSyntheticField ::
String ->
SyntheticField a ->
SyntheticField a
prefixSyntheticField prefix synthField =
synthField
{ i_syntheticFieldName = byteStringToFieldName (B8.pack prefix <> "_" <> fieldNameToByteString (syntheticFieldName synthField))
}
{- | Orders a query by the alias for the given synthetic field.
@since 1.1.0.0
-}
orderBySyntheticField :: SyntheticField a -> Expr.OrderByDirection -> Expr.OrderByExpr
orderBySyntheticField =
Expr.orderByColumnName . Expr.unqualified . Expr.fromIdentifier . Expr.identifierFromBytes . fieldNameToByteString . syntheticFieldName