/
alt3rmann
/
graphql-engine
Обзор
Документация
Войти
/
alt3rmann
/
graphql-engine
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
server/tests-py/test_subscriptions.py
1 026 строк
36 KB
Brandon Simmons
ENG-1888: Fix skipped rows in streaming subscriptions for postgres an…
24 сен 2025, 19:55
24 сен 2025, 19:55
718555e
Код
Авторство
О чём код?
from collections import OrderedDict import json import pytest import queue from ruamel.yaml import YAML import time import uuid from validate import check_query_f from utils import insert_many yaml=YAML(typ='safe', pure=True) usefixtures = pytest.mark.usefixtures @pytest.fixture(scope='class') def ws_conn_init(hge_key, ws_client): init_ws_conn(hge_key, ws_client) @pytest.fixture(scope='class') def ws_conn_init_graphql_ws(hge_key, ws_client_graphql_ws): init_graphql_ws_conn(hge_key, ws_client_graphql_ws) ''' Refer: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_connection_init ''' # This is used in other test files! Be careful when modifying it. def init_ws_conn(hge_key, ws_client, payload = None): ws_client.create_conn() init_msg = { 'type': 'connection_init', 'payload': payload or ws_payload(hge_key), } ws_client.send(init_msg) ev = ws_client.get_ws_event(3) assert ev['type'] == 'connection_ack', ev def init_graphql_ws_conn(hge_key, ws_client_graphql_ws): ws_client_graphql_ws.create_conn() init_msg = { 'type': 'connection_init', 'payload': ws_payload(hge_key), } ws_client_graphql_ws.send(init_msg) ev = ws_client_graphql_ws.get_ws_event(3) assert ev['type'] == 'connection_ack', ev def ws_payload(hge_key): if hge_key is not None: return { 'headers': { 'X-Hasura-Admin-Secret': hge_key, } } else: return {} def get_explain_graphql_query_response(hge_ctx, hge_key, query, variables, user_headers = {}): headers = {} if hge_key is not None: headers['X-Hasura-Admin-Secret'] = hge_key request = { 'query': { 'query': query, 'variables': variables }, 'user': user_headers } status_code, response, _ = hge_ctx.anyq('/v1/graphql/explain', request, headers) assert status_code == 200, (request, status_code, response) return response @pytest.mark.no_admin_secret class TestSubscriptionCtrlWithoutSecret(object): def test_connection(self, ws_client): init_ws_conn(None, ws_client) obj = { 'type': 'connection_terminate' } ws_client.send(obj) with pytest.raises(queue.Empty): ws_client.get_ws_event(3) @pytest.mark.admin_secret class TestSubscriptionCtrl(object): ''' References: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_connection_init https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_connection_terminate ''' def test_connection(self, hge_key, ws_client): init_ws_conn(hge_key, ws_client) obj = { 'type': 'connection_terminate' } ws_client.send(obj) with pytest.raises(queue.Empty): ws_client.get_ws_event(3) @pytest.mark.admin_secret # TODO: remove once parallelization work is completed # only used when running HGE outside the test suite @pytest.mark.requires_an_admin_secret class TestSubscriptionBasicNoAuth: def test_closed_connection_apollo(self, ws_client): # sends empty header so that there is not authentication present in the test ws_client.create_conn() init_msg = { 'type': 'connection_init', 'payload':{'headers':{}} } ws_client.send(init_msg) time.sleep(2) ev = ws_client.get_conn_close_state() assert ev == True, ev def test_closed_connection_graphql_ws(self, ws_client_graphql_ws): # sends empty header so that there is not authentication present in the test ws_client_graphql_ws.create_conn() init_msg = { 'type': 'connection_init', 'payload':{'headers':{}} } ws_client_graphql_ws.send(init_msg) time.sleep(2) ev = ws_client_graphql_ws.get_conn_close_state() assert ev == True, ev @pytest.mark.backend('mssql', 'postgres') @usefixtures('per_class_tests_db_state', 'ws_conn_init') @pytest.mark.admin_secret class TestSubscriptionBasic: @classmethod def dir(cls): return 'queries/subscriptions/basic' @pytest.mark.parametrize("transport", ['http', 'websocket', 'subscription']) def test_negative(self, hge_ctx, transport): check_query_f(hge_ctx, self.dir() + '/negative_test.yaml', transport) ''' Refer: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_connection_error ''' def test_connection_error(self, ws_client): ws_client.send({'type': 'test'}) ev = ws_client.get_ws_event(15) assert ev['type'] == 'connection_error', ev ''' Refer: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_start ''' def test_start(self, ws_client): id = str(uuid.uuid4()) query = """ subscription { hge_tests_test_t1(order_by: {c1: desc}, limit: 1) { c1, c2 } } """ obj = { 'id': id, 'payload': { 'query': query }, 'type': 'start' } ws_client.send(obj) ''' Refer: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_data ''' ev = ws_client.get_ws_query_event(id, 15) assert ev['type'] == 'data' and ev['id'] == id, ev ''' Refer https://github.com/apollographql/subscriptions-transport-ws/blob/01e0b2b65df07c52f5831cce5c858966ba095993/src/server.ts#L306 ''' @pytest.mark.skip(reason="refer to https://github.com/hasura/graphql-engine/pull/387#issuecomment-421343098") def test_start_duplicate(self, ws_client): self.test_start(ws_client) def test_stop_without_id(self, ws_client): obj = { 'type': 'stop' } ws_client.send(obj) ev = ws_client.get_ws_event(3) assert ev['type'] == 'connection_error', ev ''' Refer https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_stop ''' def test_stop(self, ws_client): obj = { 'type': 'stop', 'id': '1' } ws_client.send(obj) with pytest.raises(queue.Empty): ev = ws_client.get_ws_event(3) def test_start_after_stop(self, ws_client): self.test_start(ws_client) self.test_stop(ws_client) ''' Refer: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_complete ''' def test_complete(self, ws_client): query = """ query { hge_tests_test_t1(order_by: {c1: desc}, limit: 1) { c1, c2 } } """ obj = { 'id': '2', 'payload': { 'query': query }, 'type': 'start' } ws_client.send(obj) ev = ws_client.get_ws_query_event('2',3) assert ev['type'] == 'data' and ev['id'] == '2', ev # Check for complete type ev = ws_client.get_ws_query_event('2',3) assert ev['type'] == 'complete' and ev['id'] == '2', ev ## NOTE: The same tests as in TestSubscriptionBasic but with ## the subscription transport being used is `graphql-ws` ## FIXME: There's an issue with the tests being parametrized with both ## postgres and mssql data sources enabled(See issue #2084). @usefixtures('per_method_tests_db_state', 'ws_conn_init_graphql_ws') @pytest.mark.admin_secret class TestSubscriptionBasicGraphQLWS: @classmethod def dir(cls): return 'queries/subscriptions/basic' @pytest.mark.parametrize("transport", ['http', 'websocket', 'subscription']) def test_negative(self, hge_ctx, transport): check_query_f(hge_ctx, self.dir() + '/negative_test.yaml', transport, gqlws=True) def test_connection_error(self, hge_key, ws_client_graphql_ws): if ws_client_graphql_ws.get_conn_close_state(): ws_client_graphql_ws.create_conn() if hge_key == None: ws_client_graphql_ws.init() else: ws_client_graphql_ws.init_as_admin() ws_client_graphql_ws.send({'type': 'test'}) time.sleep(2) ev = ws_client_graphql_ws.get_conn_close_state() assert ev == True, ev def test_start(self, hge_key, ws_client_graphql_ws): if ws_client_graphql_ws.get_conn_close_state(): ws_client_graphql_ws.create_conn() if hge_key == None: ws_client_graphql_ws.init() else: ws_client_graphql_ws.init_as_admin() id = str(uuid.uuid4()) query = """ subscription { hge_tests_test_t1(order_by: {c1: desc}, limit: 1) { c1, c2 } } """ obj = { 'id': id, 'payload': { 'query': query }, 'type': 'subscribe' } ws_client_graphql_ws.send(obj) ev = ws_client_graphql_ws.get_ws_query_event(id, 15) assert ev['type'] == 'next' and ev['id'] == id, ev @pytest.mark.skip(reason="refer to https://github.com/hasura/graphql-engine/pull/387#issuecomment-421343098") def test_start_duplicate(self, hge_key, ws_client_graphql_ws): if ws_client_graphql_ws.get_conn_close_state(): ws_client_graphql_ws.create_conn() if hge_key == None: ws_client_graphql_ws.init() else: ws_client_graphql_ws.init_as_admin() self.test_start(hge_key, ws_client_graphql_ws) def test_stop_without_id(self, hge_key, ws_client_graphql_ws): if ws_client_graphql_ws.get_conn_close_state(): ws_client_graphql_ws.create_conn() if hge_key == None: ws_client_graphql_ws.init() else: ws_client_graphql_ws.init_as_admin() obj = { 'type': 'complete' } ws_client_graphql_ws.send(obj) time.sleep(2) ev = ws_client_graphql_ws.get_conn_close_state() assert ev == True, ev def test_stop(self, hge_key, ws_client_graphql_ws): if ws_client_graphql_ws.get_conn_close_state(): ws_client_graphql_ws.create_conn() if hge_key == None: ws_client_graphql_ws.init() else: ws_client_graphql_ws.init_as_admin() obj = { 'type': 'complete', 'id': '1' } ws_client_graphql_ws.send(obj) time.sleep(2) with pytest.raises(queue.Empty): ev = ws_client_graphql_ws.get_ws_event(3) def test_start_after_stop(self, hge_key, hge_ctx, ws_client_graphql_ws): if ws_client_graphql_ws.get_conn_close_state(): ws_client_graphql_ws.create_conn() if hge_key == None: ws_client_graphql_ws.init() else: ws_client_graphql_ws.init_as_admin() self.test_start(hge_ctx, ws_client_graphql_ws) ## NOTE: test_start leaves a message in the queue, hence clearing it if len(ws_client_graphql_ws.get_queue()) > 0: ws_client_graphql_ws.clear_queue() self.test_stop(hge_ctx, ws_client_graphql_ws) def test_complete(self, hge_key, ws_client_graphql_ws): id = str(uuid.uuid4()) if ws_client_graphql_ws.get_conn_close_state(): ws_client_graphql_ws.create_conn() if hge_key == None: ws_client_graphql_ws.init() else: ws_client_graphql_ws.init_as_admin() query = """ query { hge_tests_test_t1(order_by: {c1: desc}, limit: 1) { c1, c2 } } """ obj = { 'id': id, 'payload': { 'query': query }, 'type': 'subscribe' } ws_client_graphql_ws.send(obj) ev = ws_client_graphql_ws.get_ws_query_event(id, 3) assert ev['type'] == 'next' and ev['id'] == id, ev # Check for complete type ev = ws_client_graphql_ws.get_ws_query_event(id, 3) assert ev['type'] == 'complete' and ev['id'] == id, ev @usefixtures('per_method_tests_db_state','ws_conn_init') @pytest.mark.admin_secret class TestSubscriptionLiveQueries: @classmethod def dir(cls): return 'queries/subscriptions/live_queries' def test_live_queries(self, hge_key, ws_client): ''' Create connection using connection_init ''' headers={} if hge_key is not None: headers['X-Hasura-Admin-Secret'] = hge_key ws_client.init(headers=headers) with open(self.dir() + "/steps.yaml") as c: conf = yaml.load(c) queryTmplt = """ subscription ($result_limit: Int!) { hge_tests_live_query_{0}: hge_tests_test_t2(order_by: {c1: asc}, limit: $result_limit) { c1, c2 } } """ queries = [(0, 1), (1, 2), (2, 2)] liveQs = [] for i, resultLimit in queries: query = queryTmplt.replace('{0}',str(i)) subscrPayload = { 'query': query, 'variables': { 'result_limit': resultLimit } } respLive = ws_client.send_query(subscrPayload, query_id='live_'+str(i), timeout=15) liveQs.append(respLive) ev = next(respLive) assert ev['type'] == 'data', ev assert ev['id'] == 'live_' + str(i), ev assert ev['payload']['data'] == {'hge_tests_live_query_'+str(i): []}, ev['payload']['data'] assert isinstance(conf, list) == True, 'Not an list' for index, step in enumerate(conf): mutationPayload = { 'query': step['query'] } if 'variables' in step and step['variables']: mutationPayload['variables'] = json.loads(step['variables']) expected_resp = json.loads(step['response']) mutResp = ws_client.send_query(mutationPayload,'mutation_'+str(index),timeout=15) ev = next(mutResp) assert ev['type'] == 'data' and ev['id'] == 'mutation_'+str(index), ev assert ev['payload']['data'] == expected_resp, ev['payload']['data'] ev = next(mutResp) assert ev['type'] == 'complete' and ev['id'] == 'mutation_'+str(index), ev for (i, resultLimit), respLive in zip(queries, liveQs): ev = next(respLive) assert ev['type'] == 'data', ev assert ev['id'] == 'live_' + str(i), ev expectedReturnedResponse = [] if 'live_response' in step: expectedReturnedResponse = json.loads(step['live_response']) elif 'returning' in expected_resp[step['name']]: expectedReturnedResponse = expected_resp[step['name']]['returning'] expectedLimitedResponse = expectedReturnedResponse[:resultLimit] expectedLiveResponse = { 'hge_tests_live_query_'+str(i): expectedLimitedResponse } assert ev['payload']['data'] == expectedLiveResponse, ev['payload']['data'] for i, _ in queries: # stop live operation frame = { 'id': 'live_'+str(i), 'type': 'stop' } ws_client.send(frame) with pytest.raises(queue.Empty): ev = ws_client.get_ws_event(3) @usefixtures('per_method_tests_db_state', 'ws_conn_init') @pytest.mark.hge_env('HASURA_GRAPHQL_EXPERIMENTAL_FEATURES', 'streaming_subscriptions') class TestStreamingSubscription: @classmethod def dir(cls): return 'queries/subscriptions/streaming' def test_basic_streaming_subscription_existing_static_data(self, hge_key, hge_ctx, ws_client): ''' Create connection using connection_init ''' headers={} if hge_key is not None: headers['X-Hasura-Admin-Secret'] = hge_key ws_client.init(headers=headers) query = """ subscription ($batch_size: Int!) { hge_tests_stream_query: hge_tests_articles_stream(cursor: {initial_value: {id: 0}}, batch_size: $batch_size) { id title } } """ liveQs = [] articles_to_insert = [] for i in range(10): articles_to_insert.append({"id": i + 1, "title": "Article title {}".format(i + 1)}) insert_many(hge_ctx, {"schema": "hge_tests", "name": "articles"}, articles_to_insert) subscrPayload = { 'query': query, 'variables': { 'batch_size': 2 } } respLive = ws_client.send_query(subscrPayload, query_id='stream_1', timeout=15) liveQs.append(respLive) for idx in range(5): ev = next(respLive) assert ev['type'] == 'data', ev assert ev['id'] == 'stream_1', ev # fetching two rows per batch expected_payload = [ {"id": 2*idx+1, "title": "Article title {}".format(2*idx+1)}, {"id": 2*idx+2, "title": "Article title {}".format(2*idx+2)}] assert ev['payload']['data'] == {'hge_tests_stream_query': expected_payload}, ev['payload']['data'] # stop the streaming subscription frame = { 'id': 'stream_1', 'type': 'stop' } ws_client.send(frame) with pytest.raises(queue.Empty): ev = ws_client.get_ws_event(3) def test_streaming_subscriptions_with_concurrent_data_inserts(self, ws_client): ''' Create connection using connection_init ''' ws_client.init_as_admin() query = """ subscription ($batch_size: Int!, $initial_created_at: timestamptz!) { hge_tests_stream_query: hge_tests_test_t2_stream(cursor: [{initial_value: {created_at: $initial_created_at}, ordering: ASC}], batch_size: $batch_size) { c1 c2 } } """ with open(self.dir() + "/steps.yaml") as c: conf = yaml.load(c) subscrPayload = { 'query': query, 'variables': { 'batch_size': 2, 'initial_created_at': "2020-01-01" } } respLive = ws_client.send_query(subscrPayload, query_id='stream_1', timeout=15) assert isinstance(conf, list) == True, 'Not an list' for index, step in enumerate(conf): mutationPayload = { 'query': step['query'] } if 'variables' in step and step['variables']: mutationPayload['variables'] = json.loads(step['variables']) expected_resp = json.loads(step['response']) mutResp = ws_client.send_query(mutationPayload,'mutation_'+str(index),timeout=15) ev = next(mutResp) assert ev['type'] == 'data' and ev['id'] == 'mutation_'+str(index), ev assert ev['payload']['data'] == expected_resp, ev['payload']['data'] ev = next(mutResp) assert ev['type'] == 'complete' and ev['id'] == 'mutation_'+str(index), ev ev = next(respLive) assert ev['type'] == 'data', ev assert ev['id'] == 'stream_1', ev expectedReturnedResponse = json.loads(step['stream_response']) expectedLiveResponse = { 'hge_tests_stream_query' : expectedReturnedResponse } assert ev['payload']['data'] == expectedLiveResponse, ev['payload']['data'] # stop the streaming subscription frame = { 'id': 'stream_1', 'type': 'stop' } ws_client.send(frame) with pytest.raises(queue.Empty): ev = ws_client.get_ws_event(3) def test_streaming_subscription_with_custom_name_set_for_cursor(self, hge_key, ws_client): ''' Create connection using connection_init ''' headers={} if hge_key is not None: headers['X-Hasura-Admin-Secret'] = hge_key ws_client.init(headers=headers) query = """ subscription ($batch_size: Int!) { hge_tests_stream_query: hge_tests_users_stream(cursor: {initial_value: {userId: 0}}, batch_size: $batch_size) { userId name } } """ liveQs = [] subscrPayload = { 'query': query, 'variables': { 'batch_size': 1 } } respLive = ws_client.send_query(subscrPayload, query_id='stream_1', timeout=15) liveQs.append(respLive) for idx in range(2): ev = next(respLive) assert ev['type'] == 'data', ev assert ev['id'] == 'stream_1', ev # fetching two rows per batch expected_payload = [ {"userId": idx + 1, "name": "Name {}".format(idx+1)}] assert ev['payload']['data'] == {'hge_tests_stream_query': expected_payload}, ev['payload']['data'] # stop the streaming subscription frame = { 'id': 'stream_1', 'type': 'stop' } ws_client.send(frame) with pytest.raises(queue.Empty): ev = ws_client.get_ws_event(3) @usefixtures('per_method_tests_db_state', 'ws_conn_init_graphql_ws') @pytest.mark.admin_secret class TestSubscriptionLiveQueriesForGraphQLWS: @classmethod def dir(cls): return 'queries/subscriptions/live_queries' def test_live_queries(self, hge_key, ws_client_graphql_ws): ''' Create connection using connection_init ''' headers={} if hge_key is not None: headers['X-Hasura-Admin-Secret'] = hge_key ws_client_graphql_ws.init(headers=headers) with open(self.dir() + "/steps.yaml") as c: conf = yaml.load(c) queryTmplt = """ subscription ($result_limit: Int!) { hge_tests_live_query_{0}: hge_tests_test_t2(order_by: {c1: asc}, limit: $result_limit) { c1, c2 } } """ queries = [(0, 1), (1, 2), (2, 2)] liveQs = [] for i, resultLimit in queries: query = queryTmplt.replace('{0}',str(i)) subscrPayload = { 'query': query, 'variables': { 'result_limit': resultLimit } } respLive = ws_client_graphql_ws.send_query(subscrPayload, query_id='live_'+str(i), timeout=15) liveQs.append(respLive) ev = next(respLive) assert ev['type'] == 'next', ev assert ev['id'] == 'live_' + str(i), ev assert ev['payload']['data'] == {'hge_tests_live_query_'+str(i): []}, ev['payload']['data'] assert isinstance(conf, list) == True, 'Not an list' for index, step in enumerate(conf): mutationPayload = { 'query': step['query'] } if 'variables' in step and step['variables']: mutationPayload['variables'] = json.loads(step['variables']) expected_resp = json.loads(step['response']) mutResp = ws_client_graphql_ws.send_query(mutationPayload,'mutation_'+str(index),timeout=15) ev = next(mutResp) assert ev['type'] == 'next' and ev['id'] == 'mutation_'+str(index), ev assert ev['payload']['data'] == expected_resp, ev['payload']['data'] ev = next(mutResp) assert ev['type'] == 'complete' and ev['id'] == 'mutation_'+str(index), ev for (i, resultLimit), respLive in zip(queries, liveQs): ev = next(respLive) assert ev['type'] == 'next', ev assert ev['id'] == 'live_' + str(i), ev expectedReturnedResponse = [] if 'live_response' in step: expectedReturnedResponse = json.loads(step['live_response']) elif 'returning' in expected_resp[step['name']]: expectedReturnedResponse = expected_resp[step['name']]['returning'] expectedLimitedResponse = expectedReturnedResponse[:resultLimit] expectedLiveResponse = { 'hge_tests_live_query_'+str(i): expectedLimitedResponse } assert ev['payload']['data'] == expectedLiveResponse, ev['payload']['data'] for i, _ in queries: # stop live operation frame = { 'id': 'live_'+str(i), 'type': 'complete' } ws_client_graphql_ws.send(frame) ws_client_graphql_ws.clear_queue() @pytest.mark.backend('mssql', 'postgres') @usefixtures('per_class_tests_db_state') @pytest.mark.admin_secret class TestSubscriptionMultiplexingPostgresMSSQL: @classmethod def dir(cls): return 'queries/subscriptions/multiplexing' def test_extraneous_session_variables_are_discarded_from_query(self, hge_key, hge_ctx): with open(self.dir() + '/articles_query.yaml') as c: config = yaml.load(c) query = config['query'] session_variables = { "X-Hasura-Role":"public", "X-Hasura-User-Id":"1" # extraneous session variable } response = get_explain_graphql_query_response(hge_ctx, hge_key, query, {}, session_variables) # The input session variables should be ignored because the only check for the role is # if `is_public` is `true` assert response["variables"]["session"] == {}, response["variables"] session_variables = { "X-Hasura-Role":"user", "X-Hasura-User-Id":"1", "X-Hasura-Allowed-Ids":"{1,3,4}" # extraneous session variable } response = get_explain_graphql_query_response(hge_ctx, hge_key, query, {}, session_variables) # The input session variable should not be ignored because the `user` role can only # select those roles where `user_id = X-Hasura-User-Id` assert response["variables"]["session"] == {'x-hasura-user-id':"1"}, response["variables"] # test case for https://github.com/hasura/graphql-engine-mono/issues/3689 @usefixtures('per_class_tests_db_state') @pytest.mark.admin_secret class TestSubscriptionMultiplexingPostgres: @classmethod def dir(cls): return 'queries/subscriptions/multiplexing' def test_simple_variables_are_parameterized(self, hge_key, hge_ctx): with open(self.dir() + '/articles_query_simple_variable.yaml') as c: config = yaml.load(c) response = get_explain_graphql_query_response(hge_ctx, hge_key, config['query'], config['variables'], {}) assert response["variables"]["synthetic"] == ['1'], response["variables"] def test_array_variables_are_parameterized(self, hge_key, hge_ctx): with open(self.dir() + '/articles_query_array_variable.yaml') as c: config = yaml.load(c) response = get_explain_graphql_query_response(hge_ctx, hge_key, config['query'], config['variables'], {}) assert response["variables"]["synthetic"] == ['{1,2,3}'], response["variables"] @pytest.mark.backend('postgres') @usefixtures('per_class_tests_db_state', 'ws_conn_init') @pytest.mark.admin_secret class TestSubscriptionUDFWithSessionArg: """ Test a user-defined function which uses the entire session variables as argument """ query = """ subscription { me { id name } } """ @classmethod def dir(cls): return 'queries/subscriptions/udf_session_args' def test_user_defined_function_with_session_argument(self, hge_key, ws_client): headers = {'x-hasura-role': 'user', 'x-hasura-user-id': '42'} if hge_key is not None: headers['X-Hasura-Admin-Secret'] = hge_key ws_client.init(headers=headers) payload = {'query': self.query} resp = ws_client.send_query(payload, timeout=15) ev = next(resp) assert ev['type'] == 'data', ev assert ev['payload']['data'] == {'me': [{'id': '42', 'name': 'Charlie'}]}, ev['payload']['data'] @pytest.fixture(scope='class') def add_customized_source(current_backend, add_source, hge_ctx): customization = { 'root_fields': { 'namespace': 'my_source', 'prefix': 'fpref_', 'suffix': '_fsuff', }, 'type_names': { 'prefix': 'tpref_', 'suffix': '_tsuff', } } if current_backend == 'mssql': hge_ctx.v1metadataq({ 'type': 'mssql_add_source', 'args': { 'name': 'mssql1', 'configuration': { 'connection_info': { 'database_url': { 'from_env': 'HASURA_GRAPHQL_MSSQL_SOURCE_URL', }, }, }, 'customization': customization, }, }) else: add_source('pg1', customization=customization) @pytest.mark.backend('mssql', 'postgres') @usefixtures('add_customized_source', 'per_class_tests_db_state', 'ws_conn_init') @pytest.mark.admin_secret class TestSubscriptionCustomizedSourceMSSQLPostgres: @classmethod def dir(cls): return 'queries/subscriptions/customized_source' setup_metadata_api_version = "v2" ''' Refer: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_complete ''' def test_complete(self, ws_client): id = str(uuid.uuid4()) query = """ subscription MySubscription { a: my_source { b: fpref_author_fsuff { id name } } } """ obj = { 'id': id, 'payload': { 'query': query }, 'type': 'start' } ws_client.send(obj) ''' Refer: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_data ''' ev = ws_client.get_ws_query_event(id, 15) assert ev['type'] == 'data' and ev['id'] == id, ev assert ev['payload']['data']['a'] == OrderedDict([('b', [OrderedDict([('id', 1), ('name', 'Author 1')]), OrderedDict([('id', 2), ('name', 'Author 2')])])]), ev def test_double_alias(self, ws_client): ''' This should give an error even though @_multiple_top_level_fields is specified. The two different aliases for `my_source` mean that we would have to wrap different parts of the DB response in different namespace fields, which is not currently possible. ''' query = """ subscription MySubscription @_multiple_top_level_fields { alias1: my_source { fpref_author_fsuff { id name } } alias2: my_source { fpref_author_fsuff { id name } } } """ obj = { 'id': '2', 'payload': { 'query': query }, 'type': 'start' } ws_client.send(obj) ''' Refer: https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md#gql_data ''' ev = ws_client.get_ws_query_event('2',15) assert ev['type'] == 'error' and ev['id'] == '2', ev assert ev['payload']['errors'] == [OrderedDict([('extensions', OrderedDict([('path', '$'), ('code', 'validation-failed')])), ('message', 'subscriptions must select one top level field')])], ev @pytest.mark.backend('mssql') @usefixtures('per_class_tests_db_state', 'ws_conn_init') @pytest.mark.admin_secret class TestSubscriptionMSSQLChunkedResults: @classmethod def dir(cls): return 'queries/subscriptions/mssql' query = """ subscription { hge_tests_test_subscriptions { field1 } } """ def test_chunked_results(self, ws_client): obj = { 'id': '1', 'payload': { 'query': self.query }, 'type': 'start' } ws_client.send(obj) ev = ws_client.get_ws_query_event('1',15) assert ev['type'] == 'data' and ev['id'] == '1', ev assert not "errors" in ev['payload'], ev @usefixtures('per_method_tests_db_state', 'ws_conn_init') @pytest.mark.hge_env('HASURA_GRAPHQL_EXPERIMENTAL_FEATURES', 'streaming_subscriptions') # Only citus and vanilla PG have this bugfix/behavior: @pytest.mark.backend('citus', 'postgres') class TestStreamingSubscriptionWithTies: """ Test the WITH TIES functionality for streaming subscriptions with duplicate cursor values. This addresses the issue where formerly non-unique cursor columns can cause data loss in streaming subscriptions due to the WHERE col > last_cursor_value LIMIT batch_size pattern skipping rows with duplicate cursor values when they can't all fit in batch_size. """ @classmethod def dir(cls): return 'queries/subscriptions/streaming' def test_streaming_subscription_with_duplicate_cursor_values(self, hge_ctx, hge_key, ws_client): """ Test that streaming subscriptions with duplicate cursor values return all rows using WITH TIES functionality on Postgres and Citus. This test: 1. Uses existing articles table but adds priority column for testing 2. Inserts data with duplicate cursor values on priority column 3. Verifies that WITH TIES returns all rows with duplicate cursor values """ # Add priority column to existing articles table for our test. Existing # teardown should still work fine. # # Note that while in theory the engine could be using `id` as a # tiebreaker here, implementing things this way would have been much # more difficult and furthermore we can't always be sure that a table # (that the customer has already tracked) has any unique columns setup_sql = ''' ALTER TABLE hge_tests.articles ADD COLUMN IF NOT EXISTS priority INT; -- Clear existing data and insert test data with duplicate priority values DELETE FROM hge_tests.articles; INSERT INTO hge_tests.articles (id, priority) VALUES -- batch 1 (oversized, excercising WITH TIES) (7, 1), (6, 2), (5, 2), (4, 2), -- batch 2 (full batch) (1, 3), (2, 4), -- batch 3 (undersized, not enough data to fill batch) (3, 5); ''' hge_ctx.v1q({ 'type': 'run_sql', 'args': {'sql': setup_sql} }) ## Debugging: # verify_result = hge_ctx.v1q({ # 'type': 'run_sql', # 'args': {'sql': 'SELECT id, priority FROM hge_tests.articles ORDER BY priority, id;'} # }) # print(f"Inserted data: {verify_result}") # Initialize WebSocket connection ws_client.init_as_admin() # Start streaming subscription with batch_size=2 and cursor on priority column query = """ subscription ($batch_size: Int!) { stream_data: hge_tests_articles_stream( cursor: [{initial_value: {priority: 0}, ordering: ASC}], batch_size: $batch_size ) { id priority } } """ subscrPayload = {'query': query, 'variables': {'batch_size': 2}} respLive = ws_client.send_query(subscrPayload, query_id='stream_test', timeout=15) # Test WITH TIES functionality: With batch_size=2, we should get more than 2 rows # when there are duplicate cursor values due to WITH TIES ev = next(respLive) assert ev['type'] == 'data', f"Expected data event, got {ev['type']}. Full event: {ev}" assert ev['id'] == 'stream_test', ev first_batch = ev['payload']['data']['stream_data'] # With batch size 2 we return [1,2] plus all remaining "ties" of priority 2 assert [row['priority'] for row in first_batch] == [1, 2, 2, 2], first_batch # The order wrt id is undefined assert sorted([row['id'] for row in first_batch]) == [4, 5, 6, 7], first_batch ev = next(respLive) second_batch = ev['payload']['data']['stream_data'] assert [row['priority'] for row in second_batch] == [3, 4], second_batch assert sorted([row['id'] for row in second_batch]) == [1, 2], second_batch ev = next(respLive) third_batch = ev['payload']['data']['stream_data'] assert [row['priority'] for row in third_batch] == [5], third_batch assert sorted([row['id'] for row in third_batch]) == [3], third_batch # Clean stop frame = {'id': 'stream_test', 'type': 'stop'} ws_client.send(frame)