mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-16 18:42:30 +03:00
f7a202a363
This will implement BigQuery support. Co-authored-by: Antoine Leblanc <1618949+nicuveo@users.noreply.github.com> Co-authored-by: Sibi Prabakaran <737477+psibi@users.noreply.github.com> Co-authored-by: Aniket Deshpande <922486+aniketd@users.noreply.github.com> Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com> GitOrigin-RevId: 1a6ffaf34233e13e8125a5c908eaa7e32d65007b
266 lines
7.8 KiB
Haskell
266 lines
7.8 KiB
Haskell
{-# LANGUAGE DuplicateRecordFields #-}
|
|
|
|
-- |
|
|
|
|
module Hasura.Backends.BigQuery.Meta
|
|
( MetadataError(..)
|
|
, getTables
|
|
, RestTableReference(..)
|
|
, RestTable(..)
|
|
, RestTableSchema(..)
|
|
, RestFieldSchema(..)
|
|
, RestType(..)
|
|
, Mode(..)
|
|
) where
|
|
|
|
|
|
import Control.Exception.Safe
|
|
import Control.Monad.IO.Class
|
|
import Control.Monad.Trans.Except
|
|
import Data.Aeson
|
|
import qualified Data.Aeson as Aeson
|
|
import Data.Foldable
|
|
import Data.Maybe
|
|
import qualified Data.Sequence as Seq
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import GHC.Generics
|
|
import Hasura.Backends.BigQuery.Connection
|
|
import Hasura.Backends.BigQuery.Source
|
|
import Network.HTTP.Simple
|
|
import Network.HTTP.Types
|
|
import Prelude
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Types
|
|
|
|
data MetadataError
|
|
= RestProblem RestProblem
|
|
deriving (Show)
|
|
|
|
data RestProblem
|
|
= GetTablesProblem SomeException
|
|
| GetTableProblem SomeException
|
|
| GetMetaDecodeProblem String
|
|
| GetTablesBigQueryProblem BigQueryProblem
|
|
| RESTRequestNonOK Status
|
|
deriving (Show)
|
|
|
|
data RestTableList = RestTableList
|
|
{ nextPageToken :: Maybe Text
|
|
, tables :: [RestTableBrief]
|
|
} deriving (Show)
|
|
|
|
instance FromJSON RestTableList where
|
|
parseJSON =
|
|
withObject
|
|
"RestTableList"
|
|
(\o -> do
|
|
kind <- o .: "kind"
|
|
case kind of
|
|
("bigquery#tableList"::Text) -> do
|
|
nextPageToken <- o .:? "nextPageToken"
|
|
tables <- o .: "tables"
|
|
pure RestTableList {..}
|
|
_ -> fail "Expected kind of bigquery#tableList")
|
|
|
|
data RestTableBrief = RestTableBrief
|
|
{ tableReference :: RestTableReference
|
|
} deriving (Show, Generic)
|
|
instance FromJSON RestTableBrief
|
|
|
|
data RestTableReference = RestTableReference
|
|
{ datasetId :: Text
|
|
, projectId :: Text
|
|
, tableId :: Text
|
|
} deriving (Show, Generic)
|
|
instance FromJSON RestTableReference
|
|
|
|
data RestTable = RestTable
|
|
{ tableReference :: RestTableReference
|
|
, schema :: RestTableSchema
|
|
} deriving (Show, Generic)
|
|
instance FromJSON RestTable
|
|
|
|
data RestTableSchema = RestTableSchema
|
|
{ fields :: [RestFieldSchema]
|
|
} deriving (Show, Generic)
|
|
instance FromJSON RestTableSchema
|
|
|
|
data RestFieldSchema = RestFieldSchema
|
|
{ name :: Text
|
|
, type' :: RestType
|
|
-- ^ The field data type. Possible values include STRING, BYTES,
|
|
-- INTEGER, INT64 (same as INTEGER), FLOAT, FLOAT64 (same as
|
|
-- FLOAT), BOOLEAN, BOOL (same as BOOLEAN), TIMESTAMP, DATE, TIME,
|
|
-- DATETIME, GEOGRAPHY, NUMERIC, RECORD (where RECORD indicates
|
|
-- that the field contains a nested schema) or STRUCT (same as
|
|
-- RECORD).
|
|
, mode :: Mode
|
|
-- The field mode. Possible values include NULLABLE, REQUIRED and
|
|
-- REPEATED. The default value is NULLABLE.
|
|
} deriving (Show, Generic)
|
|
instance FromJSON RestFieldSchema where
|
|
parseJSON =
|
|
withObject
|
|
"RestFieldSchema"
|
|
(\o -> do
|
|
type' <- o .: "type"
|
|
name <- o .: "name"
|
|
mode <- fmap (fromMaybe Nullable) (o .:? "mode")
|
|
pure RestFieldSchema {..})
|
|
|
|
data Mode = Nullable | Required | Repeated deriving (Show)
|
|
instance FromJSON Mode where
|
|
parseJSON j = do
|
|
s <- parseJSON j
|
|
case s :: Text of
|
|
"NULLABLE" -> pure Nullable
|
|
"REQUIRED" -> pure Required
|
|
"REPEATED" -> pure Repeated
|
|
_ -> fail ("invalid mode " ++ show s)
|
|
|
|
data RestType
|
|
= STRING
|
|
| BYTES
|
|
| INTEGER
|
|
| FLOAT
|
|
| BOOL
|
|
| TIMESTAMP
|
|
| DATE
|
|
| TIME
|
|
| DATETIME
|
|
| GEOGRAPHY
|
|
| DECIMAL
|
|
| BIGDECIMAL
|
|
| STRUCT -- (same as RECORD).
|
|
deriving (Show)
|
|
instance FromJSON RestType where
|
|
parseJSON j = do
|
|
s <- parseJSON j
|
|
case s :: Text of
|
|
"STRING" -> pure STRING
|
|
"BYTES" -> pure BYTES
|
|
"INTEGER" -> pure INTEGER
|
|
"INT64" -> pure INTEGER
|
|
"FLOAT" -> pure FLOAT
|
|
"FLOAT64" -> pure FLOAT
|
|
"BOOLEAN" -> pure BOOL
|
|
"BOOL" -> pure BOOL
|
|
"TIMESTAMP" -> pure TIMESTAMP
|
|
"DATE" -> pure DATE
|
|
"TIME" -> pure TIME
|
|
"DATETIME" -> pure DATETIME
|
|
"GEOGRAPHY" -> pure GEOGRAPHY
|
|
"NUMERIC" -> pure DECIMAL
|
|
"DECIMAL" -> pure DECIMAL
|
|
"BIGNUMERIC" -> pure BIGDECIMAL
|
|
"BIGDECIMAL" -> pure BIGDECIMAL
|
|
"RECORD" -> pure STRUCT
|
|
"STRUCT" -> pure STRUCT
|
|
_ -> fail ("invalid type " ++ show s)
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- REST request
|
|
|
|
-- | Get all tables from all specified data sets.
|
|
getTables ::
|
|
MonadIO m
|
|
=> BigQuerySourceConfig
|
|
-> m (Either RestProblem [RestTable])
|
|
getTables sc@BigQuerySourceConfig {..} =
|
|
runExceptT
|
|
(fmap concat (traverse (ExceptT . getTablesForDataSet sc) _scDatasets))
|
|
|
|
-- | Get tables in the dataset.
|
|
getTablesForDataSet ::
|
|
MonadIO m
|
|
=> BigQuerySourceConfig
|
|
-> Text
|
|
-> m (Either RestProblem [RestTable])
|
|
getTablesForDataSet sc@BigQuerySourceConfig{..} dataSet = do
|
|
result <-
|
|
liftIO (catchAny (run Nothing mempty) (pure . Left . GetTablesProblem))
|
|
case result of
|
|
Left e -> pure (Left e)
|
|
Right briefs ->
|
|
fmap
|
|
sequence
|
|
(traverse
|
|
(\RestTableBrief {tableReference = RestTableReference {tableId}} ->
|
|
getTable sc dataSet tableId)
|
|
briefs)
|
|
where
|
|
run pageToken acc = do
|
|
let req = setRequestHeader "Content-Type" ["application/json"]
|
|
$ parseRequest_ url
|
|
eResp <- runBigQuery sc req
|
|
case eResp of
|
|
Left e -> pure (Left (GetTablesBigQueryProblem e))
|
|
Right resp ->
|
|
case getResponseStatusCode resp of
|
|
200 ->
|
|
case Aeson.eitherDecode (getResponseBody resp) of
|
|
Left e -> pure (Left (GetMetaDecodeProblem e))
|
|
Right RestTableList {nextPageToken, tables} ->
|
|
case nextPageToken of
|
|
Nothing -> pure (Right (toList (acc <> Seq.fromList tables)))
|
|
Just token -> run (pure token) (acc <> Seq.fromList tables)
|
|
_ -> pure (Left (RESTRequestNonOK (getResponseStatus resp)))
|
|
where
|
|
url =
|
|
"GET https://bigquery.googleapis.com/bigquery/v2/projects/" <>
|
|
T.unpack _scProjectId <>
|
|
"/datasets/" <>
|
|
T.unpack dataSet <>
|
|
"/tables?alt=json&key=" <>
|
|
-- T.unpack apiToken <>
|
|
"&" <>
|
|
T.unpack (encodeParams extraParameters)
|
|
extraParameters = pageTokenParam
|
|
where
|
|
pageTokenParam =
|
|
case pageToken of
|
|
Nothing -> []
|
|
Just token -> [("pageToken", token)]
|
|
|
|
-- | Get tables in the schema.
|
|
getTable ::
|
|
MonadIO m
|
|
=> BigQuerySourceConfig
|
|
-> Text -> Text
|
|
-> m (Either RestProblem RestTable)
|
|
getTable sc@BigQuerySourceConfig {..} dataSet tableId = do
|
|
liftIO (catchAny run (pure . Left . GetTableProblem))
|
|
where
|
|
run = do
|
|
let req =
|
|
setRequestHeader "Content-Type" ["application/json"] $
|
|
parseRequest_ url
|
|
eResp <- runBigQuery sc req
|
|
case eResp of
|
|
Left e -> pure (Left (GetTablesBigQueryProblem e))
|
|
Right resp ->
|
|
case getResponseStatusCode resp of
|
|
200 ->
|
|
case Aeson.eitherDecode (getResponseBody resp) of
|
|
Left e -> pure (Left (GetMetaDecodeProblem e))
|
|
Right table -> pure (Right table)
|
|
_ -> pure (Left (RESTRequestNonOK (getResponseStatus resp)))
|
|
where
|
|
url =
|
|
"GET https://bigquery.googleapis.com/bigquery/v2/projects/" <>
|
|
T.unpack _scProjectId <>
|
|
"/datasets/" <>
|
|
T.unpack dataSet <>
|
|
"/tables/" <>
|
|
T.unpack tableId <>
|
|
"?alt=json&key=" <>
|
|
-- T.unpack apiToken <>
|
|
"&" <>
|
|
T.unpack (encodeParams extraParameters)
|
|
extraParameters = []
|
|
|
|
encodeParams :: [(Text, Text)] -> Text
|
|
encodeParams = T.intercalate "&" . map (\(k, v) -> k <> "=" <> v)
|