mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-20 15:09:02 +03:00
(Fix #8267) Handle subscriptions in MSSQL when results exceed 2048 characters
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3959 GitOrigin-RevId: ea037c9dc4392d1b98ee086f6c87f79ce8ea5c8f
This commit is contained in:
parent
a13ed140e8
commit
23520f67d0
@ -142,7 +142,8 @@ function:
|
|||||||
|
|
||||||
### Bug fixes and improvements
|
### Bug fixes and improvements
|
||||||
|
|
||||||
- server: add jsonb to string cast support - postgres
|
- server: Fix regression in MSSQL subscriptions when results exceed 2048 characters (#8267)
|
||||||
|
- server: add jsonb to string cast support - postgres
|
||||||
- server: improve performance of fetching postgres catalog metadata for tables and functions
|
- server: improve performance of fetching postgres catalog metadata for tables and functions
|
||||||
- server: Queries present in query collections, such as allow-list, and rest-endpoints are now validated (against the schema)
|
- server: Queries present in query collections, such as allow-list, and rest-endpoints are now validated (against the schema)
|
||||||
- server: Redesigns internal implementation of webhook transforms.
|
- server: Redesigns internal implementation of webhook transforms.
|
||||||
|
@ -11,6 +11,7 @@ module Database.MSSQL.Transaction
|
|||||||
singleRowQueryE,
|
singleRowQueryE,
|
||||||
multiRowQuery,
|
multiRowQuery,
|
||||||
multiRowQueryE,
|
multiRowQueryE,
|
||||||
|
forJsonQueryE,
|
||||||
buildGenericQueryTxE,
|
buildGenericQueryTxE,
|
||||||
withTxET,
|
withTxET,
|
||||||
)
|
)
|
||||||
@ -123,6 +124,24 @@ singleRowQueryE ef = rawQueryE ef singleRowResult
|
|||||||
singleRowResult (MSSQLResult [row]) = ODBC.fromRow row
|
singleRowResult (MSSQLResult [row]) = ODBC.fromRow row
|
||||||
singleRowResult (MSSQLResult _) = Left "expecting single row"
|
singleRowResult (MSSQLResult _) = Left "expecting single row"
|
||||||
|
|
||||||
|
-- | MSSQL splits up results that have a @SELECT .. FOR JSON@ at the top-level
|
||||||
|
-- into multiple rows with a single column, see
|
||||||
|
-- https://docs.microsoft.com/en-us/sql/relational-databases/json/format-query-results-as-json-with-for-json-sql-server?view=sql-server-ver15#output-of-the-for-json-clause
|
||||||
|
--
|
||||||
|
-- This function simply concatenates each single-column row into one long 'Text' string.
|
||||||
|
forJsonQueryE ::
|
||||||
|
forall m e.
|
||||||
|
MonadIO m =>
|
||||||
|
(MSSQLTxError -> e) ->
|
||||||
|
ODBC.Query ->
|
||||||
|
TxET e m Text
|
||||||
|
forJsonQueryE ef = rawQueryE ef concatRowResult
|
||||||
|
where
|
||||||
|
concatRowResult :: MSSQLResult -> Either String Text
|
||||||
|
concatRowResult (MSSQLResult []) = pure mempty
|
||||||
|
concatRowResult (MSSQLResult rows@(r1 : _)) | length r1 == 1 = mconcat <$> mapM ODBC.fromRow rows
|
||||||
|
concatRowResult (MSSQLResult (r1 : _)) = Left $ "forJsonQueryE: Expected single-column results, but got " <> show (length r1) <> " columns"
|
||||||
|
|
||||||
-- | Useful for building query transactions which return multiple rows.
|
-- | Useful for building query transactions which return multiple rows.
|
||||||
--
|
--
|
||||||
-- @
|
-- @
|
||||||
|
@ -150,9 +150,7 @@ msDBSubscriptionExplain (SubscriptionQueryPlan plan sourceConfig variables _) =
|
|||||||
explainInfo <- liftEitherM $ runExceptT $ (mssqlRunReadOnly mssqlExecCtx) (runShowplan query)
|
explainInfo <- liftEitherM $ runExceptT $ (mssqlRunReadOnly mssqlExecCtx) (runShowplan query)
|
||||||
pure $ SubscriptionQueryPlanExplanation (T.toTxt query) explainInfo variables
|
pure $ SubscriptionQueryPlanExplanation (T.toTxt query) explainInfo variables
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
-- | Producing the correct SQL-level list comprehension to multiplex a query
|
||||||
-- Producing the correct SQL-level list comprehension to multiplex a query
|
|
||||||
|
|
||||||
-- Problem description:
|
-- Problem description:
|
||||||
--
|
--
|
||||||
-- Generate a query that repeats the same query N times but with
|
-- Generate a query that repeats the same query N times but with
|
||||||
@ -160,7 +158,9 @@ msDBSubscriptionExplain (SubscriptionQueryPlan plan sourceConfig variables _) =
|
|||||||
--
|
--
|
||||||
-- [ Select x y | (x,y) <- [..] ]
|
-- [ Select x y | (x,y) <- [..] ]
|
||||||
--
|
--
|
||||||
|
-- Caution: Be aware that this query has a @FOR JSON@ clause at the top-level
|
||||||
|
-- and hence its results may be split up across multiple rows. Use
|
||||||
|
-- 'Database.MSSQL.Transaction.forJsonQueryE' to handle this.
|
||||||
multiplexRootReselect ::
|
multiplexRootReselect ::
|
||||||
[(CohortId, CohortVariables)] ->
|
[(CohortId, CohortVariables)] ->
|
||||||
TSQL.Reselect ->
|
TSQL.Reselect ->
|
||||||
|
@ -12,7 +12,7 @@ import Data.ByteString qualified as B
|
|||||||
import Data.String (fromString)
|
import Data.String (fromString)
|
||||||
import Data.Text.Encoding (encodeUtf8)
|
import Data.Text.Encoding (encodeUtf8)
|
||||||
import Data.Text.Extended
|
import Data.Text.Extended
|
||||||
import Database.MSSQL.Transaction (singleRowQueryE)
|
import Database.MSSQL.Transaction (forJsonQueryE)
|
||||||
import Database.ODBC.SQLServer qualified as ODBC
|
import Database.ODBC.SQLServer qualified as ODBC
|
||||||
import Hasura.Backends.MSSQL.Connection
|
import Hasura.Backends.MSSQL.Connection
|
||||||
import Hasura.Backends.MSSQL.Instances.Execute
|
import Hasura.Backends.MSSQL.Instances.Execute
|
||||||
@ -121,7 +121,10 @@ executeMultiplexedQuery mssqlExecCtx query = do
|
|||||||
let parseResult r = J.eitherDecodeStrict (encodeUtf8 r) `onLeft` \s -> throw400 ParseFailed (fromString s)
|
let parseResult r = J.eitherDecodeStrict (encodeUtf8 r) `onLeft` \s -> throw400 ParseFailed (fromString s)
|
||||||
convertFromJSON :: [CohortResult] -> [(CohortId, B.ByteString)]
|
convertFromJSON :: [CohortResult] -> [(CohortId, B.ByteString)]
|
||||||
convertFromJSON = map \(CohortResult (cid, cresult)) -> (cid, encodeUtf8 cresult)
|
convertFromJSON = map \(CohortResult (cid, cresult)) -> (cid, encodeUtf8 cresult)
|
||||||
textResult <- run $ mssqlRunReadOnly mssqlExecCtx $ singleRowQueryE defaultMSSQLTxErrorHandler query
|
-- Because the 'query' will have a @FOR JSON@ clause at the toplevel it will
|
||||||
|
-- be split across multiple rows, hence use of 'forJsonQueryE' which takes
|
||||||
|
-- care of concatenating the results.
|
||||||
|
textResult <- run $ mssqlRunReadOnly mssqlExecCtx $ forJsonQueryE defaultMSSQLTxErrorHandler query
|
||||||
parsedResult <- parseResult textResult
|
parsedResult <- parseResult textResult
|
||||||
pure $ convertFromJSON parsedResult
|
pure $ convertFromJSON parsedResult
|
||||||
|
|
||||||
|
@ -5,3 +5,4 @@ args:
|
|||||||
source: mssql
|
source: mssql
|
||||||
sql: |
|
sql: |
|
||||||
drop table hge_tests.test_t1
|
drop table hge_tests.test_t1
|
||||||
|
drop schema hge_tests
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
type: bulk
|
||||||
|
args:
|
||||||
|
|
||||||
|
- type: mssql_run_sql
|
||||||
|
args:
|
||||||
|
source: mssql
|
||||||
|
sql: |
|
||||||
|
create schema hge_tests
|
||||||
|
|
||||||
|
create table hge_tests.test_subscriptions (
|
||||||
|
id int primary key,
|
||||||
|
field1 varchar(max)
|
||||||
|
)
|
||||||
|
|
||||||
|
- type: mssql_run_sql
|
||||||
|
args:
|
||||||
|
source: mssql
|
||||||
|
sql: |
|
||||||
|
INSERT INTO
|
||||||
|
hge_tests.test_subscriptions(id, field1)
|
||||||
|
VALUES
|
||||||
|
(1, '1234567890'),
|
||||||
|
(2, '12345678901234567890'),
|
||||||
|
(3, '123456789012345678901234567890'),
|
||||||
|
(4, '1234567890123456789012345678901234567890'),
|
||||||
|
(5, '12345678901234567890123456789012345678901234567890'),
|
||||||
|
(6, '123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(7, '1234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(8, '12345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(9, '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(10, '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(11, '1234567890'),
|
||||||
|
(12, '12345678901234567890'),
|
||||||
|
(13, '123456789012345678901234567890'),
|
||||||
|
(14, '1234567890123456789012345678901234567890'),
|
||||||
|
(15, '12345678901234567890123456789012345678901234567890'),
|
||||||
|
(16, '123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(17, '1234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(18, '12345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(19, '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(20, '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(21, '1234567890'),
|
||||||
|
(22, '12345678901234567890'),
|
||||||
|
(23, '123456789012345678901234567890'),
|
||||||
|
(24, '1234567890123456789012345678901234567890'),
|
||||||
|
(25, '12345678901234567890123456789012345678901234567890'),
|
||||||
|
(26, '123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(27, '1234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(28, '12345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(29, '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(30, '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(31, '1234567890'),
|
||||||
|
(32, '12345678901234567890'),
|
||||||
|
(33, '123456789012345678901234567890'),
|
||||||
|
(34, '1234567890123456789012345678901234567890'),
|
||||||
|
(35, '12345678901234567890123456789012345678901234567890'),
|
||||||
|
(36, '123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(37, '1234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(38, '12345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(39, '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'),
|
||||||
|
(40, '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890')
|
@ -0,0 +1,9 @@
|
|||||||
|
type: bulk
|
||||||
|
args:
|
||||||
|
- type: mssql_run_sql
|
||||||
|
args:
|
||||||
|
source: mssql
|
||||||
|
sql: |
|
||||||
|
drop table hge_tests.test_subscriptions
|
||||||
|
|
||||||
|
drop schema hge_tests
|
@ -0,0 +1,9 @@
|
|||||||
|
type: bulk
|
||||||
|
args:
|
||||||
|
|
||||||
|
- type: mssql_track_table
|
||||||
|
args:
|
||||||
|
source: mssql
|
||||||
|
table:
|
||||||
|
schema: hge_tests
|
||||||
|
name: test_subscriptions
|
@ -0,0 +1,9 @@
|
|||||||
|
type: bulk
|
||||||
|
args:
|
||||||
|
|
||||||
|
- type: mssql_untrack_table
|
||||||
|
args:
|
||||||
|
source: mssql
|
||||||
|
table:
|
||||||
|
schema: hge_tests
|
||||||
|
name: test_subscriptions
|
@ -197,7 +197,7 @@ class TestSubscriptionBasic:
|
|||||||
ev = ws_client.get_ws_query_event('2',3)
|
ev = ws_client.get_ws_query_event('2',3)
|
||||||
assert ev['type'] == 'complete' and ev['id'] == '2', ev
|
assert ev['type'] == 'complete' and ev['id'] == '2', ev
|
||||||
|
|
||||||
## NOTE: The same tests as in TestSubcscriptionBasic but with
|
## NOTE: The same tests as in TestSubscriptionBasic but with
|
||||||
## the subscription transport being used is `graphql-ws`
|
## the subscription transport being used is `graphql-ws`
|
||||||
## FIXME: There's an issue with the tests being parametrized with both
|
## FIXME: There's an issue with the tests being parametrized with both
|
||||||
## postgres and mssql data sources enabled(See issue #2084).
|
## postgres and mssql data sources enabled(See issue #2084).
|
||||||
@ -644,3 +644,31 @@ class TestSubscriptionCustomizedSourceCommon:
|
|||||||
ev = ws_client.get_ws_query_event('2',15)
|
ev = ws_client.get_ws_query_event('2',15)
|
||||||
assert ev['type'] == 'error' and ev['id'] == '2', ev
|
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
|
assert ev['payload']['errors'] == [OrderedDict([('extensions', OrderedDict([('path', '$'), ('code', 'validation-failed')])), ('message', 'subscriptions must select one top level field')])], ev
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("backend", ['mssql'])
|
||||||
|
@usefixtures('per_class_tests_db_state', 'ws_conn_init')
|
||||||
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user