Skip to content

Commit

Permalink
Support tracking VOLATILE SQL functions as mutations. Closes #1514
Browse files Browse the repository at this point in the history
  • Loading branch information
jberryman committed Oct 28, 2020
1 parent ebe6990 commit 731bead
Show file tree
Hide file tree
Showing 21 changed files with 530 additions and 72 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ This release contains the [PDV refactor (#4111)](https://github.com/hasura/graph
- server: allow remote relationships joining `type` column with `[type]` input argument as spec allows this coercion (fixes #5133)
- server: add action-like URL templating for event triggers and remote schemas (fixes #2483)
- server: change `created_at` column type from `timestamp` to `timestamptz` for scheduled triggers tables (fix #5722)
- server: Support tracking VOLATILE SQL functions as mutations. (closing #1514)
- server: allow configuring timeouts for actions (fixes #4966)
- server: accept only non-negative integers for batch size and refetch interval (close #5653) (#5759)
- server: limit the length of event trigger names (close #5786)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Only tracked custom functions are available for querying/mutating/subscribing da
track_function
--------------

``track_function`` is used to add a custom SQL function to the GraphQL schema.
``track_function`` is used to add a custom SQL function to the ``query`` root field of the GraphQL schema.
Also refer a note :ref:`here <note>`.

Add an SQL function ``search_articles``:
Expand All @@ -48,10 +48,12 @@ Add an SQL function ``search_articles``:
track_function v2
-----------------

Version 2 of ``track_function`` is used to add a custom SQL function to the GraphQL schema with configuration.
Version 2 of ``track_function`` is used to add a custom SQL function to the GraphQL schema.
Its support more configuration options than v1, and also supports tracking
``VOLATILE`` functions as mutations.
Also refer a note :ref:`here <note>`.

Add an SQL function called ``search_articles`` with a Hasura session argument.
Track a SQL function called ``search_articles`` with a Hasura session argument:

.. code-block:: http
Expand All @@ -73,6 +75,29 @@ Add an SQL function called ``search_articles`` with a Hasura session argument.
}
}
Track ``VOLATILE`` SQL function ``reset_widget`` as a mutation, so it appears
as a top-level field under the ``mutation`` root field:

.. code-block:: http
POST /v1/query HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "track_function",
"version": 2,
"args": {
"function": {
"schema": "public",
"name": "reset_widget"
},
"configuration": {
"as_mutation": true
}
}
}
.. _track_function_args_syntax_v2:

Args syntax
Expand Down Expand Up @@ -110,6 +135,10 @@ Function Configuration
- false
- `String`
- Function argument which accepts session info JSON
* - as_mutation
- false
- `Bool`
- Should this function be exposed as a mutation? If true, the function must be VOLATILE.

.. _note:

Expand All @@ -118,8 +147,9 @@ Function Configuration
Currently, only functions which satisfy the following constraints can be exposed over the GraphQL API
(*terminology from* `Postgres docs <https://www.postgresql.org/docs/current/sql-createfunction.html>`__):

- **Function behaviour**: ONLY ``STABLE`` or ``IMMUTABLE``
- **Return type**: MUST be ``SETOF <table-name>``
- **Function behaviour**: ``STABLE`` or ``IMMUTABLE`` functions may *only* be exposed as queries
(i.e. with ``as_mutation: false``), while ``VOLATILE`` functions may only be exposed as mutations.
- **Return type**: MUST be ``SETOF <table-name>`` where ``<table-name>`` is already tracked
- **Argument modes**: ONLY ``IN``

.. _untrack_function:
Expand Down
27 changes: 24 additions & 3 deletions docs/graphql/core/schema/custom-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ that can be used to either encapsulate some custom business logic or extend the
are also referred to as **stored procedures**.

Hasura GraphQL engine lets you expose certain types of custom functions as top level fields in the GraphQL API to allow
querying them using both ``queries`` and ``subscriptions``.
querying them as either ``queries`` or ``subscriptions``, or (for ``VOLATILE`` functions) as ``mutations``.

.. note::

Expand All @@ -34,8 +34,9 @@ Supported SQL functions
Currently, only functions which satisfy the following constraints can be exposed as top level fields in the GraphQL API
(*terminology from* `Postgres docs <https://www.postgresql.org/docs/current/sql-createfunction.html>`__):

- **Function behaviour**: ONLY ``STABLE`` or ``IMMUTABLE``
- **Return type**: MUST be ``SETOF <table-name>``
- **Function behaviour**: ``STABLE`` or ``IMMUTABLE`` functions may *only* be exposed as queries
(i.e. with ``as_mutation: false``), while ``VOLATILE`` functions may only be exposed as mutations.
- **Return type**: MUST be ``SETOF <table-name>`` where ``<table-name>`` is already tracked
- **Argument modes**: ONLY ``IN``

.. _create_sql_functions:
Expand Down Expand Up @@ -551,6 +552,26 @@ following example.

The specified session argument will not be included in the ``<function-name>_args`` input object in the GraphQL schema.


Using functions as mutations
****************************

You can also use the :ref:`track_function_v2 <track_function_v2>` API to track
functions as mutations. In this case the SQL function *must* be marked
``VOLATILE`` (this is the default if volatility isn't specified; see
`the postgres docs <https://www.postgresql.org/docs/current/xfunc-volatility.html>`).

Aside from showing up under the ``mutation`` root (and presumably having
side-effects), these behave the same as described above for ``queries``.

.. note::

It's easy to accidentally give a SQL function the wrong volatility (or for a
function to end up with ``VOLATILE`` mistakenly, since it's the default).
That's one reason we require specifying ``as_mutation``: it lets us raise an
error rather than do something unintended.


Permissions for custom function queries
---------------------------------------

Expand Down
3 changes: 3 additions & 0 deletions server/src-lib/Hasura/GraphQL/Context.hs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ data MutationDB (b :: Backend) v
= MDBInsert (AnnInsert b v)
| MDBUpdate (RQL.AnnUpdG b v)
| MDBDelete (RQL.AnnDelG b v)
| MDBFunction (RQL.AnnSimpleSelG b v)
-- ^ This represents a VOLATILE function, and is AnnSimpleSelG for easy
-- re-use of non-VOLATILE function tracking code.

data ActionMutation (b :: Backend) v
= AMSync !(RQL.AnnActionExecution b v)
Expand Down
29 changes: 9 additions & 20 deletions server/src-lib/Hasura/GraphQL/Execute/Mutation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import Hasura.GraphQL.Context
import Hasura.GraphQL.Execute.Action
import Hasura.GraphQL.Execute.Insert
import Hasura.GraphQL.Execute.Prepare
-- We borrow some Query code to handle the case of VOLATILE functions:
import Hasura.GraphQL.Execute.Query
import Hasura.GraphQL.Execute.Remote
import Hasura.GraphQL.Execute.Resolve
import Hasura.GraphQL.Parser
Expand Down Expand Up @@ -92,24 +94,6 @@ convertInsert env usrVars remoteJoinCtx insertOperation stringifyNum = do
validateSessionVariables expectedVariables usrVars
pure $ convertToSQLTransaction env preparedInsert remoteJoinCtx Seq.empty stringifyNum

convertMutationDB
:: ( HasVersion
, MonadIO m
, MonadError QErr m
, Tracing.MonadTrace tx
, MonadIO tx
, MonadTx tx
)
=> Env.Environment
-> SessionVariables
-> RQL.MutationRemoteJoinCtx
-> Bool
-> MutationDB 'Postgres UnpreparedValue
-> m (tx EncJSON, HTTP.ResponseHeaders)
convertMutationDB env userSession remoteJoinCtx stringifyNum = \case
MDBInsert s -> noResponseHeaders <$> convertInsert env userSession remoteJoinCtx s stringifyNum
MDBUpdate s -> noResponseHeaders <$> convertUpdate env userSession remoteJoinCtx s stringifyNum
MDBDelete s -> noResponseHeaders <$> convertDelete env userSession remoteJoinCtx s stringifyNum

noResponseHeaders :: tx EncJSON -> (tx EncJSON, HTTP.ResponseHeaders)
noResponseHeaders rTx = (rTx, [])
Expand Down Expand Up @@ -159,7 +143,7 @@ convertMutationSelectionSet
-> [G.VariableDefinition]
-> Maybe GH.VariableValues
-> m (ExecutionPlan (tx EncJSON, HTTP.ResponseHeaders))
convertMutationSelectionSet env logger gqlContext sqlGenCtx userInfo manager reqHeaders fields varDefs varValsM = do
convertMutationSelectionSet env logger gqlContext SQLGenCtx{stringifyNum} userInfo manager reqHeaders fields varDefs varValsM = do
mutationParser <- onNothing (gqlMutationParser gqlContext) $
throw400 ValidationFailed "no mutations exist"
-- Parse the GraphQL query into the RQL AST
Expand All @@ -172,7 +156,12 @@ convertMutationSelectionSet env logger gqlContext sqlGenCtx userInfo manager req
let userSession = _uiSession userInfo
remoteJoinCtx = (manager, reqHeaders, userInfo)
txs <- for unpreparedQueries \case
RFDB db -> ExecStepDB <$> convertMutationDB env userSession remoteJoinCtx (stringifyNum sqlGenCtx) db
RFDB db -> ExecStepDB . noResponseHeaders <$> case db of
MDBInsert s -> convertInsert env userSession remoteJoinCtx s stringifyNum
MDBUpdate s -> convertUpdate env userSession remoteJoinCtx s stringifyNum
MDBDelete s -> convertDelete env userSession remoteJoinCtx s stringifyNum
MDBFunction s -> convertFunction env userInfo manager reqHeaders s

RFRemote (remoteSchemaInfo, remoteField) ->
pure $ buildExecStepRemote
remoteSchemaInfo
Expand Down
30 changes: 30 additions & 0 deletions server/src-lib/Hasura/GraphQL/Execute/Query.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Hasura.GraphQL.Execute.Query
( convertQuerySelSet
, convertFunction
-- , queryOpFromPlan
-- , ReusableQueryPlan
, PreparedSql(..)
Expand Down Expand Up @@ -294,6 +295,35 @@ convertQuerySelSet env logger gqlContext userInfo manager reqHeaders directives
AQAsync s -> AQPAsyncQuery <$>
DS.traverseAnnSimpleSelect prepareWithPlan (resolveAsyncActionQuery userInfo s)

-- | A pared-down version of 'convertQuerySelSet', for use in execution of
-- special case of SQL function mutations (see 'MDBFunction').
convertFunction
:: forall m tx .
( MonadError QErr m
, HasVersion
, MonadIO tx
, MonadTx tx
, Tracing.MonadTrace tx
)
=> Env.Environment
-> UserInfo
-> HTTP.Manager
-> HTTP.RequestHeaders
-> DS.AnnSimpleSelG 'Postgres UnpreparedValue
-- ^ VOLATILE function as 'SelectExp'
-> m (tx EncJSON)
convertFunction env userInfo manager reqHeaders unpreparedQuery = do
-- Transform the RQL AST into a prepared SQL query
(preparedQuery, PlanningSt _ _ planVals expectedVariables)
<- flip runStateT initPlanningSt
$ DS.traverseAnnSimpleSelect prepareWithPlan unpreparedQuery
validateSessionVariables expectedVariables $ _uiSession userInfo

pure $!
fst $ -- forget (Maybe PreparedSql)
mkCurPlanTx env manager reqHeaders userInfo id noProfile $
RFPPostgres $ irToRootFieldPlan planVals $ QDBSimple preparedQuery

-- See Note [Temporarily disabling query plan caching]
-- use the existing plan and new variables to create a pg query
-- queryOpFromPlan
Expand Down
Loading

0 comments on commit 731bead

Please sign in to comment.