-- | Check version compatability against postgres-like flavors.
module Hasura.Backends.Postgres.Connection.VersionCheck
  ( runCockroachVersionCheck,
    CockroachDbVersion (..),
    parseCrdbVersion,
    crdbVersionIsSupported,
  )
where

import Data.Aeson (object, (.=))
import Data.Aeson.Types (Pair)
import Data.Environment qualified as Env
import Database.PG.Query qualified as PG
import Hasura.Backends.Postgres.Connection qualified as PG
import Hasura.Backends.Postgres.Connection.Connect (withPostgresDB)
import Hasura.Base.Error
import Hasura.Prelude
import Text.Parsec qualified as P
import Text.Parsec.Text qualified as P

-- * Cockroach

-- | Cockroach version
data CockroachDbVersion = CockroachDbVersion
  { CockroachDbVersion -> Word
crdbMajor :: Word,
    CockroachDbVersion -> Word
crdbMinor :: Word,
    CockroachDbVersion -> Word
crdbPatch :: Word,
    -- | includes additional information such as "-beta.4"
    CockroachDbVersion -> String
crdbRest :: String
  }
  deriving (CockroachDbVersion -> CockroachDbVersion -> Bool
(CockroachDbVersion -> CockroachDbVersion -> Bool)
-> (CockroachDbVersion -> CockroachDbVersion -> Bool)
-> Eq CockroachDbVersion
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CockroachDbVersion -> CockroachDbVersion -> Bool
== :: CockroachDbVersion -> CockroachDbVersion -> Bool
$c/= :: CockroachDbVersion -> CockroachDbVersion -> Bool
/= :: CockroachDbVersion -> CockroachDbVersion -> Bool
Eq, Int -> CockroachDbVersion -> ShowS
[CockroachDbVersion] -> ShowS
CockroachDbVersion -> String
(Int -> CockroachDbVersion -> ShowS)
-> (CockroachDbVersion -> String)
-> ([CockroachDbVersion] -> ShowS)
-> Show CockroachDbVersion
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> CockroachDbVersion -> ShowS
showsPrec :: Int -> CockroachDbVersion -> ShowS
$cshow :: CockroachDbVersion -> String
show :: CockroachDbVersion -> String
$cshowList :: [CockroachDbVersion] -> ShowS
showList :: [CockroachDbVersion] -> ShowS
Show)

-- | Check cockroachdb version compatability.
runCockroachVersionCheck :: Env.Environment -> PG.PostgresConnConfiguration -> IO (Either QErr ())
runCockroachVersionCheck :: Environment -> PostgresConnConfiguration -> IO (Either QErr ())
runCockroachVersionCheck Environment
env PostgresConnConfiguration
connConf = do
  Either QErr (SingleRow (Identity Text))
result <-
    Environment
-> PostgresConnConfiguration
-> TxET QErr IO (SingleRow (Identity Text))
-> IO (Either QErr (SingleRow (Identity Text)))
forall a.
Environment
-> PostgresConnConfiguration
-> TxET QErr IO a
-> IO (Either QErr a)
withPostgresDB Environment
env PostgresConnConfiguration
connConf
      (TxET QErr IO (SingleRow (Identity Text))
 -> IO (Either QErr (SingleRow (Identity Text))))
-> TxET QErr IO (SingleRow (Identity Text))
-> IO (Either QErr (SingleRow (Identity Text)))
forall a b. (a -> b) -> a -> b
$ (PGTxErr -> QErr)
-> Query
-> [PrepArg]
-> Bool
-> TxET QErr IO (SingleRow (Identity Text))
forall (m :: * -> *) a e.
(MonadIO m, FromRes a) =>
(PGTxErr -> e) -> Query -> [PrepArg] -> Bool -> TxET e m a
PG.rawQE PGTxErr -> QErr
PG.dmlTxErrorHandler (Text -> Query
PG.fromText Text
"select version();") [] Bool
False
  Either QErr () -> IO (Either QErr ())
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure case Either QErr (SingleRow (Identity Text))
result of
    -- running the query failed
    Left QErr
err ->
      QErr -> Either QErr ()
forall a b. a -> Either a b
Left QErr
err
    -- running the query succeeded
    Right (PG.SingleRow (Identity Text
versionString)) ->
      case Text -> Either ParseError CockroachDbVersion
parseCrdbVersion Text
versionString of
        -- parsing the query output failed
        Left ParseError
err ->
          QErr -> Either QErr ()
