2022-02-08 12:24:34 +03:00
|
|
|
|
-- | Postgres DDL Table
|
|
|
|
|
--
|
|
|
|
|
-- Used to fill up the enum values field of 'Hasura.RQL.Types.Table.TableCoreInfoG'.
|
|
|
|
|
--
|
|
|
|
|
-- See 'Hasura.RQL.Types.Eventing.Backend'.
|
2021-02-14 09:07:52 +03:00
|
|
|
|
module Hasura.Backends.Postgres.DDL.Table
|
2021-09-24 01:56:37 +03:00
|
|
|
|
( fetchAndValidateEnumValues,
|
2021-02-14 09:07:52 +03:00
|
|
|
|
)
|
|
|
|
|
where
|
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
|
import Control.Monad.Trans.Control (MonadBaseControl)
|
|
|
|
|
import Control.Monad.Validate
|
|
|
|
|
import Data.HashMap.Strict qualified as Map
|
|
|
|
|
import Data.List (delete)
|
|
|
|
|
import Data.List.NonEmpty qualified as NE
|
|
|
|
|
import Data.Sequence qualified as Seq
|
2022-07-18 18:15:34 +03:00
|
|
|
|
import Data.Sequence.NESeq qualified as NESeq
|
2021-09-24 01:56:37 +03:00
|
|
|
|
import Data.Text.Extended
|
|
|
|
|
import Database.PG.Query qualified as Q
|
|
|
|
|
import Hasura.Backends.Postgres.Connection
|
|
|
|
|
import Hasura.Backends.Postgres.SQL.DML
|
|
|
|
|
import Hasura.Backends.Postgres.SQL.Types
|
|
|
|
|
import Hasura.Base.Error
|
|
|
|
|
import Hasura.Prelude
|
|
|
|
|
import Hasura.RQL.Types.Backend
|
|
|
|
|
import Hasura.RQL.Types.Column
|
|
|
|
|
import Hasura.RQL.Types.Table
|
|
|
|
|
import Hasura.SQL.Backend
|
|
|
|
|
import Hasura.SQL.Types
|
|
|
|
|
import Hasura.Server.Utils
|
|
|
|
|
import Language.GraphQL.Draft.Syntax qualified as G
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
|
data EnumTableIntegrityError (b :: BackendType)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
= EnumTablePostgresError !Text
|
|
|
|
|
| EnumTableMissingPrimaryKey
|
|
|
|
|
| EnumTableMultiColumnPrimaryKey ![PGCol]
|
2021-04-22 00:44:37 +03:00
|
|
|
|
| EnumTableNonTextualPrimaryKey !(RawColumnInfo b)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
| EnumTableNoEnumValues
|
|
|
|
|
| EnumTableInvalidEnumValueNames !(NE.NonEmpty Text)
|
2021-04-22 00:44:37 +03:00
|
|
|
|
| EnumTableNonTextualCommentColumn !(RawColumnInfo b)
|
2021-02-14 09:07:52 +03:00
|
|
|
|
| EnumTableTooManyColumns ![PGCol]
|
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
|
fetchAndValidateEnumValues ::
|
|
|
|
|
forall pgKind m.
|
|
|
|
|
(Backend ('Postgres pgKind), MonadIO m, MonadBaseControl IO m) =>
|
|
|
|
|
PGSourceConfig ->
|
|
|
|
|
QualifiedTable ->
|
|
|
|
|
Maybe (PrimaryKey ('Postgres pgKind) (RawColumnInfo ('Postgres pgKind))) ->
|
|
|
|
|
[RawColumnInfo ('Postgres pgKind)] ->
|
|
|
|
|
m (Either QErr EnumValues)
|
|
|
|
|
fetchAndValidateEnumValues pgSourceConfig tableName maybePrimaryKey columnInfos =
|
|
|
|
|
runExceptT $
|
|
|
|
|
either (throw400 ConstraintViolation . showErrors) pure =<< runValidateT fetchAndValidate
|
2021-02-14 09:07:52 +03:00
|
|
|
|
where
|
2021-09-24 01:56:37 +03:00
|
|
|
|
fetchAndValidate ::
|
|
|
|
|
(MonadIO n, MonadBaseControl IO n, MonadValidate [EnumTableIntegrityError ('Postgres pgKind)] n) =>
|
|
|
|
|
n EnumValues
|
2021-02-14 09:07:52 +03:00
|
|
|
|
fetchAndValidate = do
|
|
|
|
|
maybePrimaryKeyColumn <- tolerate validatePrimaryKey
|
|
|
|
|
maybeCommentColumn <- validateColumns maybePrimaryKeyColumn
|
|
|
|
|
case maybePrimaryKeyColumn of
|
2021-09-24 01:56:37 +03:00
|
|
|
|
Nothing -> refute mempty
|
2021-02-14 09:07:52 +03:00
|
|
|
|
Just primaryKeyColumn -> do
|
2021-09-24 01:56:37 +03:00
|
|
|
|
result <-
|
|
|
|
|
runPgSourceReadTx pgSourceConfig $
|
|
|
|
|
runValidateT $
|
|
|
|
|
fetchEnumValuesFromDb tableName primaryKeyColumn maybeCommentColumn
|
2021-02-14 09:07:52 +03:00
|
|
|
|
case result of
|
2021-09-24 01:56:37 +03:00
|
|
|
|
Left e -> (refute . pure . EnumTablePostgresError . qeError) e
|
2021-02-14 09:07:52 +03:00
|
|
|
|
Right (Left vErrs) -> refute vErrs
|
2021-09-24 01:56:37 +03:00
|
|
|
|
Right (Right r) -> pure r
|
2021-02-14 09:07:52 +03:00
|
|
|
|
where
|
|
|
|
|
validatePrimaryKey = case maybePrimaryKey of
|
|
|
|
|
Nothing -> refute [EnumTableMissingPrimaryKey]
|
|
|
|
|
Just primaryKey -> case _pkColumns primaryKey of
|
2022-01-19 11:37:50 +03:00
|
|
|
|
column NESeq.:<|| Seq.Empty -> case rciType column of
|
2021-02-14 09:07:52 +03:00
|
|
|
|
PGText -> pure column
|
2021-09-24 01:56:37 +03:00
|
|
|
|
_ -> refute [EnumTableNonTextualPrimaryKey column]
|
2022-01-19 11:37:50 +03:00
|
|
|
|
columns -> refute [EnumTableMultiColumnPrimaryKey $ map rciName (toList columns)]
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
|
|
|
|
validateColumns primaryKeyColumn = do
|
|
|
|
|
let nonPrimaryKeyColumns = maybe columnInfos (`delete` columnInfos) primaryKeyColumn
|
|
|
|
|
case nonPrimaryKeyColumns of
|
|
|
|
|
[] -> pure Nothing
|
2022-01-19 11:37:50 +03:00
|
|
|
|
[column] -> case rciType column of
|
2021-02-14 09:07:52 +03:00
|
|
|
|
PGText -> pure $ Just column
|
2021-09-24 01:56:37 +03:00
|
|
|
|
_ -> dispute [EnumTableNonTextualCommentColumn column] $> Nothing
|
2022-01-19 11:37:50 +03:00
|
|
|
|
columns -> dispute [EnumTableTooManyColumns $ map rciName columns] $> Nothing
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
|
showErrors :: [EnumTableIntegrityError ('Postgres pgKind)] -> Text
|
2021-02-14 09:07:52 +03:00
|
|
|
|
showErrors allErrors =
|
|
|
|
|
"the table " <> tableName <<> " cannot be used as an enum " <> reasonsMessage
|
|
|
|
|
where
|
|
|
|
|
reasonsMessage = makeReasonMessage allErrors showOne
|
|
|
|
|
|
2021-04-22 00:44:37 +03:00
|
|
|
|
showOne :: EnumTableIntegrityError ('Postgres pgKind) -> Text
|
2021-02-14 09:07:52 +03:00
|
|
|
|
showOne = \case
|
|
|
|
|
EnumTablePostgresError err -> "postgres error: " <> err
|
|
|
|
|
EnumTableMissingPrimaryKey -> "the table must have a primary key"
|
|
|
|
|
EnumTableMultiColumnPrimaryKey cols ->
|
|
|
|
|
"the table’s primary key must not span multiple columns ("
|
2021-09-24 01:56:37 +03:00
|
|
|
|
<> commaSeparated (sort cols)
|
|
|
|
|
<> ")"
|
2021-02-14 09:07:52 +03:00
|
|
|
|
EnumTableNonTextualPrimaryKey colInfo -> typeMismatch "primary key" colInfo PGText
|
|
|
|
|
EnumTableNoEnumValues -> "the table must have at least one row"
|
|
|
|
|
EnumTableInvalidEnumValueNames values ->
|
|
|
|
|
let pluralString = " are not valid GraphQL enum value names"
|
|
|
|
|
valuesString = case NE.reverse (NE.sort values) of
|
|
|
|
|
value NE.:| [] -> "value " <> value <<> " is not a valid GraphQL enum value name"
|
|
|
|
|
value2 NE.:| [value1] -> "values " <> value1 <<> " and " <> value2 <<> pluralString
|
|
|
|
|
lastValue NE.:| otherValues ->
|
|
|
|
|
"values " <> commaSeparated (reverse otherValues) <> ", and "
|
|
|
|
|
<> lastValue <<> pluralString
|
2021-09-24 01:56:37 +03:00
|
|
|
|
in "the " <> valuesString
|
2021-02-14 09:07:52 +03:00
|
|
|
|
EnumTableNonTextualCommentColumn colInfo -> typeMismatch "comment column" colInfo PGText
|
|
|
|
|
EnumTableTooManyColumns cols ->
|
|
|
|
|
"the table must have exactly one primary key and optionally one comment column, not "
|
2021-09-24 01:56:37 +03:00
|
|
|
|
<> tshow (length cols)
|
|
|
|
|
<> " columns ("
|
|
|
|
|
<> commaSeparated (sort cols)
|
|
|
|
|
<> ")"
|
2021-02-14 09:07:52 +03:00
|
|
|
|
where
|
|
|
|
|
typeMismatch description colInfo expected =
|
2022-01-19 11:37:50 +03:00
|
|
|
|
"the table’s " <> description <> " (" <> rciName colInfo <<> ") must have type "
|
|
|
|
|
<> expected <<> ", not type " <>> rciType colInfo
|
2021-02-14 09:07:52 +03:00
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
|
fetchEnumValuesFromDb ::
|
|
|
|
|
forall pgKind m.
|
|
|
|
|
(MonadTx m, MonadValidate [EnumTableIntegrityError ('Postgres pgKind)] m) =>
|
|
|
|
|
QualifiedTable ->
|
|
|
|
|
RawColumnInfo ('Postgres pgKind) ->
|
|
|
|
|
Maybe (RawColumnInfo ('Postgres pgKind)) ->
|
|
|
|
|
m EnumValues
|
2021-02-14 09:07:52 +03:00
|
|
|
|
fetchEnumValuesFromDb tableName primaryKeyColumn maybeCommentColumn = do
|
|
|
|
|
let nullExtr = Extractor SENull Nothing
|
2022-01-19 11:37:50 +03:00
|
|
|
|
commentExtr = maybe nullExtr (mkExtr . rciName) maybeCommentColumn
|
2021-09-24 01:56:37 +03:00
|
|
|
|
query =
|
|
|
|
|
Q.fromBuilder $
|
|
|
|
|
toSQL
|
|
|
|
|
mkSelect
|
|
|
|
|
{ selFrom = Just $ mkSimpleFromExp tableName,
|
2022-01-19 11:37:50 +03:00
|
|
|
|
selExtr = [mkExtr (rciName primaryKeyColumn), commentExtr]
|
2021-09-24 01:56:37 +03:00
|
|
|
|
}
|
2021-02-14 09:07:52 +03:00
|
|
|
|
rawEnumValues <- liftTx $ Q.withQE defaultTxErrorHandler query () True
|
|
|
|
|
when (null rawEnumValues) $ dispute [EnumTableNoEnumValues]
|
|
|
|
|
let enumValues = flip map rawEnumValues $
|
|
|
|
|
\(enumValueText, comment) ->
|
|
|
|
|
case mkValidEnumValueName enumValueText of
|
2021-09-24 01:56:37 +03:00
|
|
|
|
Nothing -> Left enumValueText
|
2021-02-14 09:07:52 +03:00
|
|
|
|
Just enumValue -> Right (EnumValue enumValue, EnumValueInfo comment)
|
|
|
|
|
badNames = lefts enumValues
|
|
|
|
|
validEnums = rights enumValues
|
|
|
|
|
case NE.nonEmpty badNames of
|
|
|
|
|
Just someBadNames -> refute [EnumTableInvalidEnumValueNames someBadNames]
|
2021-09-24 01:56:37 +03:00
|
|
|
|
Nothing -> pure $ Map.fromList validEnums
|
2021-02-14 09:07:52 +03:00
|
|
|
|
where
|
|
|
|
|
-- https://graphql.github.io/graphql-spec/June2018/#EnumValue
|
|
|
|
|
mkValidEnumValueName name =
|
2021-09-24 01:56:37 +03:00
|
|
|
|
if name `elem` ["true", "false", "null"]
|
|
|
|
|
then Nothing
|
|
|
|
|
else G.mkName name
|