content: {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
  
  {-# HLINT ignore "Use list comprehension" #-}
  {-# HLINT ignore "Replace case with maybe" #-}
  
  module AirGQL.GraphQL (
    getDerivedSchema,
    queryType,
    sqlDataToGQLValue,
    gqlValueToSQLData,
  )
  where
  
  import Protolude (
    Applicative (pure),
    Bool (False, True),
    Double,
    Either (Left, Right),
    Eq ((==)),
    IO,
    Int,
    Integer,
    Maybe (Just, Nothing),
    MonadIO (liftIO),
    MonadReader (ask),
    Monoid (mempty),
    ReaderT,
    Semigroup ((<>)),
    Text,
    fromIntegral,
    notElem,
    otherwise,
    show,
    when,
    ($),
    (&),
    (&&),
    (.),
    (<&>),
    (<=),
    (>),
    (>=),
    (||),
   )
  import Protolude qualified as P
  
  import Control.Exception (throw)
  import Control.Monad.Catch (catchAll)
  import Data.Aeson (object, (.=))
  import Data.HashMap.Strict (HashMap)
  import Data.HashMap.Strict qualified as HashMap
  import Data.List (nub)
  import Data.Ord (Ord (min))
  import Data.Text (intercalate, isInfixOf, pack, toUpper)
  import Data.Text qualified as T
  import Database.SQLite.Simple (
    Connection,
    Query (Query),
    SQLData (SQLBlob, SQLFloat, SQLInteger, SQLNull, SQLText),
    changes,
    execute_,
    query,
    query_,
   )
  import Database.SQLite.Simple qualified as SS
  import DoubleXEncoding (doubleXDecode, doubleXEncodeGql)
  import GHC.IO.Exception (userError)
  import Language.GraphQL.AST.Document (Name)
  import Language.GraphQL.Error (ResolverException (ResolverException))
  import Language.GraphQL.Type as GQL (
    Arguments (Arguments),
    Resolver (EventStreamResolver, ValueResolver),
    Schema,
    Value (Boolean, Enum, Float, Int, List, Null, Object, String),
    schema,
   )
  import Language.GraphQL.Type.Out qualified as Out
  import Numeric (showFFloat)
  
  import AirGQL.Config (
    maxGraphqlResultCount,
   )
  
  import AirGQL.Introspection qualified as Introspection
  import AirGQL.Introspection.NamingConflict (encodeOutsidePKNames)
  import AirGQL.Introspection.Resolver qualified as Introspection
  import AirGQL.Introspection.Types qualified as Introspection
  import AirGQL.Lib (
    AccessMode (canInsert),
    ColumnEntry (column_name, datatype),
    ObjectType (Table),
    TableEntry (columns, name, object_type),
    canRead,
    canWrite,
    column_name_gql,
   )
  import AirGQL.Types.OutObjectType (
    OutObjectType (OutObjectType, descriptionMb, fields, interfaceTypes, name),
    outObjectTypeToObjectType,
   )
  import AirGQL.Types.PragmaConf (getSQLitePragmas)
  import AirGQL.Types.SchemaConf (
    SchemaConf (accessMode, maxRowsPerTable, pragmaConf),
   )
  import AirGQL.Types.Utils (encodeToText)
  import AirGQL.Utils (colToFileUrl, quoteKeyword, quoteText)
  import Data.List qualified as List
  import Language.GraphQL.Class (FromGraphQL (fromGraphQL))
  
  
  -- | Prevent numbers of being shown with exponents (0.01 instead of 1e-2)
  showFullPrecision :: Double -> Text
  showFullPrecision x =
    pack $ showFFloat Nothing x ""
  
  
  showGqlValue :: Value -> Text
  showGqlValue = \case
    String str -> str
    Int integer -> show integer
    Float double -> showFullPrecision double
    Boolean bool -> show bool
    Enum text -> text
    List list -> "[" <> T.intercalate ", " (list <&> showGqlValue) <> "]"
    Object obj -> show $ Object obj
    Null -> "null"
  
  
  gqlValueToSQLText :: Value -> Text
  gqlValueToSQLText = \case
    String str -> quoteText str
    Int integer -> show integer
    Float double -> showFullPrecision double
    Boolean bool -> T.toUpper $ show bool
    Enum text -> text
    List list ->
      quoteText $
        "[" <> T.intercalate ", " (list <&> showGqlValue) <> "]"
    Object obj -> quoteText $ show $ Object obj
    Null -> "NULL"
  
  
  -- TODO: Add Support for GraphQL's type "ID"
  
  -- | Convert any GraphQL value to a nullable String
  gqlValueToNullableString :: Value -> Value
  gqlValueToNullableString value =
    case value of
      String text -> String text
      Null -> Null
      val -> String $ showGqlValue val
  
  
  buildSortClause :: [ColumnEntry] -> [(Name, Value)] -> Text
  buildSortClause columnEntries orderElems =
    if P.null orderElems
      then
        if "rowid" `P.elem` (columnEntries <&> T.toLower . AirGQL.Lib.column_name)
          then "ORDER BY rowid ASC"
          else ""
      else
        "ORDER BY "
          <> ( orderElems
                <&> ( \(name, value) ->
                        ( name
                        , case value of
                            Enum "ASC" -> "ASC"
                            Enum "asc" -> "ASC"
                            Enum "DESC" -> "DESC"
                            Enum "desc" -> "DESC"
                            _ -> ""
                        )
                    )
                <&> (\(name, order) -> name <> " " <> order)
                & T.intercalate ", "
             )
  
  
  data Pagination = Pagination
    { limit :: Int
    , offset :: Maybe Int
    }
  
  
  buildPaginationClause :: Maybe Pagination -> Text
  buildPaginationClause = \case
    Nothing -> ""
    Just pagination ->
      P.fold
        [ "LIMIT "
        , show (min pagination.limit maxGraphqlResultCount)
        , case pagination.offset of
            Nothing -> ""
            Just offset -> "\nOFFSET " <> show offset
        ]
  
  
  getColNamesQuoted :: [ColumnEntry] -> [Text]
  getColNamesQuoted columnEntries =
    columnEntries
      <&> ( \col ->
              ( if "BLOB" `T.isPrefixOf` col.datatype
                  then
                    "IIF("
                      <> quoteKeyword col.column_name
                      <> " IS NOT NULL, rowid, NULL)"
                      <> " AS "
                      <> quoteKeyword col.column_name
                  else quoteKeyword col.column_name
              )
          )
  
  
  opAndValToSql :: HashMap.HashMap Text Value -> [Text]
  opAndValToSql operatorAndValue =
    case HashMap.toList operatorAndValue of
      [(op, value)]
        | op == "eq" || op == "_eq" ->
            pure $
              if value == Null
                then " IS NULL"
                else " == " <> gqlValueToSQLText value
        | op == "neq" || op == "_neq" ->
            if value == Null
              then pure " IS NOT NULL"
              else
                [ " != " <> gqlValueToSQLText value
                , " IS NULL"
                ]
        | op == "in" || op == "_in"
        , List values <- value ->
            let listValues = values <&> gqlValueToSQLText & intercalate ","
            in  [" IN (" <> listValues <> ")"]
        | op == "nin" || op == "_nin"
        , List values <- value ->
            let listValues = values <&> gqlValueToSQLText & intercalate ","
            in  [" NOT IN (" <> listValues <> ")"]
                  <> if P.elem Null values
                    then []
                    else [" IS NULL"]
        | op == "gt" || op == "_gt" -> [" > " <> gqlValueToSQLText value]
        | op == "gte" || op == "_gte" -> [" >= " <> gqlValueToSQLText value]
        | op == "lt" || op == "_lt" -> [" < " <> gqlValueToSQLText value]
        | op == "lte" || op == "_lte" -> [" <= " <> gqlValueToSQLText value]
        | op == "like" || op == "_like" -> [" like " <> gqlValueToSQLText value]
        | op == "ilike" || op == "_ilike" -> [" like " <> gqlValueToSQLText value]
      filter -> do
        throw $
          userError $
            "Error: Filter "
              <> show filter
              <> " is not yet supported"
  
  
  getWhereClause :: [(Text, Value)] -> Text
  getWhereClause filterElements =
    if P.null filterElements
      then " "
      else
        "WHERE "
          <> ( filterElements
                <&> ( \(colName, x) -> case x of
                        Object operatorAndValue ->
                          let orClauses =
                                opAndValToSql operatorAndValue
                                  <&> (colName <>)
                                  & intercalate " OR "
                          in  "(" <> orClauses <> ")"
                        _ -> ""
                    )
                & intercalate " AND "
             )
  
  
  -- The gql lib does not offer a way to know what properties have
  -- been requested at the moment, so we always return every column
  getReturningClause :: TableEntry -> Text
  getReturningClause table =
    "RETURNING "
      <> ( table.columns
            <&> column_name
            <&> quoteKeyword
            & intercalate ", "
         )
  
  
  -- | Converts an argument map of (pk, value) pairs into a list of filters
  getByPKFilterElements :: HashMap Text Value -> [(Text, Value)]
  getByPKFilterElements args = do
    (key, value) <- HashMap.toList args
    pure (key, Object $ HashMap.singleton "eq" value)
  
  
  setCaseInsensitive :: Connection -> [(Text, Value)] -> IO ()
  setCaseInsensitive connection filterElements = do
    when
      ( filterElements
          & P.any
            ( \(_, value) -> case value of
                Object operatorAndValue ->
                  case HashMap.toList operatorAndValue of
                    [("ilike", _)] -> True
                    _ -> False
                _ -> False
            )
      )
      $ do
        execute_ connection "PRAGMA case_sensitive_like = False"
  
  
  executeSqlQuery
    :: Connection
    -> Text
    -> [ColumnEntry]
    -> [(Text, Value)]
    -> [(Text, Value)]
    -> Maybe Pagination
    -> IO [[SQLData]]
  executeSqlQuery
    connection
    tableName
    colEntries
    filterElems
    orderElems
    paginationMb = do
      let
        sqlQuery :: Query
        sqlQuery =
          Query $
            "SELECT "
              <> intercalate ", " (getColNamesQuoted colEntries)
              <> "\n"
              <> "FROM "
              <> quoteKeyword tableName
              <> "\n"
              <> getWhereClause filterElems
              <> "\n"
              <> buildSortClause colEntries orderElems
              <> "\n"
              <> buildPaginationClause paginationMb
  
      setCaseInsensitive connection filterElems
  
      liftIO $ query_ connection sqlQuery
  
  
  -- | WARNING: Also change duplicate `sqlDataToAesonValue`
  sqlDataToGQLValue :: Text -> SQLData -> Either Text Value
  sqlDataToGQLValue datatype sqlData = case (datatype, sqlData) of
    (_, SQLInteger int64) ->
      if isInfixOf "BOOL" $ toUpper datatype
        then pure $ case int64 of
          0 -> Boolean False
          _ -> Boolean True
        else
          if int64 >= fromIntegral (P.minBound :: P.Int32)
            && int64 <= fromIntegral (P.maxBound :: P.Int32)
            then pure $ Int $ fromIntegral int64 -- Int32
            else
              Left $
                "Integer "
                  <> show int64
                  <> " would overflow. "
                  <> "This happens because SQLite uses 64-bit ints, "
                  <> "but GraphQL uses 32-bit ints. "
                  <> "Use a Number (64-bit float) or Text column instead."
    (_, SQLFloat double) -> pure $ Float double
    (_, SQLText text) -> pure $ String text
    (_, SQLBlob _) -> Left "Can't encode BLOB as a GraphQL value"
    (_, SQLNull) -> pure Null
  
  
  {-| Convert a GraphQL `Value` to a `SQLData`
  TODO: ? -> SQLBlob $ string
  -}
  gqlValueToSQLData :: Value -> SQLData
  gqlValueToSQLData = \case
    Int int32 -> SQLInteger $ fromIntegral int32 -- Int64
    Float double -> SQLFloat double
    String text -> SQLText text
    Null -> SQLNull
    Boolean aBool ->
      if aBool
        then SQLInteger 1
        else SQLInteger 0
    Enum name -> SQLText name
    List aList -> SQLText $ show aList
    Object obj -> SQLText $ show obj
  
  
  -- The way Airsequel works at the moment is by generating one big GQL object at
  -- the root-most resolver, and then having child resolvers pick up the sections
  -- they need.
  --
  -- One issue with this way of doing things is that we don't have a way to
  -- generate location-specific errors, thus we used to simply error out at the
  -- first issue, without returning partial results.
  --
  -- The "hack" I came up to fix this, is to return a failed field named "foo" as
  -- a field named "__error_foo" containing the text of the error. The child
  -- resolvers can later pick up this error, and fail themselves only, thus
  -- returning partial results.
  rowToGraphQL :: Text -> TableEntry -> [SQLData] -> Value
  rowToGraphQL dbId table row =
    let
      buildMetadataJson :: Text -> Text -> Text
      buildMetadataJson colName rowid =
        object ["url" .= colToFileUrl dbId table.name colName rowid]
          & encodeToText
  
      parseSqlData :: (ColumnEntry, SQLData) -> (Text, Value)
      parseSqlData (colEntry, colVal) =
        if "BLOB" `T.isPrefixOf` colEntry.datatype
          then
            ( colEntry.column_name_gql
            , case colVal of
                SQLNull -> Null
                SQLInteger id ->
                  String $
                    buildMetadataJson colEntry.column_name (show id)
                SQLText id ->
                  String $
                    buildMetadataJson colEntry.column_name id
                _ -> Null
            )
          else case sqlDataToGQLValue colEntry.datatype colVal of
            Left err ->
              ( "__error_" <> colEntry.column_name_gql
              , String err
              )
            Right gqlData ->
              ( colEntry.column_name_gql
              , case colEntry.datatype of
                  -- Coerce value to nullable String
                  -- if no datatype is set.
                  -- This happens for columns in views.
                  "" -> gqlValueToNullableString gqlData
                  _ -> gqlData
              )
    in
      -- => [(ColumnEntry, SQLData)]
      P.zip table.columns row
        -- => [(Text, Value)]
        <&> parseSqlData
        -- => HashMap Text Value
        & HashMap.fromList
        -- => Value
        & Object
  
  
  rowsToGraphQL :: Text -> TableEntry -> [[SQLData]] -> Value
  rowsToGraphQL dbId table updatedRows =
    updatedRows
      <&> rowToGraphQL dbId table
      & List
  
  
  -- Attempts to find and decode an argument, given its current name, and a list
  -- of deprecated older names.
  tryGetArg
    :: forall m a
     . (FromGraphQL a)
    => (MonadIO m)
    => Text
    -> [Text]
    -> HashMap Text Value
    -> m (Maybe a)
  tryGetArg name alts args = do
    case HashMap.lookup name args of
      Nothing -> case alts of
        alt : others -> tryGetArg alt others args
        [] -> pure Nothing
      Just value ->
        case fromGraphQL value of
          Just decoded -> pure $ Just decoded
          Nothing ->
            P.throwIO $
              userError $
                "Argument " <> T.unpack name <> " has invalid format"
  
  
  -- Similar to `tryGetArg`, but will error out on failure.
  getArg
    :: forall m a
     . (FromGraphQL a)
    => (MonadIO m)
    => Text
    -> [Text]
    -> HashMap Text Value
    -> m a
  getArg name alts args = do
    result <- tryGetArg name alts args
    case result of
      Just value -> pure value
      Nothing ->
        P.throwIO $
          userError $
            "Argument " <> T.unpack name <> " not found"
  
  
  -- Similar to `tryGetArg`, but will return a custom value on failure.
  getArgWithDefault
    :: forall m a
     . (FromGraphQL a)
    => (MonadIO m)
    => Text
    -> [Text]
    -> HashMap Text Value
    -> a
    -> m a
  getArgWithDefault name alts args def =
    tryGetArg name alts args <&> P.fromMaybe def
  
  
  executeUpdateMutation
    :: Connection
    -> TableEntry
    -> HashMap Text Value
    -> [(Text, Value)]
    -> IO (Int, [[SQLData]])
  executeUpdateMutation connection table pairsToSet filterElements = do
    let
      columnsToSet :: [(ColumnEntry, Value)]
      columnsToSet =
        table.columns
          & P.mapMaybe
            ( \col -> case HashMap.lookup col.column_name_gql pairsToSet of
                Just value -> Just (col, value)
                _ -> Nothing
            )
  
      columnsToSetText :: Text
      columnsToSetText =
        columnsToSet
          <&> (\(col, _) -> quoteKeyword col.column_name <> " = ?")
          & intercalate ", "
  
    updatedRows :: [[SQLData]] <-
      if P.null columnsToSet
        then pure []
        else liftIO $ do
          let
            sqlQuery =
              Query $
                "UPDATE "
                  <> quoteKeyword table.name
                  <> "\n"
                  <> "SET "
                  <> columnsToSetText
                  <> "\n"
                  <> getWhereClause filterElements
                  <> "\n"
                  <> getReturningClause table
  
            valuesToSetNorm =
              columnsToSet
                <&> \(col, gqlValue) -> do
                  let sqlValue = gqlValueToSQLData gqlValue
                  if (sqlValue == SQLText "{}")
                    P.&& ("BLOB" `T.isPrefixOf` T.toUpper col.datatype)
                    then SQLBlob ""
                    else sqlValue
  
          setCaseInsensitive connection filterElements
          query connection sqlQuery valuesToSetNorm
  
    liftIO $
      changes connection
        & P.fmap (,updatedRows)
  
  
  {-| Ties custom resolver logic to a pre-existing field type. The resolver is
  wrapped such that exceptions are caught and converted to the appropriate type
  expected by the GQL query executor.
  -}
  makeResolver
    :: Introspection.Field
    -> Out.Resolve IO
    -> IO (Text, Out.Resolver IO)
  makeResolver field resolve = do
    case Introspection.makeField field of
      Left err -> P.throwIO $ userError $ T.unpack err
      Right outField ->
        pure
          ( field.name
          , ValueResolver
              outField
              $ catchAll
                resolve
                (throw . ResolverException)
          )
  
  
  -- | Maps the inner computation held by a resolver
  wrapResolver
    :: (Out.Resolve IO -> Out.Resolve IO)
    -> Out.Resolver IO
    -> Out.Resolver IO
  wrapResolver f = \case
    ValueResolver field resolve ->
      ValueResolver field (f resolve)
    EventStreamResolver field resolve subscribe ->
      EventStreamResolver field (f resolve) subscribe
  
  
  queryType
    :: Connection
    -> AccessMode
    -> Text
    -> [TableEntry]
    -> IO (Out.ObjectType IO)
  queryType connection accessMode dbId tables = do
    let
      documentation :: Text
      documentation =
        "Available queries for database \"" <> dbId <> "\""
  
      getDbEntries :: TableEntry -> Out.Resolve IO
      getDbEntries table = do
        context <- ask
  
        rows :: [[SQLData]] <- case context.arguments of
          Arguments args -> do
            filterElements :: HashMap Text Value <-
              getArgWithDefault "where" ["filter"] args HashMap.empty
  
            orderElements :: [(Name, Value)] <-
              case args & HashMap.lookup "order_by" of
                Nothing -> pure []
                Just colToOrder -> case colToOrder of
                  List objects ->
                    -- => [Value]
                    objects
                      -- => IO [[(Name, Value)]]
                      & P.traverse
                        ( \case
                            Object orderObject -> case HashMap.toList orderObject of
                              [] -> P.throwIO $ userError "Error: Order must not be empty"
                              orderElements -> pure orderElements
                            _ -> pure [] -- Should not be reachable
                        )
                      -- => IO [(Name, Value)]
                      <&> P.join
                  _ -> pure []
  
            limitElements :: Maybe P.Int32 <-
              case args & HashMap.lookup "limit" of
                Just (Int limit)
                  | limit >= 0 ->
                      pure (Just limit)
                  | otherwise ->
                      P.throwIO $
                        userError
                          "Error: limit must be positive"
                _ -> pure Nothing
  
            paginationMb :: Maybe Pagination <-
              case (limitElements, args & HashMap.lookup "offset") of
                (Just limit, Just (Int offset))
                  | offset >= 0 ->
                      pure $
                        Just $
                          Pagination
                            (fromIntegral limit)
                            (Just $ fromIntegral offset)
                  | otherwise ->
                      P.throwIO $ userError "Error: offset must be positive"
                (Just limit, _) ->
                  pure $
                    Just $
                      Pagination
                        (fromIntegral limit)
                        Nothing
                (Nothing, Just (Int _)) ->
                  P.throwIO $
                    userError
                      "Error: cannot specify offset \
                      \without also specifying a limit"
                _ -> pure Nothing
  
            let
              countQuery :: Query
              countQuery =
                Query $
                  P.fold
                    [ "SELECT COUNT() FROM"
                    , quoteKeyword table.name
                    , "\n"
                    , getWhereClause $ HashMap.toList filterElements
                    ]
  
            -- Will be equal `Just numRows` when the number of
            -- returned rows is too large.
            tooManyReturnedRows :: Maybe Int <- case paginationMb of
              -- Limit doesn't seem to affect COUNT(),
              -- so we consider it manually.
              Just pagination
                | pagination.limit <= maxGraphqlResultCount ->
                    pure Nothing
              _ -> do
                results <- liftIO $ SS.query_ connection countQuery
  
                let numRows = case P.head results of
                      Just numRowsOnly -> SS.fromOnly numRowsOnly
                      Nothing -> 0
  
                pure $
                  if numRows > maxGraphqlResultCount
                    then Just numRows
                    else Nothing
  
            P.for_ tooManyReturnedRows $ \numRows -> do
              P.throwIO $
                userError $
                  P.fold
                    [ "The graphql API cannot return more than "
                    , show maxGraphqlResultCount
                    , " entries at a time. Your query would have returned "
                    , show numRows
                    , " rows. "
                    , "Consider setting the `limit` argument on your query: `{ "
                    , T.unpack table.name
                    , " (limit: 50) { ... } }`"
                    ]
  
            liftIO $
              executeSqlQuery
                connection
                table.name
                table.columns
                (HashMap.toList filterElements)
                orderElements
                paginationMb
  
        pure $ rowsToGraphQL dbId table rows
  
      getDbEntriesByPK :: TableEntry -> Out.Resolve IO
      getDbEntriesByPK tableEntry = do
        context <- ask
        let Arguments args = context.arguments
  
        -- This query can return at most one row, so we don't worry checking for
        -- COUNT() and asserting it's within the set limits.
        queryResult <-
          liftIO $
            executeSqlQuery
              connection
              tableEntry.name
              tableEntry.columns
              (getByPKFilterElements args)
              []
              Nothing
  
        case P.head queryResult of
          Nothing -> pure Null
          Just row ->
            pure $
              rowToGraphQL
                dbId
                tableEntry
                row
  
      getResolvers :: IO (HashMap.HashMap Text (Resolver IO))
      getResolvers = do
        let
          getTableTuple :: TableEntry -> IO (Text, Resolver IO)
          getTableTuple table =
            makeResolver
              (Introspection.tableQueryField table)
              (getDbEntries table)
  
          getTableByPKTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
          getTableByPKTuple table =
            P.for (Introspection.tableQueryByPKField tables table) $ \field ->
              makeResolver field (getDbEntriesByPK table)
  
        queryMany <- P.for tables getTableTuple
        queryByPKMbs <- P.for tables getTableByPKTuple
        let queryByPK = P.catMaybes queryByPKMbs
        pure $ HashMap.fromList $ queryMany <> queryByPK
  
    resolvers <- getResolvers
    schemaResolver <- Introspection.getSchemaResolver accessMode tables
  
    let
      requireRead :: Out.Resolve IO -> Out.Resolve IO
      requireRead resolve = do
        when (P.not $ canRead accessMode) $ do
          throw $
            ResolverException $
              userError "Cannot read field using the provided token"
        resolve
  
    pure $
      outObjectTypeToObjectType $
        OutObjectType
          { name = "Query"
          , descriptionMb = Just documentation
          , interfaceTypes = []
          , fields =
              P.fold
                [ schemaResolver
                , Introspection.typeNameResolver
                , resolvers
                ]
                -- TODO: is it better to wrap the resolvers here,
                -- or to just return an empty list of resolvers
                -- when given a token that cannot read?
                <&> wrapResolver requireRead
          }
  
  
  mutationType
    :: Connection
    -> Integer
    -> AccessMode
    -> Text
    -> [TableEntry]
    -> IO (Maybe (Out.ObjectType IO))
  mutationType connection maxRowsPerTable accessMode dbId tables = do
    let
      getColValue :: HashMap.HashMap Text Value -> Text -> Value
      getColValue rowObj columnName =
        HashMap.findWithDefault Null (doubleXEncodeGql columnName) rowObj
  
      mutationResponse :: TableEntry -> Int -> [[SQLData]] -> IO Value
      mutationResponse table numChanges rows = do
        let returning = rowsToGraphQL dbId table rows
  
        pure $
          Object $
            HashMap.fromList
              [ ("affected_rows", Int $ fromIntegral numChanges)
              , ("returning", returning)
              ]
  
      mutationByPKResponse :: TableEntry -> Int -> Maybe [SQLData] -> IO Value
      mutationByPKResponse table numChanges mbRow = do
        let returning = case mbRow of
              Nothing -> Null
              Just row -> rowToGraphQL dbId table row
  
        pure $
          Object $
            HashMap.fromList
              [ ("affected_rows", Int $ fromIntegral numChanges)
              , ("returning", returning)
              ]
  
      executeDbInserts :: TableEntry -> ReaderT Out.Context IO Value
      executeDbInserts table = do
        context <- ask
        let
          insertInDb :: Arguments -> ReaderT Out.Context IO (Int, [[SQLData]])
          insertInDb (Arguments argMap) = do
            -- Yields for example:
            --   [ { name: "John", email: "john@example.com" }
            --   , { name: "Eve",  email: "eve@example.com" }
            --   ]
            values :: [HashMap Text Value] <- getArg "objects" [] argMap
            let
              -- All colums that are contained in the entries
              containedColumns :: [Text]
              containedColumns =
                values
                  <&> HashMap.keys
                  & P.concat
                  & nub
                  <&> doubleXDecode
  
              boundVariableNames :: [Text]
              boundVariableNames =
                containedColumns
                  <&> (\name -> ":" <> doubleXEncodeGql name)
  
            onConflictArg :: [HashMap Text Value] <-
              getArgWithDefault "on_conflict" [] argMap []
  
            onConflictClauses <- P.for onConflictArg $ \fields -> do
              let
                getColumnList fieldName =
                  getArgWithDefault fieldName [] fields []
                    <&> P.mapMaybe
                      ( \case
                          Enum columnName -> Just columnName
                          _ -> Nothing
                      )
  
              constraint <- getColumnList "constraint"
              update <- getColumnList "update_columns"
  
              updateClauses <- P.for update $ \column -> do
                when (column `notElem` containedColumns) $ do
                  P.throwIO $
                    userError $
                      "Column "
                        <> T.unpack column
                        <> " cannot be set on conflicts without \
                           \being explicitly provided"
  
                pure $
                  quoteKeyword column
                    <> " = :"
                    <> doubleXEncodeGql column
  
              filterElements <- getArgWithDefault "where" ["filter"] fields mempty
  
              pure $
                "ON CONFLICT ("
                  <> ( constraint
                        <&> quoteKeyword
                        & intercalate ", "
                     )
                  <> ")\n DO UPDATE SET \n"
                  <> intercalate ",\n" updateClauses
                  <> "\n"
                  <> getWhereClause (HashMap.toList filterElements)
  
            let
              columnList =
                if P.null containedColumns
                  then ""
                  else
                    " ("
                      <> ( containedColumns
                            <&> quoteKeyword
                            & intercalate ", "
                         )
                      <> ")"
              insertedValues =
                if P.null boundVariableNames
                  then "DEFAULT VALUES"
                  else
                    "VALUES ("
                      <> intercalate ", " boundVariableNames
                      <> ")"
              sqlQuery =
                Query $
                  P.unlines
                    [ "INSERT INTO "
                        <> quoteKeyword table.name
                        <> columnList
                    , insertedValues
                    , P.unlines onConflictClauses
                    , getReturningClause table
                    ]
  
              sqlDataRows :: [[SQLData]]
              sqlDataRows =
                values <&> \rowObj ->
                  containedColumns
                    <&> getColValue rowObj
                    <&> gqlValueToSQLData
  
            returnedRows <-
              liftIO $ P.forM sqlDataRows $ \sqlDataRow -> do
                numRowsRes :: [[Integer]] <-
                  query_
                    connection
                    $ Query
                    $ "SELECT COUNT() FROM "
                      <> quoteKeyword table.name
  
                case numRowsRes of
                  [[numRows]] -> do
                    when (numRows >= maxRowsPerTable) $
                      P.throwIO $
                        userError $
                          "Please upgrade to a Pro account \
                          \to insert more than "
                            <> show maxRowsPerTable
                            <> " rows into a table"
                  _ -> pure ()
  
                SS.queryNamed connection sqlQuery $
                  P.zipWith (SS.:=) boundVariableNames sqlDataRow
  
            -- FIXME:
            --   This should probably be used, but sqlite-simple
            --   doesn't use only one query to execute the insert
            --   https://github.com/nurpax/sqlite-simple/issues/82
            -- liftIO $ changes connection
            pure (P.length sqlDataRows, returnedRows & P.concat)
  
        (numOfChanges, returnedRows) <- insertInDb context.arguments
        liftIO $ mutationResponse table numOfChanges returnedRows
  
      -- Execute SQL query to update selected entries
      executeDbUpdates :: TableEntry -> ReaderT Out.Context IO Value
      executeDbUpdates table = do
        context <- ask
        let Arguments args = context.arguments
        liftIO $ do
          filterObj <- getArg "where" ["filter"] args
          pairsToSet <- getArg "_set" ["set"] args
          (numOfChanges, updatedRows) <- case HashMap.toList filterObj of
            [] -> P.throwIO $ userError "Error: Filter must not be empty"
            filterElements ->
              executeUpdateMutation
                connection
                table
                pairsToSet
                filterElements
  
          mutationResponse table numOfChanges updatedRows
  
      executeDbUpdatesByPK :: TableEntry -> ReaderT Out.Context IO Value
      executeDbUpdatesByPK table = do
        context <- ask
        let Arguments args = context.arguments
        let filterElements =
              args
                & HashMap.delete (encodeOutsidePKNames table "_set")
                & HashMap.delete (encodeOutsidePKNames table "set")
                & getByPKFilterElements
  
        liftIO $ do
          pairsToSet <-
            getArg
              (encodeOutsidePKNames table "_set")
              [encodeOutsidePKNames table "set"]
              args
  
          (numOfChanges, updatedRows) <-
            executeUpdateMutation
              connection
              table
              pairsToSet
              filterElements
  
          mutationByPKResponse table numOfChanges $ P.head updatedRows
  
      -- Execute SQL query to delete selected entries
      executeDbDeletions :: TableEntry -> ReaderT Out.Context IO Value
      executeDbDeletions table = do
        context <- ask
        let Arguments args = context.arguments
  
        liftIO $ do
          filterElements <- getArg "where" ["filter"] args
          let sqlQuery =
                Query $
                  P.unlines
                    [ "DELETE FROM " <> quoteKeyword table.name
                    , getWhereClause $ HashMap.toList filterElements
                    , getReturningClause table
                    ]
  
          deletedRows :: [[SQLData]] <- SS.query_ connection sqlQuery
          numOfChanges <- SS.changes connection
          mutationResponse table numOfChanges deletedRows
  
      executeDbDeletionsByPK :: TableEntry -> ReaderT Out.Context IO Value
      executeDbDeletionsByPK table = do
        context <- ask
        let Arguments args = context.arguments
  
        liftIO $ do
          let sqlQuery =
                Query $
                  P.unlines
                    [ "DELETE FROM " <> quoteKeyword table.name
                    , getWhereClause $ getByPKFilterElements args
                    , getReturningClause table
                    ]
  
          deletedRows :: [[SQLData]] <- SS.query_ connection sqlQuery
          numOfChanges <- SS.changes connection
          mutationByPKResponse table numOfChanges $ P.head deletedRows
  
      getInsertTableTuple :: TableEntry -> IO (Text, Resolver IO)
      getInsertTableTuple table =
        makeResolver
          (Introspection.tableInsertField accessMode table)
          (executeDbInserts table)
  
      getUpdateTableTuple :: TableEntry -> IO (Text, Resolver IO)
      getUpdateTableTuple table =
        makeResolver
          (Introspection.tableUpdateField accessMode table)
          (executeDbUpdates table)
  
      getUpdateByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
      getUpdateByPKTableTuple table =
        P.for (Introspection.tableUpdateFieldByPk accessMode tables table) $
          \field -> makeResolver field (executeDbUpdatesByPK table)
  
      getDeleteTableTuple :: TableEntry -> IO (Text, Resolver IO)
      getDeleteTableTuple table =
        makeResolver
          (Introspection.tableDeleteField accessMode table)
          (executeDbDeletions table)
  
      getDeleteByPKTableTuple :: TableEntry -> IO (Maybe (Text, Resolver IO))
      getDeleteByPKTableTuple table =
        P.for (Introspection.tableDeleteFieldByPK accessMode tables table) $
          \field -> makeResolver field (executeDbDeletionsByPK table)
  
      tablesWithoutViews :: [TableEntry]
      tablesWithoutViews =
        List.filter
          (\table -> table.object_type == Table)
          tables
  
    insertTuples <-
      P.fold
        [ P.for tablesWithoutViews getInsertTableTuple
        ]
  
    writeTuples <-
      P.fold
        [ P.for tablesWithoutViews getUpdateTableTuple
        , P.for tablesWithoutViews getDeleteTableTuple
        , P.for tablesWithoutViews getUpdateByPKTableTuple
            <&> P.catMaybes
        , P.for tablesWithoutViews getDeleteByPKTableTuple
            <&> P.catMaybes
        ]
  
    let
      insertResolvers =
        if canInsert accessMode
          then HashMap.fromList insertTuples
          else mempty
  
      writeResolvers =
        if canWrite accessMode
          then HashMap.fromList writeTuples
          else mempty
  
    pure
      $ Just
      $ Out.ObjectType
        "Mutation"
        Nothing
        []
      $ insertResolvers <> writeResolvers
  
  
  -- | Automatically generated schema derived from the SQLite database
  getDerivedSchema
    :: SchemaConf
    -> Connection
    -> Text
    -> [TableEntry]
    -> IO (Schema IO)
  getDerivedSchema schemaConf connection dbId tables = do
    let sqlitePragmas = getSQLitePragmas schemaConf.pragmaConf
    P.forM_ sqlitePragmas (execute_ connection)
  
    queries <- queryType connection schemaConf.accessMode dbId tables
    mutations <-
      mutationType
        connection
        schemaConf.maxRowsPerTable
        schemaConf.accessMode
        dbId
        tables
  
    pure $
      schema
        queries
        mutations
        Nothing -- subscriptions
        mempty
  