presto-hdbc-0.1.0.2: src/Database/HDBC/Presto.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
module Database.HDBC.Presto (PrestoConnection(..), connectToPresto) where
import Control.Applicative ((<$>))
import Control.Concurrent (threadDelay)
import Control.Exception (throw)
import Control.Monad (when)
import Control.Monad.Trans
import Control.Monad.Trans.Either
import Data.Aeson
import qualified Data.ByteString as BN
import qualified Data.ByteString.Lazy as B
import Data.Convertible
import Data.Maybe
import Database.HDBC.PrestoData
import GHC.Word (Word16)
import Network.URI
import Safe (readMay)
-- TODO: remove
import Data.Either.Unwrap
import qualified Data.Text.Encoding as DE
import qualified Data.Text.Lazy.Encoding as LDE
import Data.Text.Lazy as T
import Data.Text.Lazy.Encoding (encodeUtf8)
import System.IO.Error
-- HDBC stuff
import Control.Concurrent.MVar(MVar, readMVar, modifyMVar, modifyMVar_, newMVar, withMVar, isEmptyMVar)
import Database.HDBC (IConnection(..), SqlValue, fetchAllRows, SqlError(..))
import Database.HDBC.ColTypes as CT
import Database.HDBC.SqlValue
import Database.HDBC.Statement (Statement(..))
import Network.Http.Client
import qualified System.IO.Streams as Streams
import Util
instance IConnection PrestoConnection where
disconnect = disconnectPresto
commit _ = fail "Not implemented"
rollback _ = fail "Not implemented"
run _ _ _ = fail "Not implemented"
prepare = preparePresto
clone _ = fail "Not implemented"
hdbcDriverName _ = "presto"
hdbcClientVer _ = fail "Not implemented"
proxiedClientName _ = fail "Not implemented"
proxiedClientVer _ = fail "Not implemented"
dbServerVer _ = fail "Not implemented"
dbTransactionSupport _ = False
getTables conn = do
showTables <- prepare conn "show tables"
_ <- execute showTables []
rows <- fetchAllRows showTables
-- Each row should be a single string
return $ fmap ((\(SqlString s) -> s) . Prelude.head) rows
describeTable conn name = do
statement <- prepare conn $ "describe " ++ name
execute statement []
rows <- fetchAllRows statement
-- Presto's describe table output is of the following form:
-- Column (varchar)
-- Type (varchar)
-- Null (boolean)
-- Partition Key (boolean)
-- Comment (boolean)
-- Pack the data into a SqlColDesc
return $ (flip fmap) rows (\((SqlString name):
(SqlString typ):
(SqlBool nullable):
(SqlBool partition):
(SqlString comment):
xs) ->
(name, SqlColDesc {
colType = prestoTypeToSqlColType typ,
colSize = Just $ if partition then 1 else 0, -- Hide the partition info here
colOctetLength = Nothing,
colDecDigits = Nothing,
colNullable = Just nullable
}))
data StmtState = Prepared
| Executed [PrestoColumn] [PrestoRow]
| Errored String
| Finished
columnsFromStatement :: PrestoStatement -> [PrestoColumn]
columnsFromStatement statement = case statement of
PrestoStatement (Executed cols rows) _ _ -> cols
_ -> fail "You have to call execute before getting column names"
type Catalog = T.Text
type Schema = T.Text
-- Store the uri in here for now
data PrestoConnection = PrestoConnection URI Connection Catalog Schema
data PrestoStatement = PrestoStatement StmtState PrestoConnection Text
connectToPresto :: URI -> T.Text -> T.Text -> IO (Either Text PrestoConnection)
connectToPresto uri catalog schema = runEitherT $ do
auth <- maybeToEitherT "URI didn't contain authority" $ uriAuthority uri
port <- maybeToEitherT "Couldn't read port from URI" $ readMay $ Prelude.tail $ uriPort auth
eitherConn <- liftIO $ tryIOError $ openConnection "localhost" port
conn <- hoistEither $ mapLeft (convert . show) eitherConn
return $ PrestoConnection uri conn catalog schema
fetchPrestoRow :: PrestoConnection -> MVar PrestoStatement -> IO (Maybe [SqlValue])
fetchPrestoRow conn prestoStatementVar = modifyMVar prestoStatementVar fetch where
fetch (PrestoStatement Prepared _ _) = fail "Trying to fetch before executing statement."
fetch statement@(PrestoStatement (Executed _ []) conn query) = do
let newStatement = PrestoStatement Finished conn query
return (newStatement, Nothing)
fetch statement@(PrestoStatement (Executed cols rows@(row:remainder)) conn query) = do
let newStatement = PrestoStatement (Executed cols remainder) conn query
return (newStatement, Just row)
fetch (PrestoStatement (Errored msg) _ _) = throw $ SqlError "Query failed" 0 msg
fetch (PrestoStatement Finished _ _ ) = throw $ SqlError "Can't execute an already executed statement." 0 ""
executePresto conn prestoStatementVar vals = modifyMVar prestoStatementVar exec where
exec stmt@(PrestoStatement (Executed _ _) _ _) = return (stmt, 0)
exec (PrestoStatement Prepared (PrestoConnection uri httpConn _ _) query) = do
result <- doPollingPrestoQuery httpConn conn uri query
let newStatement = case result of
Left msg -> throw $ SqlError "Query failed" 0 msg
Right (columns, rows) -> PrestoStatement (Executed columns rows) conn query
return (newStatement, 0)
preparePresto :: PrestoConnection -> String -> IO Statement
preparePresto conn query = do
prestoStatementVar <- newMVar $ PrestoStatement Prepared conn (T.pack query)
return Statement {
executeRaw = undefined
, execute = executePresto conn prestoStatementVar
, executeMany = \_ -> fail "Not implemented"
, finish = fail "Not implemented"
, fetchRow = fetchPrestoRow conn prestoStatementVar
, originalQuery = query
, getColumnNames = do
prestoStatement <- readMVar prestoStatementVar
let columns = columnsFromStatement prestoStatement
return $ fmap prestoColumn_name columns
, describeResult = do
prestoStatement <- readMVar prestoStatementVar
let columns = columnsFromStatement prestoStatement
return $ [(prestoColumn_name x, SqlColDesc {
colType = prestoTypeToSqlColType $ prestoColumn_type x,
colSize = Nothing,
colOctetLength = Nothing,
colDecDigits = Nothing,
colNullable = Nothing
}) | x <- columns]
}
disconnectPresto :: PrestoConnection -> IO ()
disconnectPresto conn = return ()
-- TODO: the JDBC version seems to do a DELETE when you close the StatementClient
------------------- Connection and polling stuff -------------------
getPrestoResponse :: (FromJSON a, ToJSON a) => Connection -> PrestoConnection -> URI -> Method -> B.ByteString -> EitherT String IO a
getPrestoResponse conn (PrestoConnection _ _ catalog schema) uri method body = do
request <- liftIO $ buildRequest $ do
http method (DE.encodeUtf8 $ convert $ uriPath uri)
setContentType "text/plain"
setAccept "*/*"
setHeader "X-Presto-User" "presto"
setHeader "X-Presto-Catalog" (DE.encodeUtf8 $ convert catalog)
setHeader "X-Presto-Schema" (DE.encodeUtf8 $ convert schema)
bodyStream <- liftIO $ Streams.fromByteString $ convert body
liftIO $ sendRequest conn request $ inputStreamBody bodyStream
-- maybeBytes <- liftIO $ receiveResponse conn (\_ i -> do
-- (i2, lenio) <- Streams.countInput i
-- len <- lenio
-- putStrLn $ "Bytes: " ++ show len
-- Streams.read i)
liftIO $ receiveResponse conn jsonHandler
pollForResult :: Connection -> PrestoConnection -> URI -> Int -> EitherT String IO ([PrestoColumn], [[SqlValue]])
pollForResult conn prestoConn uri attemptNo = do
liftIO $ threadDelay (attemptNo * 100000) -- Linear backoff in increments of 100ms = 100000 us
response <- getPrestoResponse conn prestoConn uri GET B.empty
case _data response of
Nothing -> do
when (isNothing $ nextUri response) $ do
iUri <- hoistEither $ maybeToEither "No data or nextUri and couldn't parse infoUri" $ parseAbsoluteURI (infoUri response)
info :: PrestoInfo <- getPrestoResponse conn prestoConn iUri GET B.empty
left $ "No data and no nextUri: " ++ show response ++ "\n\n" ++ "Info: " ++ show info
newUri <- case parseAbsoluteURI $ fromJust $ nextUri response of
Nothing -> left "Failed to parse nextUri"
Just newUri -> right newUri
pollForResult conn prestoConn newUri (attemptNo + 1)
Just dat -> case columns response of
Nothing -> left "Data but no columns found in response"
Just cols -> right (cols, fmap (makeSqlValues cols) dat)
initialUri = fromJust $ parseRelativeReference "/v1/statement"
doPollingPrestoQuery :: Connection -> PrestoConnection -> URI -> T.Text -> IO (Either String ([PrestoColumn], [PrestoRow]))
doPollingPrestoQuery conn prestoConn _ q = runEitherT $ do
initialResponse <- getPrestoResponse conn prestoConn initialUri POST (encodeUtf8 q)
uri <- maybeToEitherT "No nextUri found in initialResponse" $ nextUri initialResponse
newUri <- case parseAbsoluteURI uri of
Nothing -> left "Failed to parse nextUri"
Just newUri -> right newUri
pollForResult conn prestoConn newUri 0
------------------- SQL stuff -------------------
prestoTypeToSqlColType :: String -> CT.SqlTypeId
prestoTypeToSqlColType prestoType = case prestoType of
"varchar" -> SqlVarCharT
"bigint" -> SqlBigIntT
"boolean" -> SqlBitT
"double" -> SqlDoubleT
"date" -> SqlDateT
"time" -> SqlTimeT
"time_with_timezone" -> SqlTimeWithZoneT
"timestamp" -> SqlTimestampT
"timestamp_with_timezone" -> SqlTimestampWithZoneT
"interval_year_to_month" -> SqlIntervalT SqlIntervalYearToMonthT
"interval_year_to_second" -> error "Can't represent year to second as SqlTypeId (?)"
makeSqlValue :: PrestoColumn -> AnyValue -> SqlValue
makeSqlValue column value = case (prestoColumn_type column, value) of
("varchar", TextValue t) -> SqlString $ T.unpack $ T.fromStrict t
("double", NumValue s) -> SqlDouble (realToFrac s)
("bigint", NumValue s) -> SqlInteger $ floor $ realToFrac s
("boolean", BoolValue b) -> SqlBool b
(_, NullValue) -> SqlNull
_ -> error $ "Unrecognized Presto column type: " ++ (prestoColumn_type column)
makeSqlValues :: [PrestoColumn] -> [AnyValue] -> [SqlValue]
makeSqlValues columns strings = [makeSqlValue column string | (column, string) <- Prelude.zip columns strings]