forall a b. a -> Either a b
Left
            (QErr -> Either QErr ()) -> QErr -> Either QErr ()
forall a b. (a -> b) -> a -> b
$ [Pair] -> QErr
crdbVersionCheckErr500
              [ Key
"version-parse-error" Key -> String -> Pair
forall kv v. (KeyValue kv, ToJSON v) => Key -> v -> kv
forall v. ToJSON v => Key -> v -> Pair
.= ParseError -> String
forall a. Show a => a -> String
show ParseError
err,
                Key
"version-string" Key -> Text -> Pair
forall kv v. (KeyValue kv, ToJSON v) => Key -> v -> kv
forall v. ToJSON v => Key -> v -> Pair
.= Text
versionString
              ]
        -- parsing the query output succeeded
        Right CockroachDbVersion
crdbVersion ->
          if CockroachDbVersion -> Bool
crdbVersionIsSupported CockroachDbVersion
crdbVersion
            then -- the crdb version is supported
              () -> Either QErr ()
forall a b. b -> Either a b
Right ()
            else -- the crdb version is not supported

              QErr -> Either QErr ()
forall a b. a -> Either a b
Left
                (QErr -> Either QErr ()) -> QErr -> Either QErr ()
forall a b. (a -> b) -> a -> b
$ [Pair] -> QErr
crdbVersionCheckErr500
                  [ Key
"version-string" Key -> Text -> Pair
forall kv v. (KeyValue kv, ToJSON v) => Key -> v -> kv
forall v. ToJSON v => Key -> v -> Pair
.= Text
versionString
                  ]

crdbVersionCheckErr500 :: [Pair] -> QErr
crdbVersionCheckErr500 :: [Pair] -> QErr
crdbVersionCheckErr500 [Pair]
extra =
  ( Code -> Text -> QErr
err500
      Code
ValidationFailed
      Text
"Unsupported CockroachDB version. Supported versions: v22.2 onwards."
  )
    { qeInternal :: Maybe QErrExtra
qeInternal = QErrExtra -> Maybe QErrExtra
forall a. a -> Maybe a
Just (QErrExtra -> Maybe QErrExtra) -> QErrExtra -> Maybe QErrExtra
forall a b. (a -> b) -> a -> b
$ Value -> QErrExtra
ExtraInternal ([Pair] -> Value
object [Pair]
extra)
    }

-- | Check version is >= 22.2.0
-- https://hasura.io/docs/latest/databases/postgres/cockroachdb/index
crdbVersionIsSupported :: CockroachDbVersion -> Bool
crdbVersionIsSupported :: CockroachDbVersion -> Bool
crdbVersionIsSupported CockroachDbVersion {Word
crdbMajor :: CockroachDbVersion -> Word
crdbMajor :: Word
crdbMajor, Word
crdbMinor :: CockroachDbVersion -> Word
crdbMinor :: Word
crdbMinor, Word
crdbPatch :: CockroachDbVersion -> Word
crdbPatch :: Word
crdbPatch} =
  (Word
crdbMajor Word -> Word -> Bool
forall a. Ord a => a -> a -> Bool
> Word
22)
    Bool -> Bool -> Bool
|| (Word
crdbMajor Word -> Word -> Bool
forall a. Eq a => a -> a -> Bool
== Word
22 Bool -> Bool -> Bool
&& Word
crdbMinor Word -> Word -> Bool
forall a. Ord a => a -> a -> Bool
> Word
2)
    Bool -> Bool -> Bool
|| (Word
crdbMajor Word -> Word -> Bool
forall a. Eq a => a -> a -> Bool
== Word
22 Bool -> Bool -> Bool
&& Word
crdbMinor Word -> Word -> Bool
forall a. Eq a => a -> a -> Bool
== Word
2 Bool -> Bool -> Bool
&& Word
crdbPatch Word -> Word -> Bool
forall a. Ord a => a -> a -> Bool
>= Word
0)

-- | Parse a cockroachDB version string
parseCrdbVersion :: Text -> Either P.ParseError CockroachDbVersion
parseCrdbVersion :: Text -> Either ParseError CockroachDbVersion
parseCrdbVersion Text
versionString = Parsec Text () CockroachDbVersion
-> String -> Text -> Either ParseError CockroachDbVersion
forall s t a.
Stream s Identity t =>
Parsec s () a -> String -> s -> Either ParseError a
P.parse Parsec Text () CockroachDbVersion
crdbVersionParser String
"select version();" Text
versionString

