diff --git a/client/trino-client/src/main/java/io/trino/client/StatementClientV1.java b/client/trino-client/src/main/java/io/trino/client/StatementClientV1.java index 459f624f8f3c..a724cc1492d2 100644 --- a/client/trino-client/src/main/java/io/trino/client/StatementClientV1.java +++ b/client/trino-client/src/main/java/io/trino/client/StatementClientV1.java @@ -64,7 +64,7 @@ class StatementClientV1 private static final MediaType MEDIA_TYPE_TEXT = MediaType.parse("text/plain; charset=utf-8"); private static final JsonCodec QUERY_RESULTS_CODEC = jsonCodec(QueryResults.class); - private static final Splitter SESSION_HEADER_SPLITTER = Splitter.on('=').limit(2).trimResults(); + private static final Splitter COLLECTION_HEADER_SPLITTER = Splitter.on('=').limit(2).trimResults(); private static final String USER_AGENT_VALUE = StatementClientV1.class.getSimpleName() + "/" + firstNonNull(StatementClientV1.class.getPackage().getImplementationVersion(), "unknown"); @@ -395,7 +395,7 @@ private void processResponse(Headers headers, QueryResults results) setPath.set(headers.get(TRINO_HEADERS.responseSetPath())); for (String setSession : headers.values(TRINO_HEADERS.responseSetSession())) { - List keyValue = SESSION_HEADER_SPLITTER.splitToList(setSession); + List keyValue = COLLECTION_HEADER_SPLITTER.splitToList(setSession); if (keyValue.size() != 2) { continue; } @@ -404,7 +404,7 @@ private void processResponse(Headers headers, QueryResults results) resetSessionProperties.addAll(headers.values(TRINO_HEADERS.responseClearSession())); for (String setRole : headers.values(TRINO_HEADERS.responseSetRole())) { - List keyValue = SESSION_HEADER_SPLITTER.splitToList(setRole); + List keyValue = COLLECTION_HEADER_SPLITTER.splitToList(setRole); if (keyValue.size() != 2) { continue; } @@ -412,7 +412,7 @@ private void processResponse(Headers headers, QueryResults results) } for (String entry : headers.values(TRINO_HEADERS.responseAddedPrepare())) { - List keyValue = SESSION_HEADER_SPLITTER.splitToList(entry); + List keyValue = COLLECTION_HEADER_SPLITTER.splitToList(entry); if (keyValue.size() != 2) { continue; } diff --git a/client/trino-jdbc/src/test/java/io/trino/jdbc/TestJdbcPreparedStatement.java b/client/trino-jdbc/src/test/java/io/trino/jdbc/TestJdbcPreparedStatement.java index a5bf62addca9..6821b63f68d7 100644 --- a/client/trino-jdbc/src/test/java/io/trino/jdbc/TestJdbcPreparedStatement.java +++ b/client/trino-jdbc/src/test/java/io/trino/jdbc/TestJdbcPreparedStatement.java @@ -14,6 +14,8 @@ package io.trino.jdbc; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import io.airlift.log.Logging; import io.trino.client.ClientTypeSignature; import io.trino.client.ClientTypeSignatureParameter; @@ -49,6 +51,7 @@ import java.time.ZoneOffset; import java.util.Calendar; import java.util.TimeZone; +import java.util.stream.LongStream; import static com.google.common.base.Strings.repeat; import static com.google.common.base.Verify.verify; @@ -72,6 +75,8 @@ public class TestJdbcPreparedStatement { + private static final int HEADER_SIZE_LIMIT = 16 * 1024; + private TestingTrinoServer server; @BeforeClass @@ -79,7 +84,12 @@ public void setup() throws Exception { Logging.initialize(); - server = TestingTrinoServer.create(); + server = TestingTrinoServer.builder() + .setProperties(ImmutableMap.builder() + .put("http-server.max-request-header-size", format("%sB", HEADER_SIZE_LIMIT)) + .put("http-server.max-response-header-size", format("%sB", HEADER_SIZE_LIMIT)) + .buildOrThrow()) + .build(); server.installPlugin(new BlackHolePlugin()); server.installPlugin(new MemoryPlugin()); server.createCatalog("blackhole", "blackhole"); @@ -208,9 +218,9 @@ public void testGetParameterMetaData() try (PreparedStatement statement = connection.prepareStatement( "SELECT ? FROM test_get_parameterMetaData WHERE c_boolean = ? AND c_decimal = ? " + - "AND c_decimal_2 = ? AND c_varchar = ? AND c_varchar_2 = ? AND c_row = ? " + - "AND c_array = ? AND c_map = ? AND c_tinyint = ? AND c_integer = ? AND c_bigint = ? " + - "AND c_smallint = ? AND c_real = ? AND c_double = ?")) { + "AND c_decimal_2 = ? AND c_varchar = ? AND c_varchar_2 = ? AND c_row = ? " + + "AND c_array = ? AND c_map = ? AND c_tinyint = ? AND c_integer = ? AND c_bigint = ? " + + "AND c_smallint = ? AND c_real = ? AND c_double = ?")) { ParameterMetaData parameterMetaData = statement.getParameterMetaData(); assertEquals(parameterMetaData.getParameterCount(), 15); @@ -387,6 +397,23 @@ public void testDeallocate() } } + @Test + public void testLargePreparedStatement() + throws Exception + { + int elements = HEADER_SIZE_LIMIT + 1; + try (Connection connection = createConnection(); + PreparedStatement statement = connection.prepareStatement("VALUES ?" + repeat(", ?", elements - 1))) { + for (int i = 0; i < elements; i++) { + statement.setLong(i + 1, i); + } + try (ResultSet resultSet = statement.executeQuery()) { + assertThat(readRows(resultSet).stream().map(Iterables::getOnlyElement)) + .containsExactlyInAnyOrder(LongStream.range(0, elements).boxed().toArray()); + } + } + } + @Test public void testExecuteUpdate() throws Exception diff --git a/core/trino-main/src/main/java/io/trino/server/HttpRequestSessionContextFactory.java b/core/trino-main/src/main/java/io/trino/server/HttpRequestSessionContextFactory.java index e492a2c7a449..f8c84b1c23ba 100644 --- a/core/trino-main/src/main/java/io/trino/server/HttpRequestSessionContextFactory.java +++ b/core/trino-main/src/main/java/io/trino/server/HttpRequestSessionContextFactory.java @@ -24,6 +24,7 @@ import io.trino.client.ProtocolHeaders; import io.trino.metadata.Metadata; import io.trino.security.AccessControl; +import io.trino.server.protocol.PreparedStatementEncoder; import io.trino.spi.security.AccessDeniedException; import io.trino.spi.security.GroupProvider; import io.trino.spi.security.Identity; @@ -72,13 +73,15 @@ public class HttpRequestSessionContextFactory private static final Splitter DOT_SPLITTER = Splitter.on('.'); public static final String AUTHENTICATED_IDENTITY = "trino.authenticated-identity"; + private final PreparedStatementEncoder preparedStatementEncoder; private final Metadata metadata; private final GroupProvider groupProvider; private final AccessControl accessControl; @Inject - public HttpRequestSessionContextFactory(Metadata metadata, GroupProvider groupProvider, AccessControl accessControl) + public HttpRequestSessionContextFactory(PreparedStatementEncoder preparedStatementEncoder, Metadata metadata, GroupProvider groupProvider, AccessControl accessControl) { + this.preparedStatementEncoder = requireNonNull(preparedStatementEncoder, "preparedStatementEncoder is null"); this.metadata = requireNonNull(metadata, "metadata is null"); this.groupProvider = requireNonNull(groupProvider, "groupProvider is null"); this.accessControl = requireNonNull(accessControl, "accessControl is null"); @@ -378,10 +381,10 @@ private static void assertRequest(boolean expression, String format, Object... a } } - private static Map parsePreparedStatementsHeaders(ProtocolHeaders protocolHeaders, MultivaluedMap headers) + private Map parsePreparedStatementsHeaders(ProtocolHeaders protocolHeaders, MultivaluedMap headers) { ImmutableMap.Builder preparedStatements = ImmutableMap.builder(); - parseProperty(headers, protocolHeaders.requestPreparedStatement()).forEach((key, sqlString) -> { + parseProperty(headers, protocolHeaders.requestPreparedStatement()).forEach((key, value) -> { String statementName; try { statementName = urlDecode(key); @@ -389,6 +392,7 @@ private static Map parsePreparedStatementsHeaders(ProtocolHeader catch (IllegalArgumentException e) { throw badRequest(format("Invalid %s header: %s", protocolHeaders.requestPreparedStatement(), e.getMessage())); } + String sqlString = preparedStatementEncoder.decodePreparedStatementFromHeader(value); // Validate statement SqlParser sqlParser = new SqlParser(); diff --git a/core/trino-main/src/main/java/io/trino/server/ProtocolConfig.java b/core/trino-main/src/main/java/io/trino/server/ProtocolConfig.java index 363339669600..30225ba9d6d6 100644 --- a/core/trino-main/src/main/java/io/trino/server/ProtocolConfig.java +++ b/core/trino-main/src/main/java/io/trino/server/ProtocolConfig.java @@ -16,6 +16,7 @@ import io.airlift.configuration.Config; import io.airlift.configuration.ConfigDescription; +import javax.annotation.Nonnull; import javax.validation.constraints.Pattern; import java.util.Optional; @@ -23,6 +24,8 @@ public class ProtocolConfig { private String alternateHeaderName; + private int preparedStatementCompressionThreshold = 2 * 1024; + private int preparedStatementCompressionMinimalGain = 512; @Deprecated public Optional<@Pattern(regexp = "[A-Za-z]+") String> getAlternateHeaderName() @@ -38,4 +41,32 @@ public ProtocolConfig setAlternateHeaderName(String alternateHeaderName) this.alternateHeaderName = alternateHeaderName; return this; } + + @Nonnull + public int getPreparedStatementCompressionThreshold() + { + return preparedStatementCompressionThreshold; + } + + @Config("protocol.v1.prepared-statement-compression.length-threshold") + @ConfigDescription("Prepared statements longer than the configured value will be compressed") + public ProtocolConfig setPreparedStatementCompressionThreshold(int preparedStatementCompressionThreshold) + { + this.preparedStatementCompressionThreshold = preparedStatementCompressionThreshold; + return this; + } + + @Nonnull + public int getPreparedStatementCompressionMinimalGain() + { + return preparedStatementCompressionMinimalGain; + } + + @Config("protocol.v1.prepared-statement-compression.min-gain") + @ConfigDescription("Prepared statement compression will not be applied if size gain is less than the configured value") + public ProtocolConfig setPreparedStatementCompressionMinimalGain(int preparedStatementCompressionMinimalGain) + { + this.preparedStatementCompressionMinimalGain = preparedStatementCompressionMinimalGain; + return this; + } } diff --git a/core/trino-main/src/main/java/io/trino/server/ServerMainModule.java b/core/trino-main/src/main/java/io/trino/server/ServerMainModule.java index 3b3aa2587a8a..2c731549d52c 100644 --- a/core/trino-main/src/main/java/io/trino/server/ServerMainModule.java +++ b/core/trino-main/src/main/java/io/trino/server/ServerMainModule.java @@ -101,6 +101,7 @@ import io.trino.server.PluginManager.PluginsProvider; import io.trino.server.SliceSerialization.SliceDeserializer; import io.trino.server.SliceSerialization.SliceSerializer; +import io.trino.server.protocol.PreparedStatementEncoder; import io.trino.server.remotetask.HttpLocationFactory; import io.trino.spi.PageIndexerFactory; import io.trino.spi.PageSorter; @@ -207,6 +208,7 @@ protected void setup(Binder binder) httpServerConfig.setAdminEnabled(false); }); + binder.bind(PreparedStatementEncoder.class).in(Scopes.SINGLETON); binder.bind(HttpRequestSessionContextFactory.class).in(Scopes.SINGLETON); install(new InternalCommunicationModule()); diff --git a/core/trino-main/src/main/java/io/trino/server/protocol/ExecutingStatementResource.java b/core/trino-main/src/main/java/io/trino/server/protocol/ExecutingStatementResource.java index 7b9c7da97620..5516106ccfef 100644 --- a/core/trino-main/src/main/java/io/trino/server/protocol/ExecutingStatementResource.java +++ b/core/trino-main/src/main/java/io/trino/server/protocol/ExecutingStatementResource.java @@ -88,6 +88,7 @@ public class ExecutingStatementResource private final ConcurrentMap queries = new ConcurrentHashMap<>(); private final ScheduledExecutorService queryPurger = newSingleThreadScheduledExecutor(threadsNamed("execution-query-purger")); + private final PreparedStatementEncoder preparedStatementEncoder; private final boolean compressionEnabled; @Inject @@ -98,6 +99,7 @@ public ExecutingStatementResource( QueryInfoUrlFactory queryInfoUrlTemplate, @ForStatementResource BoundedExecutor responseExecutor, @ForStatementResource ScheduledExecutorService timeoutExecutor, + PreparedStatementEncoder preparedStatementEncoder, ServerConfig serverConfig) { this.queryManager = requireNonNull(queryManager, "queryManager is null"); @@ -106,6 +108,7 @@ public ExecutingStatementResource( this.queryInfoUrlFactory = requireNonNull(queryInfoUrlTemplate, "queryInfoUrlTemplate is null"); this.responseExecutor = requireNonNull(responseExecutor, "responseExecutor is null"); this.timeoutExecutor = requireNonNull(timeoutExecutor, "timeoutExecutor is null"); + this.preparedStatementEncoder = requireNonNull(preparedStatementEncoder, "preparedStatementEncoder is null"); this.compressionEnabled = requireNonNull(serverConfig, "serverConfig is null").isQueryResultsCompressionEnabled(); queryPurger.scheduleWithFixedDelay( @@ -210,12 +213,12 @@ private void asyncQueryResults( } ListenableFuture queryResultsFuture = query.waitForResults(token, uriInfo, wait, targetResultSize); - ListenableFuture response = Futures.transform(queryResultsFuture, queryResults -> toResponse(query, queryResults, compressionEnabled), directExecutor()); + ListenableFuture response = Futures.transform(queryResultsFuture, queryResults -> toResponse(query, queryResults), directExecutor()); bindAsyncResponse(asyncResponse, response, responseExecutor); } - private static Response toResponse(Query query, QueryResults queryResults, boolean compressionEnabled) + private Response toResponse(Query query, QueryResults queryResults) { ResponseBuilder response = Response.ok(queryResults); @@ -239,7 +242,7 @@ private static Response toResponse(Query query, QueryResults queryResults, boole // add added prepare statements for (Entry entry : query.getAddedPreparedStatements().entrySet()) { String encodedKey = urlEncode(entry.getKey()); - String encodedValue = urlEncode(entry.getValue()); + String encodedValue = urlEncode(preparedStatementEncoder.encodePreparedStatementForHeader(entry.getValue())); response.header(protocolHeaders.responseAddedPrepare(), encodedKey + '=' + encodedValue); } diff --git a/core/trino-main/src/main/java/io/trino/server/protocol/PreparedStatementEncoder.java b/core/trino-main/src/main/java/io/trino/server/protocol/PreparedStatementEncoder.java new file mode 100644 index 000000000000..51bca67905ac --- /dev/null +++ b/core/trino-main/src/main/java/io/trino/server/protocol/PreparedStatementEncoder.java @@ -0,0 +1,74 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.server.protocol; + +import io.airlift.compress.zstd.ZstdCompressor; +import io.airlift.compress.zstd.ZstdDecompressor; +import io.trino.server.ProtocolConfig; + +import javax.inject.Inject; + +import static com.google.common.io.BaseEncoding.base64Url; +import static java.lang.Math.toIntExact; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +public class PreparedStatementEncoder +{ + // No valid SQL statement starts with $ + private static final String PREFIX = "$zstd:"; + + private final int compressionThreshold; + private final int compressionMinGain; + + @Inject + public PreparedStatementEncoder(ProtocolConfig protocolConfig) + { + requireNonNull(protocolConfig, "protocolConfig is null"); + this.compressionThreshold = protocolConfig.getPreparedStatementCompressionThreshold(); + this.compressionMinGain = protocolConfig.getPreparedStatementCompressionMinimalGain(); + } + + public String encodePreparedStatementForHeader(String preparedStatement) + { + if (preparedStatement.length() < compressionThreshold) { + return preparedStatement; + } + + ZstdCompressor compressor = new ZstdCompressor(); + byte[] inputBytes = preparedStatement.getBytes(UTF_8); + byte[] compressed = new byte[compressor.maxCompressedLength(inputBytes.length)]; + int outputSize = compressor.compress(inputBytes, 0, inputBytes.length, compressed, 0, compressed.length); + String encoded = base64Url().encode(compressed, 0, outputSize); + + if (encoded.length() + PREFIX.length() + compressionMinGain > preparedStatement.length()) { + return preparedStatement; + } + return PREFIX + encoded; + } + + public String decodePreparedStatementFromHeader(String headerValue) + { + if (!headerValue.startsWith(PREFIX)) { + return headerValue; + } + + String encoded = headerValue.substring(PREFIX.length()); + byte[] compressed = base64Url().decode(encoded); + + byte[] preparedStatement = new byte[toIntExact(ZstdDecompressor.getDecompressedSize(compressed, 0, compressed.length))]; + new ZstdDecompressor().decompress(compressed, 0, compressed.length, preparedStatement, 0, preparedStatement.length); + return new String(preparedStatement, UTF_8); + } +} diff --git a/core/trino-main/src/test/java/io/trino/server/TestHttpRequestSessionContextFactory.java b/core/trino-main/src/test/java/io/trino/server/TestHttpRequestSessionContextFactory.java index 8ef3810ea860..28caadde3356 100644 --- a/core/trino-main/src/test/java/io/trino/server/TestHttpRequestSessionContextFactory.java +++ b/core/trino-main/src/test/java/io/trino/server/TestHttpRequestSessionContextFactory.java @@ -19,6 +19,7 @@ import io.airlift.jaxrs.testing.GuavaMultivaluedMap; import io.trino.client.ProtocolHeaders; import io.trino.security.AllowAllAccessControl; +import io.trino.server.protocol.PreparedStatementEncoder; import io.trino.spi.security.Identity; import io.trino.spi.security.SelectedRole; import org.testng.annotations.Test; @@ -40,7 +41,11 @@ public class TestHttpRequestSessionContextFactory { - private static final HttpRequestSessionContextFactory SESSION_CONTEXT_FACTORY = new HttpRequestSessionContextFactory(createTestMetadataManager(), ImmutableSet::of, new AllowAllAccessControl()); + private static final HttpRequestSessionContextFactory SESSION_CONTEXT_FACTORY = new HttpRequestSessionContextFactory( + new PreparedStatementEncoder(new ProtocolConfig()), + createTestMetadataManager(), + ImmutableSet::of, + new AllowAllAccessControl()); @Test public void testSessionContext() diff --git a/core/trino-main/src/test/java/io/trino/server/TestProtocolConfig.java b/core/trino-main/src/test/java/io/trino/server/TestProtocolConfig.java index 498271861a46..c99ef4dcc7e6 100644 --- a/core/trino-main/src/test/java/io/trino/server/TestProtocolConfig.java +++ b/core/trino-main/src/test/java/io/trino/server/TestProtocolConfig.java @@ -28,7 +28,9 @@ public class TestProtocolConfig public void testDefaults() { assertRecordedDefaults(recordDefaults(ProtocolConfig.class) - .setAlternateHeaderName(null)); + .setAlternateHeaderName(null) + .setPreparedStatementCompressionThreshold(2 * 1024) + .setPreparedStatementCompressionMinimalGain(512)); } @Test @@ -36,10 +38,14 @@ public void testExplicitPropertyMappings() { Map properties = new ImmutableMap.Builder() .put("protocol.v1.alternate-header-name", "taco") + .put("protocol.v1.prepared-statement-compression.length-threshold", "412") + .put("protocol.v1.prepared-statement-compression.min-gain", "0") .buildOrThrow(); ProtocolConfig expected = new ProtocolConfig() - .setAlternateHeaderName("taco"); + .setAlternateHeaderName("taco") + .setPreparedStatementCompressionThreshold(412) + .setPreparedStatementCompressionMinimalGain(0); assertFullMapping(properties, expected); } diff --git a/core/trino-main/src/test/java/io/trino/server/TestQuerySessionSupplier.java b/core/trino-main/src/test/java/io/trino/server/TestQuerySessionSupplier.java index 9d952b7116c9..df8b60df0842 100644 --- a/core/trino-main/src/test/java/io/trino/server/TestQuerySessionSupplier.java +++ b/core/trino-main/src/test/java/io/trino/server/TestQuerySessionSupplier.java @@ -25,6 +25,7 @@ import io.trino.metadata.Metadata; import io.trino.metadata.SessionPropertyManager; import io.trino.security.AllowAllAccessControl; +import io.trino.server.protocol.PreparedStatementEncoder; import io.trino.spi.QueryId; import io.trino.spi.TrinoException; import io.trino.sql.SqlEnvironmentConfig; @@ -67,7 +68,11 @@ public class TestQuerySessionSupplier .put(TRINO_HEADERS.requestSession(), JOIN_DISTRIBUTION_TYPE + "=partitioned," + HASH_PARTITION_COUNT + " = 43") .put(TRINO_HEADERS.requestPreparedStatement(), "query1=select * from foo,query2=select * from bar") .build()); - private static final HttpRequestSessionContextFactory SESSION_CONTEXT_FACTORY = new HttpRequestSessionContextFactory(createTestMetadataManager(), ImmutableSet::of, new AllowAllAccessControl()); + private static final HttpRequestSessionContextFactory SESSION_CONTEXT_FACTORY = new HttpRequestSessionContextFactory( + new PreparedStatementEncoder(new ProtocolConfig()), + createTestMetadataManager(), + ImmutableSet::of, + new AllowAllAccessControl()); @Test public void testCreateSession() diff --git a/core/trino-main/src/test/java/io/trino/server/security/TestResourceSecurity.java b/core/trino-main/src/test/java/io/trino/server/security/TestResourceSecurity.java index fde1f1d95180..3f0d190d9c60 100644 --- a/core/trino-main/src/test/java/io/trino/server/security/TestResourceSecurity.java +++ b/core/trino-main/src/test/java/io/trino/server/security/TestResourceSecurity.java @@ -32,6 +32,8 @@ import io.trino.security.AccessControl; import io.trino.security.AccessControlManager; import io.trino.server.HttpRequestSessionContextFactory; +import io.trino.server.ProtocolConfig; +import io.trino.server.protocol.PreparedStatementEncoder; import io.trino.server.security.oauth2.OAuth2Client; import io.trino.server.testing.TestingTrinoServer; import io.trino.spi.security.AccessDeniedException; @@ -949,7 +951,11 @@ public static class TestResource @Inject public TestResource(AccessControl accessControl) { - this.sessionContextFactory = new HttpRequestSessionContextFactory(createTestMetadataManager(), user -> ImmutableSet.of(), accessControl); + this.sessionContextFactory = new HttpRequestSessionContextFactory( + new PreparedStatementEncoder(new ProtocolConfig()), + createTestMetadataManager(), + user -> ImmutableSet.of(), + accessControl); } @ResourceSecurity(AUTHENTICATED_USER) diff --git a/core/trino-main/src/test/java/io/trino/server/ui/TestWebUi.java b/core/trino-main/src/test/java/io/trino/server/ui/TestWebUi.java index 1c09789804fb..e0949a6c29c2 100644 --- a/core/trino-main/src/test/java/io/trino/server/ui/TestWebUi.java +++ b/core/trino-main/src/test/java/io/trino/server/ui/TestWebUi.java @@ -27,6 +27,8 @@ import io.jsonwebtoken.JwtBuilder; import io.trino.security.AccessControl; import io.trino.server.HttpRequestSessionContextFactory; +import io.trino.server.ProtocolConfig; +import io.trino.server.protocol.PreparedStatementEncoder; import io.trino.server.security.PasswordAuthenticatorManager; import io.trino.server.security.ResourceSecurity; import io.trino.server.security.oauth2.OAuth2Client; @@ -392,7 +394,11 @@ public static class TestResource @Inject public TestResource(AccessControl accessControl) { - this.sessionContextFactory = new HttpRequestSessionContextFactory(createTestMetadataManager(), ImmutableSet::of, accessControl); + this.sessionContextFactory = new HttpRequestSessionContextFactory( + new PreparedStatementEncoder(new ProtocolConfig()), + createTestMetadataManager(), + ImmutableSet::of, + accessControl); } @ResourceSecurity(WEB_UI)