2019-08-12 22:41:05 +03:00
|
|
|
module Hasura.SQL.Value
|
|
|
|
( PGScalarValue(..)
|
|
|
|
, pgColValueToInt
|
2020-02-13 20:38:23 +03:00
|
|
|
, pgScalarValueToJson
|
2019-08-29 16:07:05 +03:00
|
|
|
, withConstructorFn
|
2019-08-12 22:41:05 +03:00
|
|
|
, parsePGValue
|
|
|
|
|
2019-12-11 02:20:55 +03:00
|
|
|
, TxtEncodedPGVal(..)
|
2019-08-12 22:41:05 +03:00
|
|
|
, txtEncodedPGVal
|
|
|
|
|
|
|
|
, binEncoder
|
|
|
|
, txtEncoder
|
|
|
|
, toBinaryValue
|
|
|
|
, toTxtValue
|
|
|
|
, toPrepParam
|
|
|
|
) where
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
import Hasura.SQL.GeoJSON
|
|
|
|
import Hasura.SQL.Time
|
|
|
|
import Hasura.SQL.Types
|
|
|
|
|
|
|
|
import qualified Database.PG.Query as Q
|
|
|
|
import qualified Database.PG.Query.PTI as PTI
|
|
|
|
import qualified Hasura.SQL.DML as S
|
|
|
|
|
|
|
|
import Data.Aeson
|
|
|
|
import Data.Int
|
|
|
|
import Data.Scientific
|
|
|
|
import Data.Time
|
2018-08-06 15:15:08 +03:00
|
|
|
import Hasura.Prelude
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
import qualified Data.Aeson.Text as AE
|
|
|
|
import qualified Data.Aeson.Types as AT
|
2019-08-29 16:07:05 +03:00
|
|
|
import qualified Data.ByteString as B
|
2018-06-27 16:11:32 +03:00
|
|
|
import qualified Data.Text as T
|
2019-08-29 16:07:05 +03:00
|
|
|
import qualified Data.Text.Conversions as TC
|
2018-06-27 16:11:32 +03:00
|
|
|
import qualified Data.Text.Encoding as TE
|
|
|
|
import qualified Data.Text.Lazy as TL
|
2020-02-13 20:38:23 +03:00
|
|
|
import qualified Data.UUID as UUID
|
2018-06-27 16:11:32 +03:00
|
|
|
|
|
|
|
import qualified Database.PostgreSQL.LibPQ as PQ
|
|
|
|
import qualified PostgreSQL.Binary.Encoding as PE
|
|
|
|
|
2019-08-29 16:07:05 +03:00
|
|
|
newtype RasterWKB
|
|
|
|
= RasterWKB { getRasterWKB :: TC.Base16 B.ByteString }
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
|
|
|
instance FromJSON RasterWKB where
|
|
|
|
parseJSON = \case
|
|
|
|
String t -> case TC.fromText t of
|
|
|
|
Just v -> return $ RasterWKB v
|
|
|
|
Nothing -> fail
|
|
|
|
"invalid hexadecimal representation of raster well known binary format"
|
|
|
|
_ -> fail "expecting String for raster"
|
|
|
|
|
2020-02-13 20:38:23 +03:00
|
|
|
instance ToJSON RasterWKB where
|
|
|
|
toJSON = toJSON . TC.toText . getRasterWKB
|
|
|
|
|
2018-06-27 16:11:32 +03:00
|
|
|
-- Binary value. Used in prepared sq
|
2019-08-11 18:34:38 +03:00
|
|
|
data PGScalarValue
|
2018-06-27 16:11:32 +03:00
|
|
|
= PGValInteger !Int32
|
|
|
|
| PGValSmallInt !Int16
|
|
|
|
| PGValBigInt !Int64
|
|
|
|
| PGValFloat !Float
|
|
|
|
| PGValDouble !Double
|
|
|
|
| PGValNumeric !Scientific
|
2020-04-17 12:32:09 +03:00
|
|
|
| PGValMoney !Scientific
|
2018-06-27 16:11:32 +03:00
|
|
|
| PGValBoolean !Bool
|
|
|
|
| PGValChar !Char
|
|
|
|
| PGValVarchar !T.Text
|
|
|
|
| PGValText !T.Text
|
2020-02-11 13:32:13 +03:00
|
|
|
| PGValCitext !T.Text
|
2018-06-27 16:11:32 +03:00
|
|
|
| PGValDate !Day
|
2020-04-21 15:30:48 +03:00
|
|
|
| PGValTimeStamp !LocalTime
|
2018-06-27 16:11:32 +03:00
|
|
|
| PGValTimeStampTZ !UTCTime
|
|
|
|
| PGValTimeTZ !ZonedTimeOfDay
|
2019-08-06 18:27:35 +03:00
|
|
|
| PGNull !PGScalarType
|
2018-06-27 16:11:32 +03:00
|
|
|
| PGValJSON !Q.JSON
|
|
|
|
| PGValJSONB !Q.JSONB
|
2018-09-27 15:23:17 +03:00
|
|
|
| PGValGeo !GeometryWithCRS
|
2019-08-29 16:07:05 +03:00
|
|
|
| PGValRaster !RasterWKB
|
2020-02-13 20:38:23 +03:00
|
|
|
| PGValUUID !UUID.UUID
|
2018-06-27 16:11:32 +03:00
|
|
|
| PGValUnknown !T.Text
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
2020-02-13 20:38:23 +03:00
|
|
|
pgScalarValueToJson :: PGScalarValue -> Value
|
|
|
|
pgScalarValueToJson = \case
|
|
|
|
PGValInteger i -> toJSON i
|
|
|
|
PGValSmallInt i -> toJSON i
|
|
|
|
PGValBigInt i -> toJSON i
|
|
|
|
PGValFloat f -> toJSON f
|
|
|
|
PGValDouble d -> toJSON d
|
|
|
|
PGValNumeric sc -> toJSON sc
|
2020-04-17 12:32:09 +03:00
|
|
|
PGValMoney m -> toJSON m
|
2020-02-13 20:38:23 +03:00
|
|
|
PGValBoolean b -> toJSON b
|
|
|
|
PGValChar t -> toJSON t
|
|
|
|
PGValVarchar t -> toJSON t
|
|
|
|
PGValText t -> toJSON t
|
|
|
|
PGValCitext t -> toJSON t
|
|
|
|
PGValDate d -> toJSON d
|
2020-04-21 15:30:48 +03:00
|
|
|
PGValTimeStamp u ->
|
|
|
|
toJSON $ formatTime defaultTimeLocale "%FT%T%QZ" u
|
2020-02-13 20:38:23 +03:00
|
|
|
PGValTimeStampTZ u ->
|
|
|
|
toJSON $ formatTime defaultTimeLocale "%FT%T%QZ" u
|
|
|
|
PGValTimeTZ (ZonedTimeOfDay tod tz) ->
|
|
|
|
toJSON (show tod ++ timeZoneOffsetString tz)
|
|
|
|
PGNull _ -> Null
|
|
|
|
PGValJSON (Q.JSON j) -> j
|
|
|
|
PGValJSONB (Q.JSONB j) -> j
|
|
|
|
PGValGeo o -> toJSON o
|
|
|
|
PGValRaster r -> toJSON r
|
|
|
|
PGValUUID u -> toJSON u
|
|
|
|
PGValUnknown t -> toJSON t
|
|
|
|
|
2019-08-12 22:41:05 +03:00
|
|
|
pgColValueToInt :: PGScalarValue -> Maybe Int
|
|
|
|
pgColValueToInt (PGValInteger i) = Just $ fromIntegral i
|
|
|
|
pgColValueToInt (PGValSmallInt i) = Just $ fromIntegral i
|
|
|
|
pgColValueToInt (PGValBigInt i) = Just $ fromIntegral i
|
|
|
|
pgColValueToInt _ = Nothing
|
|
|
|
|
2019-08-29 16:07:05 +03:00
|
|
|
withConstructorFn :: PGScalarType -> S.SQLExp -> S.SQLExp
|
|
|
|
withConstructorFn ty v
|
2019-08-12 22:41:05 +03:00
|
|
|
| isGeoType ty = S.SEFnApp "ST_GeomFromGeoJSON" [v] Nothing
|
2019-08-29 16:07:05 +03:00
|
|
|
| ty == PGRaster = S.SEFnApp "ST_RastFromHexWKB" [v] Nothing
|
2019-08-12 22:41:05 +03:00
|
|
|
| otherwise = v
|
|
|
|
|
|
|
|
parsePGValue :: PGScalarType -> Value -> AT.Parser PGScalarValue
|
|
|
|
parsePGValue ty val = case (ty, val) of
|
|
|
|
(_ , Null) -> pure $ PGNull ty
|
|
|
|
(PGUnknown _, String t) -> pure $ PGValUnknown t
|
2019-08-29 16:07:05 +03:00
|
|
|
(PGRaster , _) -> parseTyped -- strictly parse raster value
|
2019-08-12 22:41:05 +03:00
|
|
|
(_ , String t) -> parseTyped <|> pure (PGValUnknown t)
|
|
|
|
(_ , _) -> parseTyped
|
|
|
|
where
|
2020-04-21 16:56:15 +03:00
|
|
|
parseBoundedInt :: forall i. (Integral i, Bounded i) => Value -> AT.Parser i
|
|
|
|
parseBoundedInt val' =
|
|
|
|
withScientific
|
|
|
|
("Integer expected for input type: " ++ show ty)
|
|
|
|
go
|
|
|
|
val'
|
|
|
|
where
|
|
|
|
go num = case toBoundedInteger num of
|
|
|
|
Just parsed -> return parsed
|
|
|
|
Nothing -> fail $ "The value " ++ show num ++ " lies outside the "
|
|
|
|
++ "bounds or is not an integer. Maybe it is a "
|
|
|
|
++ "float, or is there integer overflow?"
|
|
|
|
parseBoundedFloat :: forall a. (RealFloat a) => Value -> AT.Parser a
|
|
|
|
parseBoundedFloat val' =
|
|
|
|
withScientific
|
|
|
|
("Float expected for input type: " ++ show ty)
|
|
|
|
go
|
|
|
|
val'
|
|
|
|
where
|
|
|
|
go num = case toBoundedRealFloat num of
|
|
|
|
Left _ -> fail $ "The value " ++ show num ++ " lies outside the "
|
|
|
|
++ "bounds. Is it overflowing the float bounds?"
|
|
|
|
Right parsed -> return parsed
|
2019-08-12 22:41:05 +03:00
|
|
|
parseTyped = case ty of
|
2020-04-21 16:56:15 +03:00
|
|
|
PGSmallInt -> PGValSmallInt <$> parseBoundedInt val
|
|
|
|
PGInteger -> PGValInteger <$> parseBoundedInt val
|
|
|
|
PGBigInt -> PGValBigInt <$> parseBoundedInt val
|
|
|
|
PGSerial -> PGValInteger <$> parseBoundedInt val
|
|
|
|
PGBigSerial -> PGValBigInt <$> parseBoundedInt val
|
|
|
|
PGFloat -> PGValFloat <$> parseBoundedFloat val
|
|
|
|
PGDouble -> PGValDouble <$> parseBoundedFloat val
|
2019-08-12 22:41:05 +03:00
|
|
|
PGNumeric -> PGValNumeric <$> parseJSON val
|
2020-04-17 12:32:09 +03:00
|
|
|
PGMoney -> PGValMoney <$> parseJSON val
|
2019-08-12 22:41:05 +03:00
|
|
|
PGBoolean -> PGValBoolean <$> parseJSON val
|
|
|
|
PGChar -> PGValChar <$> parseJSON val
|
|
|
|
PGVarchar -> PGValVarchar <$> parseJSON val
|
|
|
|
PGText -> PGValText <$> parseJSON val
|
2020-02-11 13:32:13 +03:00
|
|
|
PGCitext -> PGValCitext <$> parseJSON val
|
2019-08-12 22:41:05 +03:00
|
|
|
PGDate -> PGValDate <$> parseJSON val
|
2020-04-21 15:30:48 +03:00
|
|
|
PGTimeStamp -> PGValTimeStamp <$> parseJSON val
|
2019-08-12 22:41:05 +03:00
|
|
|
PGTimeStampTZ -> PGValTimeStampTZ <$> parseJSON val
|
|
|
|
PGTimeTZ -> PGValTimeTZ <$> parseJSON val
|
|
|
|
PGJSON -> PGValJSON . Q.JSON <$> parseJSON val
|
|
|
|
PGJSONB -> PGValJSONB . Q.JSONB <$> parseJSON val
|
|
|
|
PGGeometry -> PGValGeo <$> parseJSON val
|
|
|
|
PGGeography -> PGValGeo <$> parseJSON val
|
2019-08-29 16:07:05 +03:00
|
|
|
PGRaster -> PGValRaster <$> parseJSON val
|
2020-02-13 20:38:23 +03:00
|
|
|
PGUUID -> PGValUUID <$> parseJSON val
|
2019-08-29 16:07:05 +03:00
|
|
|
PGUnknown tyName ->
|
2020-04-21 16:56:15 +03:00
|
|
|
fail $ "A string is expected for type: " ++ T.unpack tyName
|
2019-08-12 22:41:05 +03:00
|
|
|
|
2019-04-17 12:48:41 +03:00
|
|
|
data TxtEncodedPGVal
|
|
|
|
= TENull
|
|
|
|
| TELit !Text
|
2019-04-18 14:20:27 +03:00
|
|
|
deriving (Show, Eq, Generic)
|
|
|
|
|
|
|
|
instance Hashable TxtEncodedPGVal
|
|
|
|
|
|
|
|
instance ToJSON TxtEncodedPGVal where
|
|
|
|
toJSON = \case
|
|
|
|
TENull -> Null
|
|
|
|
TELit t -> String t
|
2019-04-17 12:48:41 +03:00
|
|
|
|
2019-12-11 02:20:55 +03:00
|
|
|
instance FromJSON TxtEncodedPGVal where
|
|
|
|
parseJSON Null = pure TENull
|
|
|
|
parseJSON (String t) = pure $ TELit t
|
|
|
|
parseJSON v = AT.typeMismatch "String" v
|
|
|
|
|
2019-08-11 18:34:38 +03:00
|
|
|
txtEncodedPGVal :: PGScalarValue -> TxtEncodedPGVal
|
2019-04-17 12:48:41 +03:00
|
|
|
txtEncodedPGVal colVal = case colVal of
|
|
|
|
PGValInteger i -> TELit $ T.pack $ show i
|
|
|
|
PGValSmallInt i -> TELit $ T.pack $ show i
|
|
|
|
PGValBigInt i -> TELit $ T.pack $ show i
|
|
|
|
PGValFloat f -> TELit $ T.pack $ show f
|
|
|
|
PGValDouble d -> TELit $ T.pack $ show d
|
|
|
|
PGValNumeric sc -> TELit $ T.pack $ show sc
|
2020-05-13 13:10:46 +03:00
|
|
|
-- PostgreSQL doesn't like scientific notation for money, so pass it
|
|
|
|
-- with 2 decimal places.
|
|
|
|
PGValMoney m -> TELit $ T.pack $ formatScientific Fixed (Just 2) m
|
2019-04-17 12:48:41 +03:00
|
|
|
PGValBoolean b -> TELit $ bool "false" "true" b
|
|
|
|
PGValChar t -> TELit $ T.pack $ show t
|
|
|
|
PGValVarchar t -> TELit t
|
|
|
|
PGValText t -> TELit t
|
2020-02-11 13:32:13 +03:00
|
|
|
PGValCitext t -> TELit t
|
2019-04-17 12:48:41 +03:00
|
|
|
PGValDate d -> TELit $ T.pack $ showGregorian d
|
2020-04-21 15:30:48 +03:00
|
|
|
PGValTimeStamp u ->
|
|
|
|
TELit $ T.pack $ formatTime defaultTimeLocale "%FT%T%QZ" u
|
2018-06-27 16:11:32 +03:00
|
|
|
PGValTimeStampTZ u ->
|
2019-04-17 12:48:41 +03:00
|
|
|
TELit $ T.pack $ formatTime defaultTimeLocale "%FT%T%QZ" u
|
2018-06-27 16:11:32 +03:00
|
|
|
PGValTimeTZ (ZonedTimeOfDay tod tz) ->
|
2019-04-17 12:48:41 +03:00
|
|
|
TELit $ T.pack (show tod ++ timeZoneOffsetString tz)
|
2018-06-27 16:11:32 +03:00
|
|
|
PGNull _ ->
|
2019-04-17 12:48:41 +03:00
|
|
|
TENull
|
|
|
|
PGValJSON (Q.JSON j) -> TELit $ TL.toStrict $
|
2018-06-27 16:11:32 +03:00
|
|
|
AE.encodeToLazyText j
|
2019-04-17 12:48:41 +03:00
|
|
|
PGValJSONB (Q.JSONB j) -> TELit $ TL.toStrict $
|
2018-06-27 16:11:32 +03:00
|
|
|
AE.encodeToLazyText j
|
2019-04-17 12:48:41 +03:00
|
|
|
PGValGeo o -> TELit $ TL.toStrict $
|
2018-06-27 16:11:32 +03:00
|
|
|
AE.encodeToLazyText o
|
2019-08-29 16:07:05 +03:00
|
|
|
PGValRaster r -> TELit $ TC.toText $ getRasterWKB r
|
2020-02-13 20:38:23 +03:00
|
|
|
PGValUUID u -> TELit $ UUID.toText u
|
2019-04-17 12:48:41 +03:00
|
|
|
PGValUnknown t -> TELit t
|
|
|
|
|
2019-08-12 22:41:05 +03:00
|
|
|
binEncoder :: PGScalarValue -> Q.PrepArg
|
|
|
|
binEncoder colVal = case colVal of
|
2019-12-13 00:46:33 +03:00
|
|
|
PGValInteger i -> Q.toPrepVal i
|
|
|
|
PGValSmallInt i -> Q.toPrepVal i
|
|
|
|
PGValBigInt i -> Q.toPrepVal i
|
|
|
|
PGValFloat f -> Q.toPrepVal f
|
|
|
|
PGValDouble d -> Q.toPrepVal d
|
|
|
|
PGValNumeric sc -> Q.toPrepVal sc
|
2020-04-17 12:32:09 +03:00
|
|
|
PGValMoney m -> Q.toPrepVal m
|
2019-12-13 00:46:33 +03:00
|
|
|
PGValBoolean b -> Q.toPrepVal b
|
|
|
|
PGValChar t -> Q.toPrepVal t
|
|
|
|
PGValVarchar t -> Q.toPrepVal t
|
|
|
|
PGValText t -> Q.toPrepVal t
|
2020-02-11 13:32:13 +03:00
|
|
|
PGValCitext t -> Q.toPrepVal t
|
2019-12-13 00:46:33 +03:00
|
|
|
PGValDate d -> Q.toPrepVal d
|
2020-04-21 15:30:48 +03:00
|
|
|
PGValTimeStamp u -> Q.toPrepVal u
|
2019-12-13 00:46:33 +03:00
|
|
|
PGValTimeStampTZ u -> Q.toPrepVal u
|
2019-08-12 22:41:05 +03:00
|
|
|
PGValTimeTZ (ZonedTimeOfDay t z) -> Q.toPrepValHelper PTI.timetz PE.timetz_int (t, z)
|
2019-12-13 00:46:33 +03:00
|
|
|
PGNull ty -> (pgTypeOid ty, Nothing)
|
|
|
|
PGValJSON u -> Q.toPrepVal u
|
|
|
|
PGValJSONB u -> Q.toPrepVal u
|
|
|
|
PGValGeo o -> Q.toPrepVal $ TL.toStrict $ AE.encodeToLazyText o
|
|
|
|
PGValRaster r -> Q.toPrepVal $ TC.toText $ getRasterWKB r
|
2020-02-13 20:38:23 +03:00
|
|
|
PGValUUID u -> Q.toPrepVal u
|
2019-12-13 00:46:33 +03:00
|
|
|
PGValUnknown t -> (PTI.auto, Just (TE.encodeUtf8 t, PQ.Text))
|
2019-08-12 22:41:05 +03:00
|
|
|
|
2019-08-11 18:34:38 +03:00
|
|
|
txtEncoder :: PGScalarValue -> S.SQLExp
|
2019-04-17 12:48:41 +03:00
|
|
|
txtEncoder colVal = case txtEncodedPGVal colVal of
|
2019-08-29 16:07:05 +03:00
|
|
|
TENull -> S.SENull
|
2019-04-17 12:48:41 +03:00
|
|
|
TELit t -> S.SELit t
|
2018-06-27 16:11:32 +03:00
|
|
|
|
2020-02-11 13:32:13 +03:00
|
|
|
{- Note [Type casting prepared params]
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Prepared values are passed to Postgres via text encoding. Explicit type cast for prepared params
|
|
|
|
is needed to distinguish the column types. For example, the parameter for citext column type is
|
|
|
|
generated as ($i)::citext where 'i' is parameter position (integer).
|
|
|
|
|
|
|
|
Also see https://github.com/hasura/graphql-engine/issues/2818
|
|
|
|
-}
|
|
|
|
|
2019-08-06 18:27:35 +03:00
|
|
|
toPrepParam :: Int -> PGScalarType -> S.SQLExp
|
2020-02-11 13:32:13 +03:00
|
|
|
toPrepParam i ty =
|
|
|
|
-- See Note [Type casting prepared params] above
|
|
|
|
S.withTyAnn ty . withConstructorFn ty $ S.SEPrep i
|
2019-01-17 09:21:38 +03:00
|
|
|
|
2019-08-11 18:34:38 +03:00
|
|
|
toBinaryValue :: WithScalarType PGScalarValue -> Q.PrepArg
|
2019-07-22 15:47:13 +03:00
|
|
|
toBinaryValue = binEncoder . pstValue
|
|
|
|
|
2019-08-11 18:34:38 +03:00
|
|
|
toTxtValue :: WithScalarType PGScalarValue -> S.SQLExp
|
2019-08-29 16:07:05 +03:00
|
|
|
toTxtValue (WithScalarType ty val) =
|
|
|
|
S.withTyAnn ty . withConstructorFn ty $ txtEncoder val
|