-- | Cockroach DB version parser.
-- Example version string:
-- > "CockroachDB CCL v22.2.0-beta.4 (x86_64-pc-linux-gnu, built 2022/10/17 14:34:07, go1.19.1)"
crdbVersionParser :: P.Parser CockroachDbVersion
crdbVersionParser :: Parsec Text () CockroachDbVersion
crdbVersionParser = do
  String
_ <- String -> ParsecT Text () Identity String
forall s (m :: * -> *) u.
Stream s m Char =>
String -> ParsecT s u m String
P.string String
"CockroachDB"
  Char
_ <- ParsecT Text () Identity Char
forall s (m :: * -> *) u. Stream s m Char => ParsecT s u m Char
P.space
  String
_distribution <- ParsecT Text () Identity String
forall u. ParsecT Text u Identity String
word
  Char
_ <- ParsecT Text () Identity Char
forall s (m :: * -> *) u. Stream s m Char => ParsecT s u m Char
P.space
  Char
_ <- Char -> ParsecT Text () Identity Char
forall s (m :: * -> *) u.
Stream s m Char =>
Char -> ParsecT s u m Char
P.char Char
'v'
  Word
crdbMajor <- String -> Word
forall a. Read a => String -> a
read (String -> Word)
-> ParsecT Text () Identity String -> ParsecT Text () Identity Word
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ParsecT Text () Identity Char -> ParsecT Text () Identity String
forall s u (m :: * -> *) a. ParsecT s u m a -> ParsecT s u m [a]
P.many ParsecT Text () Identity Char
forall s (m :: * -> *) u. Stream s m Char => ParsecT s u m Char
P.digit
  Char
_ <- Char -> ParsecT Text () Identity Char
forall s (m :: * -> *) u.
Stream s m Char =>
Char -> ParsecT s u m Char
P.char Char
'.'
  Word
crdbMinor <- String -> Word
forall a. Read a => String -> a
read (String -> Word)
-> ParsecT Text () Identity String -> ParsecT Text () Identity Word
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ParsecT Text () Identity Char -> ParsecT Text () Identity String
forall s u (m :: * -> *) a. ParsecT s u m a -> ParsecT s u m [a]
P.many ParsecT Text () Identity Char
forall s (m :: * -> *) u. Stream s m Char => ParsecT s u m Char
P.digit
  Char
_ <- Char -> ParsecT Text () Identity Char
forall s (m :: * -> *) u.
Stream s m Char =>
Char -> ParsecT s u m Char
P.char Char
'.'
  Word
crdbPatch <- String -> Word
forall a. Read a => String -> a
read (String -> Word)
-> ParsecT Text () Identity String -> ParsecT Text () Identity Word
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ParsecT Text () Identity Char -> ParsecT Text () Identity String
forall s u (m :: * -> *) a. ParsecT s u m a -> ParsecT s u m [a]
P.many ParsecT Text () Identity Char
forall s (m :: * -> *) u. Stream s m Char => ParsecT s u m Char
P.digit
  String
crdbRest <- ParsecT Text () Identity String
forall u. ParsecT Text u Identity String
word
  CockroachDbVersion -> Parsec Text () CockroachDbVersion
forall a. a -> ParsecT Text () Identity a
forall (f :: * -> *) a. Applicative f => a -> f a
pure CockroachDbVersion {String
Word
crdbMajor :: Word
crdbMinor :: Word
crdbPatch :: Word
crdbRest :: String
crdbMajor :: Word
crdbMinor :: Word
crdbPatch :: Word
crdbRest :: String
..}

word :: P.ParsecT Text u Identity String
word :: forall u. ParsecT Text u Identity String
word = ParsecT Text u Identity Char -> ParsecT Text u Identity String
forall s u (m :: * -> *) a. ParsecT s u m a -> ParsecT s u m [a]
P.many (ParsecT Text u Identity Char
forall s (m :: * -> *) u. Stream s m Char => ParsecT s u m Char
P.alphaNum ParsecT Text u Identity Char
-> ParsecT Text u Identity Char -> ParsecT Text u Identity Char
forall a.
ParsecT Text u Identity a
-> ParsecT Text u Identity a -> ParsecT Text u Identity a
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> String -> ParsecT Text u Identity Char
forall s (m :: * -> *) u.
Stream s m Char =>
String -> ParsecT s u m Char
P.oneOf String
"-~!@#$%^&*=_.,")