mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
152 lines
6.1 KiB
Haskell
152 lines
6.1 KiB
Haskell
|
{-# LANGUAGE QuasiQuotes #-}
|
||
|
|
||
|
-- | This module defines the scalars we use specific to the BigQuery
|
||
|
-- schema.
|
||
|
--
|
||
|
-- An idiosyncracy of BigQuery is that numbers serialized via JSON uses string
|
||
|
-- literals instead of number literals, because BigQuery handles wider-bit
|
||
|
-- numbers than JSON/JavaScript does.
|
||
|
--
|
||
|
-- Therefore, the BigQuery Backend uses bespoke parsers for numeric scalar
|
||
|
-- input, which accept string literals as well as number literals, such that we
|
||
|
-- preserve symmetry with with output formats.
|
||
|
module Hasura.Backends.BigQuery.Parser.Scalars
|
||
|
( bqInt64,
|
||
|
bqFloat64,
|
||
|
bqDecimal,
|
||
|
bqBigDecimal,
|
||
|
)
|
||
|
where
|
||
|
|
||
|
import Data.Aeson qualified as A
|
||
|
import Data.Int (Int64)
|
||
|
import Data.Scientific (Scientific)
|
||
|
import Data.Scientific qualified as S
|
||
|
import Data.Scientific qualified as Scientific
|
||
|
import Data.Text qualified as Text
|
||
|
import Hasura.Backends.BigQuery.Types qualified as BigQuery
|
||
|
import Hasura.Base.ErrorMessage (toErrorMessage)
|
||
|
import Hasura.Base.ErrorValue (dquote)
|
||
|
import Hasura.GraphQL.Parser.Class
|
||
|
import Hasura.GraphQL.Parser.ErrorCode
|
||
|
import Hasura.GraphQL.Parser.Internal.TypeChecking
|
||
|
import Hasura.GraphQL.Parser.Internal.Types
|
||
|
import Hasura.GraphQL.Parser.Schema
|
||
|
import Hasura.GraphQL.Parser.Variable
|
||
|
import Hasura.Prelude
|
||
|
import Language.GraphQL.Draft.Syntax hiding (Definition)
|
||
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||
|
import Language.GraphQL.Draft.Syntax.QQ qualified as G
|
||
|
import Text.ParserCombinators.ReadP
|
||
|
|
||
|
bqInt64 :: forall origin m. MonadParse m => Parser origin 'Both m BigQuery.Int64
|
||
|
bqInt64 = mkScalar name "64-bit integers. Accepts both string and number literals." \case
|
||
|
GraphQLValue (VInt i)
|
||
|
| checkIntegerBounds i -> return $ BigQuery.Int64 (tshow i)
|
||
|
| otherwise -> boundsFailure (tshow i)
|
||
|
GraphQLValue (VString s) -> integralText s
|
||
|
JSONValue (A.String s) -> integralText s
|
||
|
JSONValue (A.Number n) -> integralSci (tshow n) n
|
||
|
v -> typeMismatch name "a 64-bit integer" v
|
||
|
where
|
||
|
name = [G.name|bigquery_int|]
|
||
|
|
||
|
checkIntegerBounds :: Integer -> Bool
|
||
|
checkIntegerBounds i = toInteger (minBound @Int64) <= i && i <= toInteger (maxBound @Int64)
|
||
|
|
||
|
integralText :: Text -> m BigQuery.Int64
|
||
|
integralText inputText
|
||
|
| [(sci, "")] <- readP_to_S Scientific.scientificP (Text.unpack inputText) = integralSci inputText sci
|
||
|
| otherwise = stringNotationError name inputText
|
||
|
|
||
|
integralSci :: Text -> Scientific -> m BigQuery.Int64
|
||
|
integralSci inputText sci
|
||
|
| Scientific.isInteger sci =
|
||
|
case Scientific.toBoundedInteger @Int64 sci of
|
||
|
Just v -> return $ BigQuery.intToInt64 v
|
||
|
Nothing -> boundsFailure inputText
|
||
|
| otherwise = integralFailure inputText
|
||
|
|
||
|
boundsFailure, integralFailure :: forall a. Text -> m a
|
||
|
boundsFailure inputText = parseErrorWith ParseFailed $ "The value " <> toErrorMessage inputText <> " lies outside the accepted numerical integral bounds."
|
||
|
integralFailure inputText = parseErrorWith ParseFailed $ "The value " <> toErrorMessage inputText <> " has a non-zero fractional part."
|
||
|
|
||
|
bqFloat64 :: forall origin m. MonadParse m => Parser origin 'Both m BigQuery.Float64
|
||
|
bqFloat64 = mkScalar name "64-bit floats. Accepts both string and number literals." \case
|
||
|
GraphQLValue (VFloat f) -> floatSci (tshow f) f
|
||
|
GraphQLValue (VInt i) -> floatSci (tshow i) (fromInteger i)
|
||
|
GraphQLValue (VString s) -> floatText s
|
||
|
JSONValue (A.String s) -> floatText s
|
||
|
JSONValue (A.Number n) -> floatSci (tshow n) n
|
||
|
v -> typeMismatch name "a 64-bit float" v
|
||
|
where
|
||
|
name = [G.name|bigquery_float|]
|
||
|
|
||
|
floatText :: Text -> m BigQuery.Float64
|
||
|
floatText inputText
|
||
|
| [(sci, "")] <- readP_to_S Scientific.scientificP (Text.unpack inputText) = floatSci inputText sci
|
||
|
| otherwise = stringNotationError name inputText
|
||
|
|
||
|
floatSci :: Text -> Scientific -> m BigQuery.Float64
|
||
|
floatSci inputText sci =
|
||
|
case Scientific.toBoundedRealFloat @Double sci of
|
||
|
Right v -> return $ BigQuery.doubleToFloat64 v
|
||
|
Left _ -> boundsFailure inputText
|
||
|
|
||
|
boundsFailure :: forall a. Text -> m a
|
||
|
boundsFailure inputText = parseErrorWith ParseFailed $ "The value " <> toErrorMessage inputText <> " lies outside the accepted numerical integral bounds."
|
||
|
|
||
|
bqBigDecimal :: MonadParse m => Parser origin 'Both m BigQuery.BigDecimal
|
||
|
bqBigDecimal = mkScalar name "BigDecimals. Accepts both string and number literals." $ fmap (BigQuery.BigDecimal . BigQuery.scientificToText) . decimal name
|
||
|
where
|
||
|
name = [G.name|bigquery_bigdecimal|]
|
||
|
|
||
|
bqDecimal :: MonadParse m => Parser origin 'Both m BigQuery.Decimal
|
||
|
bqDecimal = mkScalar name "Decimals. Accepts both string and number literals." $ fmap (BigQuery.Decimal . BigQuery.scientificToText) . decimal name
|
||
|
where
|
||
|
name = [G.name|bigquery_decimal|]
|
||
|
|
||
|
decimal :: MonadParse f => Name -> InputValue Variable -> f Scientific
|
||
|
decimal name = \case
|
||
|
GraphQLValue (VFloat f) -> pure f
|
||
|
GraphQLValue (VInt i) -> pure $ S.scientific i 0
|
||
|
GraphQLValue (VString s)
|
||
|
| Just sci <- readMaybe (Text.unpack s) -> pure $ sci
|
||
|
| otherwise -> stringNotationError name s
|
||
|
JSONValue (A.Number n) -> pure n
|
||
|
JSONValue (A.String s)
|
||
|
| Just sci <- readMaybe (Text.unpack s) -> pure $ sci
|
||
|
| otherwise -> stringNotationError name s
|
||
|
v -> typeMismatch name "decimal" v
|
||
|
|
||
|
--------------------------------------------------------------------------------
|
||
|
-- Local helpers
|
||
|
|
||
|
mkScalar ::
|
||
|
MonadParse m =>
|
||
|
Name ->
|
||
|
Description ->
|
||
|
(InputValue Variable -> m a) ->
|
||
|
Parser origin 'Both m a
|
||
|
mkScalar name desc parser =
|
||
|
Parser
|
||
|
{ pType = schemaType,
|
||
|
pParser = peelVariable (toGraphQLType schemaType) >=> parser
|
||
|
}
|
||
|
where
|
||
|
schemaType = typeNamed name (Just desc)
|
||
|
|
||
|
typeNamed :: Name -> Maybe Description -> Type origin 'Both
|
||
|
typeNamed name description = TNamed NonNullable $ Definition name description Nothing [] TIScalar
|
||
|
|
||
|
stringNotationError :: MonadParse m => G.Name -> Text -> m a
|
||
|
stringNotationError typeName actualString =
|
||
|
parseError $
|
||
|
"expected "
|
||
|
<> toErrorMessage (tshow typeName)
|
||
|
<> " represented as a string, but got "
|
||
|
<> dquote actualString
|
||
|
<> ", which is not a recognizable "
|
||
|
<> toErrorMessage (tshow typeName)
|
||
|
<> "."
|