From b72fc6922a168f46e776e0f764db7bc3ab8855ad Mon Sep 17 00:00:00 2001 From: Sameer Kolhar Date: Thu, 22 Oct 2020 18:56:42 +0530 Subject: [PATCH] server: fix issue with tracking custom functions that return `SETOF` materialized view (close #5294) (#5945) https://github.com/hasura/graphql-engine/pull/5945 --- CHANGELOG.md | 1 + .../src-lib/Hasura/RQL/DDL/Schema/Function.hs | 4 +- server/src-lib/Hasura/SQL/Types.hs | 26 +++++---- server/src-rsr/catalog_version.txt | 2 +- server/src-rsr/initialise.sql | 57 +++++++++++-------- server/src-rsr/migrations/39_to_40.sql | 46 +++++++++++++++ server/src-rsr/migrations/40_to_39.sql | 37 ++++++++++++ .../functions/query_search_author_mview.yaml | 18 ++++++ .../graphql_query/functions/setup.yaml | 42 ++++++++++++++ .../graphql_query/functions/teardown.yaml | 2 + server/tests-py/test_graphql_queries.py | 4 ++ 11 files changed, 201 insertions(+), 38 deletions(-) create mode 100644 server/src-rsr/migrations/39_to_40.sql create mode 100644 server/src-rsr/migrations/40_to_39.sql create mode 100644 server/tests-py/queries/graphql_query/functions/query_search_author_mview.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index d4edcbac8b4..3e3de7c6ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ This release contains the [PDV refactor (#4111)](https://github.com/hasura/graph - server: limit the length of event trigger names (close #5786) **NOTE:** If you have event triggers with names greater than 42 chars, then you should update their names to avoid running into Postgres identifier limit bug (#5786) - server: validate remote schema queries (fixes #4143) +- server: fix issue with tracking custom functions that return `SETOF` materialized view (close #5294) (#5945) - console: allow user to cascade Postgres dependencies when dropping Postgres objects (close #5109) (#5248) - console: mark inconsistent remote schemas in the UI (close #5093) (#5181) - cli: add missing global flags for seed command (#5565) diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs index 1339a932065..221dcef0bf7 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs @@ -222,8 +222,8 @@ handleMultipleFunctions qf = \case throw400 NotSupported $ "function " <> qf <<> " is overloaded. Overloaded functions are not supported" -fetchRawFunctioInfo :: MonadTx m => QualifiedFunction -> m RawFunctionInfo -fetchRawFunctioInfo qf@(QualifiedObject sn fn) = +fetchRawFunctionInfo :: MonadTx m => QualifiedFunction -> m RawFunctionInfo +fetchRawFunctionInfo qf@(QualifiedObject sn fn) = handleMultipleFunctions qf =<< map (Q.getAltJ . runIdentity) <$> fetchFromDatabase where fetchFromDatabase = liftTx $ diff --git a/server/src-lib/Hasura/SQL/Types.hs b/server/src-lib/Hasura/SQL/Types.hs index 04bceb43f24..f2cdd9eaa13 100644 --- a/server/src-lib/Hasura/SQL/Types.hs +++ b/server/src-lib/Hasura/SQL/Types.hs @@ -195,30 +195,34 @@ instance ToTxt TableName where data TableType = TTBaseTable | TTView + | TTMaterializedView | TTForeignTable | TTLocalTemporary deriving (Eq) tableTyToTxt :: TableType -> T.Text -tableTyToTxt TTBaseTable = "BASE TABLE" -tableTyToTxt TTView = "VIEW" -tableTyToTxt TTForeignTable = "FOREIGN TABLE" -tableTyToTxt TTLocalTemporary = "LOCAL TEMPORARY" +tableTyToTxt TTBaseTable = "BASE TABLE" +tableTyToTxt TTView = "VIEW" +tableTyToTxt TTMaterializedView = "MATERIALIZED VIEW" +tableTyToTxt TTForeignTable = "FOREIGN TABLE" +tableTyToTxt TTLocalTemporary = "LOCAL TEMPORARY" instance Show TableType where show = T.unpack . tableTyToTxt instance Q.FromCol TableType where fromCol bs = flip Q.fromColHelper bs $ PD.enum $ \case - "BASE TABLE" -> Just TTBaseTable - "VIEW" -> Just TTView - "FOREIGN TABLE" -> Just TTForeignTable - "LOCAL TEMPORARY" -> Just TTLocalTemporary - _ -> Nothing + "BASE TABLE" -> Just TTBaseTable + "VIEW" -> Just TTView + "MATERIALIZED VIEW" -> Just TTMaterializedView + "FOREIGN TABLE" -> Just TTForeignTable + "LOCAL TEMPORARY" -> Just TTLocalTemporary + _ -> Nothing isView :: TableType -> Bool -isView TTView = True -isView _ = False +isView TTView = True +isView TTMaterializedView = True +isView _ = False newtype ConstraintName = ConstraintName { getConstraintTxt :: T.Text } diff --git a/server/src-rsr/catalog_version.txt b/server/src-rsr/catalog_version.txt index a2720097dcc..425151f3a41 100644 --- a/server/src-rsr/catalog_version.txt +++ b/server/src-rsr/catalog_version.txt @@ -1 +1 @@ -39 +40 diff --git a/server/src-rsr/initialise.sql b/server/src-rsr/initialise.sql index da7428627cb..4707b06d704 100644 --- a/server/src-rsr/initialise.sql +++ b/server/src-rsr/initialise.sql @@ -568,33 +568,42 @@ CREATE VIEW hdb_catalog.hdb_function_info_agg AS ( ( SELECT e - FROM - ( + FROM + ( + SELECT + description, + has_variadic, + function_type, + return_type_schema, + return_type_name, + return_type_type, + returns_set, + input_arg_types, + input_arg_names, + default_args, + exists( SELECT - description, - has_variadic, - function_type, - return_type_schema, - return_type_name, - return_type_type, - returns_set, - input_arg_types, - input_arg_names, - default_args, - exists( - SELECT - 1 - FROM - information_schema.tables - WHERE - table_schema = return_type_schema - AND table_name = return_type_name - ) AS returns_table - ) AS e + 1 + FROM + information_schema.tables + WHERE + table_schema = return_type_schema + AND table_name = return_type_name + ) + OR exists( + SELECT + 1 + FROM + pg_matviews + WHERE + schemaname = return_type_schema + AND matviewname = return_type_name + ) AS returns_table + ) AS e ) ) AS "function_info" - FROM - hdb_catalog.hdb_function_agg + FROM + hdb_catalog.hdb_function_agg ); CREATE OR REPLACE FUNCTION diff --git a/server/src-rsr/migrations/39_to_40.sql b/server/src-rsr/migrations/39_to_40.sql new file mode 100644 index 00000000000..18f5c46922f --- /dev/null +++ b/server/src-rsr/migrations/39_to_40.sql @@ -0,0 +1,46 @@ +CREATE +OR REPLACE VIEW hdb_catalog.hdb_function_info_agg AS ( + SELECT + function_name, + function_schema, + row_to_json ( + ( + SELECT + e + FROM + ( + SELECT + description, + has_variadic, + function_type, + return_type_schema, + return_type_name, + return_type_type, + returns_set, + input_arg_types, + input_arg_names, + default_args, + exists( + SELECT + 1 + FROM + information_schema.tables + WHERE + table_schema = return_type_schema + AND table_name = return_type_name + ) + OR exists( + SELECT + 1 + FROM + pg_matviews + WHERE + schemaname = return_type_schema + AND matviewname = return_type_name + ) AS returns_table + ) AS e + ) + ) AS "function_info" + FROM + hdb_catalog.hdb_function_agg +); diff --git a/server/src-rsr/migrations/40_to_39.sql b/server/src-rsr/migrations/40_to_39.sql new file mode 100644 index 00000000000..87212a9775f --- /dev/null +++ b/server/src-rsr/migrations/40_to_39.sql @@ -0,0 +1,37 @@ +CREATE +OR REPLACE VIEW hdb_catalog.hdb_function_info_agg AS ( + SELECT + function_name, + function_schema, + row_to_json ( + ( + SELECT + e + FROM + ( + SELECT + description, + has_variadic, + function_type, + return_type_schema, + return_type_name, + return_type_type, + returns_set, + input_arg_types, + input_arg_names, + default_args, + exists( + SELECT + 1 + FROM + information_schema.tables + WHERE + table_schema = return_type_schema + AND table_name = return_type_name + ) AS returns_table + ) AS e + ) + ) AS "function_info" + FROM + hdb_catalog.hdb_function_agg +); diff --git a/server/tests-py/queries/graphql_query/functions/query_search_author_mview.yaml b/server/tests-py/queries/graphql_query/functions/query_search_author_mview.yaml new file mode 100644 index 00000000000..2c0bed8397a --- /dev/null +++ b/server/tests-py/queries/graphql_query/functions/query_search_author_mview.yaml @@ -0,0 +1,18 @@ +description: Custom GraphQL query using search_author_mview function which returns results from a materialized view +url: /v1/graphql +status: 200 +response: + data: + search_author_mview: + - first_name: franz + last_name: kafka +query: + query: | + query { + search_author_mview( + args: {query: "kafka"} + ) { + first_name + last_name + } + } diff --git a/server/tests-py/queries/graphql_query/functions/setup.yaml b/server/tests-py/queries/graphql_query/functions/setup.yaml index f37bf384b32..cda4ad5ccd5 100644 --- a/server/tests-py/queries/graphql_query/functions/setup.yaml +++ b/server/tests-py/queries/graphql_query/functions/setup.yaml @@ -176,3 +176,45 @@ args: name: get_session_var configuration: session_argument: hasura_session + +# track & query functions that return SETOF materialized views +- type: run_sql + args: + sql: | + CREATE TABLE author( + id SERIAL PRIMARY KEY, + first_name TEXT, + last_name TEXT + ); + + INSERT INTO author(first_name, last_name) VALUES + ('enid', 'blyton'), + ('ruskin', 'bond'), + ('franz', 'kafka'); + + CREATE MATERIALIZED VIEW author_mat_view AS + SELECT * FROM author; + + CREATE FUNCTION search_author_mview(query text) + RETURNS SETOF author_mat_view AS $FUNCTION$ + SELECT * FROM author_mat_view WHERE + first_name = query OR + last_name = query + $FUNCTION$ LANGUAGE sql STABLE; + +- type: track_table + args: + name: author + schema: public + +- type: track_table + args: + name: author_mat_view + schema: public + +- type: track_function + version: 2 + args: + function: + schema: public + name: search_author_mview diff --git a/server/tests-py/queries/graphql_query/functions/teardown.yaml b/server/tests-py/queries/graphql_query/functions/teardown.yaml index dd657eab8cf..ca14cb896f5 100644 --- a/server/tests-py/queries/graphql_query/functions/teardown.yaml +++ b/server/tests-py/queries/graphql_query/functions/teardown.yaml @@ -9,4 +9,6 @@ args: DROP TABLE integer_column cascade; DROP TABLE "user" cascade; DROP TABLE text_result cascade; + DROP TABLE author cascade; + DROP MATERIALIZED VIEW IF EXISTS author_mat_view cascade; cascade: true diff --git a/server/tests-py/test_graphql_queries.py b/server/tests-py/test_graphql_queries.py index 647806ce875..aaa50f19246 100644 --- a/server/tests-py/test_graphql_queries.py +++ b/server/tests-py/test_graphql_queries.py @@ -544,6 +544,10 @@ class TestGraphQLQueryFunctions: @pytest.mark.parametrize("transport", ['http', 'websocket']) def test_query_get_test_session_id(self, hge_ctx, transport): check_query_f(hge_ctx, self.dir() + '/query_get_test_session_id.yaml') + + @pytest.mark.parametrize("transport", ['http', 'websocket']) + def test_query_search_author_mview(self, hge_ctx, transport): + check_query_f(hge_ctx, self.dir() + '/query_search_author_mview.yaml') @classmethod def dir(cls):