diff --git a/presto-clp/pom.xml b/presto-clp/pom.xml index d13b59968aff..7a654371416e 100644 --- a/presto-clp/pom.xml +++ b/presto-clp/pom.xml @@ -105,6 +105,12 @@ test + + com.facebook.presto + presto-main + test + + com.facebook.presto presto-main-base diff --git a/presto-clp/src/test/java/com/facebook/presto/plugin/clp/ClpQueryRunner.java b/presto-clp/src/test/java/com/facebook/presto/plugin/clp/ClpQueryRunner.java index dc5b03e4111f..88910857a5f1 100644 --- a/presto-clp/src/test/java/com/facebook/presto/plugin/clp/ClpQueryRunner.java +++ b/presto-clp/src/test/java/com/facebook/presto/plugin/clp/ClpQueryRunner.java @@ -18,12 +18,21 @@ import com.facebook.presto.tests.DistributedQueryRunner; import com.google.common.collect.ImmutableMap; +import java.io.File; +import java.io.FileOutputStream; import java.net.URI; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.function.BiFunction; +import static com.facebook.presto.plugin.clp.ClpConfig.SplitFilterProviderType; +import static com.facebook.presto.plugin.clp.ClpConfig.SplitProviderType; import static com.facebook.presto.testing.TestingSession.testSessionBuilder; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; public class ClpQueryRunner { @@ -43,7 +52,6 @@ public static DistributedQueryRunner createQueryRunner( Optional> externalWorkerLauncher) throws Exception { - log.info("Creating CLP query runner with default session"); return createQueryRunner( createDefaultSession(), metadataDbUrl, @@ -54,6 +62,29 @@ public static DistributedQueryRunner createQueryRunner( externalWorkerLauncher); } + public static DistributedQueryRunner createQueryRunner( + String metadataDbUrl, + String metadataDbUser, + String metadataDbPassword, + String metadataDbTablePrefix, + Optional splitFilterProvider, + Optional splitFilterConfigPath, + Optional workerCount, + Optional> externalWorkerLauncher) + throws Exception + { + return createQueryRunner( + createDefaultSession(), + metadataDbUrl, + metadataDbUser, + metadataDbPassword, + metadataDbTablePrefix, + splitFilterProvider, + splitFilterConfigPath, + workerCount, + externalWorkerLauncher); + } + public static DistributedQueryRunner createQueryRunner( Session session, String metadataDbUrl, @@ -63,25 +94,98 @@ public static DistributedQueryRunner createQueryRunner( Optional workerCount, Optional> externalWorkerLauncher) throws Exception + { + return createQueryRunner( + session, + metadataDbUrl, + metadataDbUser, + metadataDbPassword, + metadataDbTablePrefix, + Optional.empty(), + Optional.empty(), + workerCount, + externalWorkerLauncher); + } + + public static DistributedQueryRunner createQueryRunner( + Session session, + String metadataDbUrl, + String metadataDbUser, + String metadataDbPassword, + String metadataDbTablePrefix, + Optional splitFilterProvider, + Optional splitFilterConfigPath, + Optional workerCount, + Optional> externalWorkerLauncher) + throws Exception { DistributedQueryRunner clpQueryRunner = DistributedQueryRunner.builder(session) - .setNodeCount(workerCount.orElse(DEFAULT_NUM_OF_WORKERS)) - .setExternalWorkerLauncher(externalWorkerLauncher) - .build(); - Map clpProperties = ImmutableMap.builder() + .setNodeCount(workerCount.orElse(DEFAULT_NUM_OF_WORKERS)) + .setExternalWorkerLauncher(externalWorkerLauncher) + .build(); + ImmutableMap.Builder clpProperties = ImmutableMap.builder() .put("clp.metadata-provider-type", "mysql") .put("clp.metadata-db-url", metadataDbUrl) .put("clp.metadata-db-user", metadataDbUser) .put("clp.metadata-db-password", metadataDbPassword) .put("clp.metadata-table-prefix", metadataDbTablePrefix) - .put("clp.split-provider-type", "mysql") - .build(); + .put("clp.split-provider-type", SplitProviderType.MYSQL.name()) + .put("clp.split-filter-provider-type", splitFilterProvider.orElse(SplitFilterProviderType.MYSQL.name())); + splitFilterConfigPath.ifPresent(s -> clpProperties.put("clp.split-filter-config", s)); + Map clpPropertiesMap = clpProperties.build(); + log.info("Creating query runner with clp properties:"); + for (Map.Entry entry : clpPropertiesMap.entrySet()) { + log.info("\t%s=%s", entry.getKey(), entry.getValue()); + } clpQueryRunner.installPlugin(new ClpPlugin()); - clpQueryRunner.createCatalog(CLP_CATALOG, CLP_CONNECTOR, clpProperties); + clpQueryRunner.createCatalog(CLP_CATALOG, CLP_CONNECTOR, clpPropertiesMap); return clpQueryRunner; } + /** + * Create a config file with the given literal string of the configuration. + * + * @param configuration the given literal string of the configuration, which will be written into the config file + * @return the string of the config file path + */ + public static String createConfigFile(String configuration) + { + UUID uuid = UUID.randomUUID(); + File file = new File(System.getProperty("java.io.tmpdir"), format("config-%s", uuid)); + try { + boolean fileCreated = file.createNewFile(); + assertTrue(fileCreated); + } + catch (Exception e) { + fail(e.getMessage()); + } + + if (!file.exists() || !file.canWrite()) { + fail("Cannot create split filter config file"); + } + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(configuration.getBytes(UTF_8)); + } + catch (Exception e) { + fail(e.getMessage()); + } + return file.getAbsolutePath(); + } + + /** + * Delete a config file by the given path. + * + * @param configFilePath the path of the config file + */ + public static void deleteConfigFile(String configFilePath) + { + File splitFilterConfigFile = new File(configFilePath); + if (splitFilterConfigFile.exists()) { + assertTrue(splitFilterConfigFile.delete()); + } + } + /** * Creates a default mock session for query use. * diff --git a/presto-clp/src/test/java/com/facebook/presto/plugin/clp/TestClpComputePushDown.java b/presto-clp/src/test/java/com/facebook/presto/plugin/clp/TestClpComputePushDown.java new file mode 100644 index 000000000000..48e77d94a962 --- /dev/null +++ b/presto-clp/src/test/java/com/facebook/presto/plugin/clp/TestClpComputePushDown.java @@ -0,0 +1,500 @@ +/* + * 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 com.facebook.presto.plugin.clp; + +import com.facebook.presto.dispatcher.DispatchManager; +import com.facebook.presto.execution.SqlQueryManager; +import com.facebook.presto.plugin.clp.mockdb.ClpMockMetadataDatabase; +import com.facebook.presto.plugin.clp.mockdb.table.ColumnMetadataTableRows; +import com.facebook.presto.spi.ConnectorTableLayoutHandle; +import com.facebook.presto.spi.QueryId; +import com.facebook.presto.spi.plan.FilterNode; +import com.facebook.presto.spi.plan.PlanNode; +import com.facebook.presto.spi.plan.PlanNodeId; +import com.facebook.presto.spi.plan.TableScanNode; +import com.facebook.presto.spi.relation.RowExpression; +import com.facebook.presto.sql.planner.Plan; +import com.facebook.presto.tests.DistributedQueryRunner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.intellij.lang.annotations.Language; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.regex.Pattern; + +import static com.facebook.presto.execution.QueryState.RUNNING; +import static com.facebook.presto.plugin.clp.ClpQueryRunner.createConfigFile; +import static com.facebook.presto.plugin.clp.ClpQueryRunner.createQueryRunner; +import static com.facebook.presto.plugin.clp.ClpQueryRunner.deleteConfigFile; +import static com.facebook.presto.plugin.clp.metadata.ClpSchemaTreeNodeType.Boolean; +import static com.facebook.presto.plugin.clp.metadata.ClpSchemaTreeNodeType.DateString; +import static com.facebook.presto.plugin.clp.metadata.ClpSchemaTreeNodeType.Float; +import static com.facebook.presto.plugin.clp.metadata.ClpSchemaTreeNodeType.Integer; +import static com.facebook.presto.plugin.clp.metadata.ClpSchemaTreeNodeType.VarString; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +@Test(singleThreaded = true) +public class TestClpComputePushDown +{ + private static final String TABLE_NAME = "test_pushdown"; + private static final Pattern BASE_PTR = + Pattern.compile("(?<=base=)\\[B@[0-9a-fA-F]+"); + + private ClpMockMetadataDatabase mockMetadataDatabase; + private DistributedQueryRunner queryRunner; + private SqlQueryManager queryManager; + private DispatchManager dispatchManager; + private String splitFilterConfigFilePath; + + @BeforeClass + public void setUp() + throws Exception + { + // Set up metadata database + mockMetadataDatabase = ClpMockMetadataDatabase + .builder() + .build(); + mockMetadataDatabase.addTableToDatasetsTableIfNotExist(ImmutableList.of(TABLE_NAME)); + mockMetadataDatabase.addColumnMetadata(ImmutableMap.of(TABLE_NAME, new ColumnMetadataTableRows( + ImmutableList.of( + "city.Name", + "city.Region.id", + "city.Region.population_lowerbound", + "city.Region.population_upperbound", + "city.Region.Name", + "fare", + "isHoliday", + "ts"), + ImmutableList.of( + VarString, + Integer, + Float, + Float, + VarString, + Float, + Boolean, + DateString)))); + // Set up split filter config + String splitFilterConfigJsonString = "{\n" + + " \"clp\": [\n" + + " {\n" + + " \"columnName\": \"fare\",\n" + + " \"customOptions\": {\n" + + " \"rangeMapping\": {\n" + + " \"lowerBound\": \"fare_lb\",\n" + + " \"upperBound\": \"fare_ub\"\n" + + " }\n" + + " },\n" + + " \"required\": false\n" + + " }\n" + + " ]\n" + + "}"; + splitFilterConfigFilePath = createConfigFile(splitFilterConfigJsonString); + queryRunner = createQueryRunner( + mockMetadataDatabase.getUrl(), + mockMetadataDatabase.getUsername(), + mockMetadataDatabase.getPassword(), + mockMetadataDatabase.getTablePrefix(), + Optional.empty(), + Optional.of(splitFilterConfigFilePath), + Optional.of(0), + Optional.empty()); + queryManager = (SqlQueryManager) queryRunner.getCoordinator().getQueryManager(); + requireNonNull(queryManager, "queryManager is null"); + dispatchManager = queryRunner.getCoordinator().getDispatchManager(); + } + + @AfterClass + public void tearDown() + throws InterruptedException + { + queryRunner.cancelAllQueries(); + long maxCleanUpTime = 5 * 1000L; // 5 seconds + long currentCleanUpTime = 0L; + while (!queryManager.getQueries().isEmpty() && currentCleanUpTime < maxCleanUpTime) { + Thread.sleep(1000L); + currentCleanUpTime += 1000L; + } + if (null != mockMetadataDatabase) { + mockMetadataDatabase.teardown(); + } + deleteConfigFile(splitFilterConfigFilePath); + } + + @Test + public void testStringMatchPushDown() + { + // Exact match + testPushDown("city.Name = 'hello world'", "city.Name: \"hello world\"", null); + testPushDown("'hello world' = city.Name", "city.Name: \"hello world\"", null); + + // Like predicates that are transformed into substring match + testPushDown("city.Name like 'hello%'", "city.Name: \"hello*\"", null); + testPushDown("city.Name like '%hello'", "city.Name: \"*hello\"", null); + + // Like predicate not pushed down + testPushDown("city.Name like '%hello%'", null, "city.Name like '%hello%'"); + + // Like predicates that are kept in the original forms + testPushDown("city.Name like 'hello_'", "city.Name: \"hello?\"", null); + testPushDown("city.Name like '_hello'", "city.Name: \"?hello\"", null); + testPushDown("city.Name like 'hello_w%'", "city.Name: \"hello?w*\"", null); + testPushDown("city.Name like '%hello_w'", "city.Name: \"*hello?w\"", null); + testPushDown("city.Name like 'hello%world'", "city.Name: \"hello*world\"", null); + testPushDown("city.Name like 'hello%wor%ld'", "city.Name: \"hello*wor*ld\"", null); + } + + @Test + public void testSubStringPushDown() + { + testPushDown("substr(city.Name, 1, 2) = 'he'", "city.Name: \"he*\"", null); + testPushDown("substr(city.Name, 5, 2) = 'he'", "city.Name: \"????he*\"", null); + testPushDown("substr(city.Name, 5) = 'he'", "city.Name: \"????he\"", null); + testPushDown("substr(city.Name, -2) = 'he'", "city.Name: \"*he\"", null); + + // Invalid substring index — not pushed down + testPushDown("substr(city.Name, 1, 5) = 'he'", null, "substr(city.Name, 1, 5) = 'he'"); + testPushDown("substr(city.Name, -5) = 'he'", null, "substr(city.Name, -5) = 'he'"); + } + + @Test + public void testNumericComparisonPushDown() + { + // Numeric comparisons + testPushDown("fare > 0", "fare > 0.0", null); + testPushDown("fare >= 0", "fare >= 0.0", null); + testPushDown("fare < 0", "fare < 0.0", null); + testPushDown("fare <= 0", "fare <= 0.0", null); + testPushDown("fare = 0", "fare: 0.0", null); + testPushDown("fare != 0", "NOT fare: 0.0", null); + testPushDown("fare <> 0", "NOT fare: 0.0", null); + testPushDown("0 < fare", "fare > 0.0", null); + testPushDown("0 <= fare", "fare >= 0.0", null); + testPushDown("0 > fare", "fare < 0.0", null); + testPushDown("0 >= fare", "fare <= 0.0", null); + testPushDown("0 = fare", "fare: 0.0", null); + testPushDown("0 != fare", "NOT fare: 0.0", null); + testPushDown("0 <> fare", "NOT fare: 0.0", null); + } + + @Test + public void testBetweenPushDown() + { + // Normal cases + testPushDown("fare BETWEEN 0 AND 5", "fare >= 0.0 AND fare <= 5.0", null); + testPushDown("fare BETWEEN 5 AND 0", null, null); + + // No push down for non-constant expressions + testPushDown( + "fare BETWEEN city.Region.population_lowerbound AND city.Region.population_upperbound", + null, + "fare >= city.Region.population_lowerbound AND fare <= city.Region.population_upperbound"); + + // If the last two arguments of BETWEEN are not numeric constants, then the CLP connector + // won't push them down. + testPushDown("city.Name BETWEEN 'a' AND 'b'", null, "city.Name >= 'a' AND city.Name <= 'b'"); + } + + @Test + public void testOrPushDown() + { + // OR conditions with partial push down support + testPushDown("fare > 0 OR city.Name like 'b%'", "(fare > 0.0 OR city.Name: \"b*\")", null); + testPushDown( + "lower(city.Region.Name) = 'hello world' OR city.Region.id != 1", + null, + "(lower(city.Region.Name) = 'hello world' OR city.Region.id != 1)"); + + // Multiple ORs + testPushDown( + "fare > 0 OR city.Name like 'b%' OR lower(city.Region.Name) = 'hello world' OR city.Region.id != 1", + null, + "(fare > 0 OR city.Name like 'b%') OR (lower(city.Region.Name) = 'hello world' OR city.Region.id != 1)"); + testPushDown( + "fare > 0 OR city.Name like 'b%' OR city.Region.id != 1", + "((fare > 0.0 OR city.Name: \"b*\") OR NOT city.Region.id: 1)", + null); + } + + @Test + public void testAndPushDown() + { + // AND conditions with partial/full push down + testPushDown("fare > 0 AND city.Name like 'b%'", "(fare > 0.0 AND city.Name: \"b*\")", null); + + testPushDown("lower(city.Region.Name) = 'hello world' AND city.Region.id != 1", "(NOT city.Region.id: 1)", "lower(city.Region.Name) = 'hello world'"); + + // Multiple ANDs + testPushDown( + "fare > 0 AND city.Name like 'b%' AND lower(city.Region.Name) = 'hello world' AND city.Region.id != 1", + "((fare > 0.0 AND city.Name: \"b*\") AND (NOT city.Region.id: 1))", + "(lower(city.Region.Name) = 'hello world')"); + + testPushDown( + "fare > 0 AND city.Name like '%b%' AND lower(city.Region.Name) = 'hello world' AND city.Region.id != 1", + "((fare > 0.0) AND (NOT city.Region.id: 1))", + "city.Name like '%b%' AND lower(city.Region.Name) = 'hello world'"); + } + + @Test + public void testNotPushDown() + { + // NOT and inequality predicates + testPushDown("city.Region.Name NOT LIKE 'hello%'", "NOT city.Region.Name: \"hello*\"", null); + testPushDown("NOT (city.Region.Name LIKE 'hello%')", "NOT city.Region.Name: \"hello*\"", null); + testPushDown("city.Name != 'hello world'", "NOT city.Name: \"hello world\"", null); + testPushDown("city.Name <> 'hello world'", "NOT city.Name: \"hello world\"", null); + testPushDown("NOT (city.Name = 'hello world')", "NOT city.Name: \"hello world\"", null); + testPushDown("fare != 0", "NOT fare: 0.0", null); + testPushDown("fare <> 0", "NOT fare: 0.0", null); + testPushDown("NOT (fare = 0)", "NOT fare: 0.0", null); + + // Multiple NOTs + testPushDown("NOT (NOT fare = 0)", "fare: 0.0", null); + testPushDown("NOT (fare = 0 AND city.Name = 'hello world')", "(NOT fare: 0.0 OR NOT city.Name: \"hello world\")", null); + testPushDown("NOT (fare = 0 OR city.Name = 'hello world')", "(NOT fare: 0.0 AND NOT city.Name: \"hello world\")", null); + } + + @Test + public void testInPushDown() + { + // IN predicate + testPushDown("city.Name IN ('hello world', 'hello world 2')", "(city.Name: \"hello world\" OR city.Name: \"hello world 2\")", null); + } + + @Test + public void testIsNullPushDown() + { + // IS NULL / IS NOT NULL predicates + testPushDown("city.Name IS NULL", "NOT city.Name: *", null); + testPushDown("city.Name IS NOT NULL", "NOT NOT city.Name: *", null); + testPushDown("NOT (city.Name IS NULL)", "NOT NOT city.Name: *", null); + } + + @Test + public void testComplexPushDown() + { + // Complex AND/OR with partial pushdown + testPushDown( + "(fare > 0 OR city.Name like 'b%') AND (lower(city.Region.Name) = 'hello world' OR city.Name IS NULL)", + "((fare > 0.0 OR city.Name: \"b*\"))", + "(lower(city.Region.Name) = 'hello world' OR city.Name IS NULL)"); + + testPushDown( + "city.Region.id = 1 AND (fare > 0 OR city.Name NOT like 'b%') AND (lower(city.Region.Name) = 'hello world' OR city.Name IS NULL)", + "((city.Region.id: 1 AND (fare > 0.0 OR NOT city.Name: \"b*\")))", + "lower(city.Region.Name) = 'hello world' OR city.Name IS NULL"); + } + + @Test + public void testSplitFilterPushDown() + { + // Normal case + testPushDown( + "(fare > 0 AND city.Name like 'b%')", + "(fare > 0.0 AND city.Name: \"b*\")", + null, + "(fare_ub > 0.0)"); + + // With BETWEEN + testPushDown( + "((fare BETWEEN 0 AND 5) AND city.Name like 'b%')", + "(fare >= 0.0 AND fare <= 5.0 AND city.Name: \"b*\")", + null, + "(fare_ub >= 0.0 AND fare_lb <= 5.0)"); + + // The cases of that the metadata filter column exist but cannot be push down + testPushDown( + "(fare > 0 OR city.Name like 'b%')", + "(fare > 0.0 OR city.Name: \"b*\")", + null, + null); + testPushDown( + "(fare > 0 AND city.Name like 'b%') OR city.Region.Id = 1", + "((city.Region.id: 1 OR fare > 0.0) AND (city.Region.id: 1 OR city.Name: \"b*\"))", + null, + null); + + // Complicated case + testPushDown( + "fare = 0 AND (city.Name like 'b%' OR city.Region.Id = 1)", + "(fare: 0.0 AND (city.Name: \"b*\" OR city.Region.id: 1))", + null, + "((fare_lb <= 0.0 AND fare_ub >= 0.0))"); + } + + @Test + public void testClpWildcardUdf() + { + testPushDown("CLP_WILDCARD_STRING_COLUMN() = 'Beijing'", "*: \"Beijing\"", null); + testPushDown("CLP_WILDCARD_INT_COLUMN() = 1", "*: 1", null); + testPushDown("CLP_WILDCARD_FLOAT_COLUMN() > 0", "* > 0.0", null); + testPushDown("CLP_WILDCARD_BOOL_COLUMN() = true", "*: true", null); + + testPushDown("CLP_WILDCARD_STRING_COLUMN() like 'hello%'", "*: \"hello*\"", null); + testPushDown("substr(CLP_WILDCARD_STRING_COLUMN(), 1, 2) = 'he'", "*: \"he*\"", null); + testPushDown("CLP_WILDCARD_INT_COLUMN() BETWEEN 0 AND 5", "* >= 0 AND * <= 5", null); + testPushDown("CLP_WILDCARD_STRING_COLUMN() IN ('hello world', 'hello world 2')", "(*: \"hello world\" OR *: \"hello world 2\")", null); + + testPushDown("NOT CLP_WILDCARD_FLOAT_COLUMN() > 0", "* <= 0.0", null); + testPushDown( + "CLP_WILDCARD_STRING_COLUMN() = 'Beijing' AND CLP_WILDCARD_INT_COLUMN() = 1 AND city.Region.id = 1", + "((*: \"Beijing\" AND *: 1) AND city.Region.id: 1)", + null); + testPushDown( + "CLP_WILDCARD_STRING_COLUMN() = 'Toronto' OR CLP_WILDCARD_INT_COLUMN() = 2", + "(*: \"Toronto\" OR *: 2)", + null); + testPushDown( + "CLP_WILDCARD_STRING_COLUMN() = 'Shanghai' AND (CLP_WILDCARD_INT_COLUMN() = 3 OR city.Region.id = 5)", + "(*: \"Shanghai\" AND (*: 3 OR city.Region.id: 5))", + null); + } + + private void testPushDown(String filter, String expectedPushDown, String expectedRemaining) + { + testPushDown(filter, expectedPushDown, expectedRemaining, true, null); + } + + private void testPushDown(String filter, String expectedPushDown, String expectedRemaining, String expectedSplitFilterPushDown) + { + testPushDown(filter, expectedPushDown, expectedRemaining, false, expectedSplitFilterPushDown); + } + + private void testPushDown(String filter, String expectedPushDown, String expectedRemaining, boolean ignoreSplitFilterPushDown, String expectedSplitFilterPushDown) + { + QueryId originalQueryId = null; + QueryId remainingFilterQueryId = null; + try { + // We first execute a query using the original filter and look for the FilterNode (for remaining expression) + // and TableScanNode (for KQL pushdown and split filter pushdown) + originalQueryId = createAndPlanQuery(filter); + String actualPushDown = null; + String actualSplitFilterPushDown = null; + RowExpression actualRemainingExpression = null; + Plan originalQueryPlan = queryManager.getQueryPlan(originalQueryId); + for (Map.Entry entry : originalQueryPlan.getPlanIdNodeMap().entrySet()) { + ClpTableLayoutHandle clpTableLayoutHandle = tryGetClpTableLayoutHandleFromFilterNode(entry.getValue()); + if (clpTableLayoutHandle != null && actualRemainingExpression == null) { + actualRemainingExpression = ((FilterNode) entry.getValue()).getPredicate(); + continue; + } + clpTableLayoutHandle = tryGetClpTableLayoutHandleFromTableScanNode(entry.getValue()); + if (clpTableLayoutHandle != null && actualPushDown == null) { + actualPushDown = clpTableLayoutHandle.getKqlQuery().orElse(null); + actualSplitFilterPushDown = clpTableLayoutHandle.getMetadataSql().orElse(null); + } + } + assertEquals(actualPushDown, expectedPushDown); + if (!ignoreSplitFilterPushDown) { + assertEquals(actualSplitFilterPushDown, expectedSplitFilterPushDown); + } + if (expectedRemaining != null) { + assertNotNull(actualRemainingExpression); + // Since the remaining expression cannot be simply compared by given String, we have to first convert + // the expectedRemaining to a RowExpression so that we can compare with what we just fetched from the + // plan of the original query. To ensure the translation process is the same, we create another query + // which only contain the remaining expression as the filter, then we look for the FilterNode and get + // the predict as the translated RowExpression of the expectedRemaining. + remainingFilterQueryId = createAndPlanQuery(expectedRemaining); + Plan remainingFilterPlan = queryManager.getQueryPlan(remainingFilterQueryId); + RowExpression expectedRemainingExpression = null; + for (Map.Entry entry : remainingFilterPlan.getPlanIdNodeMap().entrySet()) { + if (tryGetClpTableLayoutHandleFromFilterNode(entry.getValue()) != null) { + expectedRemainingExpression = ((FilterNode) entry.getValue()).getPredicate(); + break; + } + } + equalsIgnoreBase(actualRemainingExpression, expectedRemainingExpression); + } + else { + assertNull(actualRemainingExpression); + } + } + catch (Exception e) { + fail(e.getMessage()); + } + finally { + if (originalQueryId != null) { + queryManager.cancelQuery(originalQueryId); + } + if (remainingFilterQueryId != null) { + queryManager.cancelQuery(remainingFilterQueryId); + } + } + } + + private QueryId createAndPlanQuery(String filter) + throws ExecutionException, InterruptedException + { + QueryId id = queryRunner.getCoordinator().getDispatchManager().createQueryId(); + @Language("SQL") String sql = format("SELECT * FROM clp.default.test_pushdown WHERE %s LIMIT 1", filter); + dispatchManager.createQuery( + id, + "slug", + 0, + new TestingClpSessionContext(queryRunner.getDefaultSession()), + sql).get(); + long maxDispatchingAndPlanningTime = 60 * 1000L; // 1 minute + long currentWaitingTime = 0L; + while (dispatchManager.getQueryInfo(id).getState().ordinal() != RUNNING.ordinal() && currentWaitingTime < maxDispatchingAndPlanningTime) { + Thread.sleep(1000L); + currentWaitingTime += 1000L; + } + assertTrue(currentWaitingTime < maxDispatchingAndPlanningTime); + return id; + } + + private void equalsIgnoreBase(RowExpression actualExpression, RowExpression expectedExpression) + { + if (actualExpression == null) { + assertNull(expectedExpression); + return; + } + String normalizedActualExpressionText = BASE_PTR.matcher(actualExpression.toString()).replaceAll("[B@IGNORED"); + String normalizedExpectedExpressionText = BASE_PTR.matcher(expectedExpression.toString()).replaceAll("[B@IGNORED"); + assertEquals(normalizedActualExpressionText, normalizedExpectedExpressionText); + } + + private ClpTableLayoutHandle tryGetClpTableLayoutHandleFromFilterNode(PlanNode node) + { + if (!(node instanceof FilterNode)) { + return null; + } + return (tryGetClpTableLayoutHandleFromTableScanNode(((FilterNode) node).getSource())); + } + + private ClpTableLayoutHandle tryGetClpTableLayoutHandleFromTableScanNode(PlanNode node) + { + if (!(node instanceof TableScanNode)) { + return null; + } + ConnectorTableLayoutHandle tableLayoutHandle = ((TableScanNode) node).getTable().getLayout().orElse(null); + if (!(tableLayoutHandle instanceof ClpTableLayoutHandle)) { + return null; + } + return (ClpTableLayoutHandle) tableLayoutHandle; + } +} diff --git a/presto-clp/src/test/java/com/facebook/presto/plugin/clp/TestClpFilterToKql.java b/presto-clp/src/test/java/com/facebook/presto/plugin/clp/TestClpFilterToKql.java deleted file mode 100644 index 413c9cd773a6..000000000000 --- a/presto-clp/src/test/java/com/facebook/presto/plugin/clp/TestClpFilterToKql.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * 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 com.facebook.presto.plugin.clp; - -import com.facebook.presto.plugin.clp.optimization.ClpFilterToKqlConverter; -import com.facebook.presto.spi.ColumnHandle; -import com.facebook.presto.spi.relation.RowExpression; -import com.facebook.presto.spi.relation.VariableReferenceExpression; -import com.google.common.collect.ImmutableSet; -import org.testng.annotations.Test; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; - -@Test -public class TestClpFilterToKql - extends TestClpQueryBase -{ - @Test - public void testStringMatchPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - // Exact match - testPushDown(sessionHolder, "city.Name = 'hello world'", "city.Name: \"hello world\"", null); - testPushDown(sessionHolder, "'hello world' = city.Name", "city.Name: \"hello world\"", null); - - // Like predicates that are transformed into substring match - testPushDown(sessionHolder, "city.Name like 'hello%'", "city.Name: \"hello*\"", null); - testPushDown(sessionHolder, "city.Name like '%hello'", "city.Name: \"*hello\"", null); - - // Like predicate not pushed down - testPushDown(sessionHolder, "city.Name like '%hello%'", null, "city.Name like '%hello%'"); - - // Like predicates that are kept in the original forms - testPushDown(sessionHolder, "city.Name like 'hello_'", "city.Name: \"hello?\"", null); - testPushDown(sessionHolder, "city.Name like '_hello'", "city.Name: \"?hello\"", null); - testPushDown(sessionHolder, "city.Name like 'hello_w%'", "city.Name: \"hello?w*\"", null); - testPushDown(sessionHolder, "city.Name like '%hello_w'", "city.Name: \"*hello?w\"", null); - testPushDown(sessionHolder, "city.Name like 'hello%world'", "city.Name: \"hello*world\"", null); - testPushDown(sessionHolder, "city.Name like 'hello%wor%ld'", "city.Name: \"hello*wor*ld\"", null); - } - - @Test - public void testSubStringPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - testPushDown(sessionHolder, "substr(city.Name, 1, 2) = 'he'", "city.Name: \"he*\"", null); - testPushDown(sessionHolder, "substr(city.Name, 5, 2) = 'he'", "city.Name: \"????he*\"", null); - testPushDown(sessionHolder, "substr(city.Name, 5) = 'he'", "city.Name: \"????he\"", null); - testPushDown(sessionHolder, "substr(city.Name, -2) = 'he'", "city.Name: \"*he\"", null); - - // Invalid substring index — not pushed down - testPushDown(sessionHolder, "substr(city.Name, 1, 5) = 'he'", null, "substr(city.Name, 1, 5) = 'he'"); - testPushDown(sessionHolder, "substr(city.Name, -5) = 'he'", null, "substr(city.Name, -5) = 'he'"); - } - - @Test - public void testNumericComparisonPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - // Numeric comparisons - testPushDown(sessionHolder, "fare > 0", "fare > 0", null); - testPushDown(sessionHolder, "fare >= 0", "fare >= 0", null); - testPushDown(sessionHolder, "fare < 0", "fare < 0", null); - testPushDown(sessionHolder, "fare <= 0", "fare <= 0", null); - testPushDown(sessionHolder, "fare = 0", "fare: 0", null); - testPushDown(sessionHolder, "fare != 0", "NOT fare: 0", null); - testPushDown(sessionHolder, "fare <> 0", "NOT fare: 0", null); - testPushDown(sessionHolder, "0 < fare", "fare > 0", null); - testPushDown(sessionHolder, "0 <= fare", "fare >= 0", null); - testPushDown(sessionHolder, "0 > fare", "fare < 0", null); - testPushDown(sessionHolder, "0 >= fare", "fare <= 0", null); - testPushDown(sessionHolder, "0 = fare", "fare: 0", null); - testPushDown(sessionHolder, "0 != fare", "NOT fare: 0", null); - testPushDown(sessionHolder, "0 <> fare", "NOT fare: 0", null); - } - - @Test - public void testBetweenPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - // Normal cases - testPushDown(sessionHolder, "fare BETWEEN 0 AND 5", "fare >= 0 AND fare <= 5", null); - testPushDown(sessionHolder, "fare BETWEEN 5 AND 0", "fare >= 5 AND fare <= 0", null); - - // No push down for non-constant expressions - testPushDown( - sessionHolder, - "fare BETWEEN (city.Region.Id - 5) AND (city.Region.Id + 5)", - null, - "fare BETWEEN (city.Region.Id - 5) AND (city.Region.Id + 5)"); - - // If the last two arguments of BETWEEN are not numeric constants, then the CLP connector - // won't push them down. - testPushDown(sessionHolder, "city.Name BETWEEN 'a' AND 'b'", null, "city.Name BETWEEN 'a' AND 'b'"); - } - - @Test - public void testOrPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - // OR conditions with partial push down support - testPushDown(sessionHolder, "fare > 0 OR city.Name like 'b%'", "(fare > 0 OR city.Name: \"b*\")", null); - testPushDown( - sessionHolder, - "lower(city.Region.Name) = 'hello world' OR city.Region.Id != 1", - null, - "(lower(city.Region.Name) = 'hello world' OR city.Region.Id != 1)"); - - // Multiple ORs - testPushDown( - sessionHolder, - "fare > 0 OR city.Name like 'b%' OR lower(city.Region.Name) = 'hello world' OR city.Region.Id != 1", - null, - "fare > 0 OR city.Name like 'b%' OR lower(city.Region.Name) = 'hello world' OR city.Region.Id != 1"); - testPushDown( - sessionHolder, - "fare > 0 OR city.Name like 'b%' OR city.Region.Id != 1", - "((fare > 0 OR city.Name: \"b*\") OR NOT city.Region.Id: 1)", - null); - } - - @Test - public void testAndPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - // AND conditions with partial/full push down - testPushDown(sessionHolder, "fare > 0 AND city.Name like 'b%'", "(fare > 0 AND city.Name: \"b*\")", null); - - testPushDown(sessionHolder, "lower(city.Region.Name) = 'hello world' AND city.Region.Id != 1", "(NOT city.Region.Id: 1)", "lower(city.Region.Name) = 'hello world'"); - - // Multiple ANDs - testPushDown( - sessionHolder, - "fare > 0 AND city.Name like 'b%' AND lower(city.Region.Name) = 'hello world' AND city.Region.Id != 1", - "(((fare > 0 AND city.Name: \"b*\")) AND NOT city.Region.Id: 1)", - "(lower(city.Region.Name) = 'hello world')"); - - testPushDown( - sessionHolder, - "fare > 0 AND city.Name like '%b%' AND lower(city.Region.Name) = 'hello world' AND city.Region.Id != 1", - "(((fare > 0)) AND NOT city.Region.Id: 1)", - "city.Name like '%b%' AND lower(city.Region.Name) = 'hello world'"); - } - - @Test - public void testNotPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - // NOT and inequality predicates - testPushDown(sessionHolder, "city.Region.Name NOT LIKE 'hello%'", "NOT city.Region.Name: \"hello*\"", null); - testPushDown(sessionHolder, "NOT (city.Region.Name LIKE 'hello%')", "NOT city.Region.Name: \"hello*\"", null); - testPushDown(sessionHolder, "city.Name != 'hello world'", "NOT city.Name: \"hello world\"", null); - testPushDown(sessionHolder, "city.Name <> 'hello world'", "NOT city.Name: \"hello world\"", null); - testPushDown(sessionHolder, "NOT (city.Name = 'hello world')", "NOT city.Name: \"hello world\"", null); - testPushDown(sessionHolder, "fare != 0", "NOT fare: 0", null); - testPushDown(sessionHolder, "fare <> 0", "NOT fare: 0", null); - testPushDown(sessionHolder, "NOT (fare = 0)", "NOT fare: 0", null); - - // Multiple NOTs - testPushDown(sessionHolder, "NOT (NOT fare = 0)", "NOT NOT fare: 0", null); - testPushDown(sessionHolder, "NOT (fare = 0 AND city.Name = 'hello world')", "NOT (fare: 0 AND city.Name: \"hello world\")", null); - testPushDown(sessionHolder, "NOT (fare = 0 OR city.Name = 'hello world')", "NOT (fare: 0 OR city.Name: \"hello world\")", null); - } - - @Test - public void testInPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - // IN predicate - testPushDown(sessionHolder, "city.Name IN ('hello world', 'hello world 2')", "(city.Name: \"hello world\" OR city.Name: \"hello world 2\")", null); - } - - @Test - public void testIsNullPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - // IS NULL / IS NOT NULL predicates - testPushDown(sessionHolder, "city.Name IS NULL", "NOT city.Name: *", null); - testPushDown(sessionHolder, "city.Name IS NOT NULL", "NOT NOT city.Name: *", null); - testPushDown(sessionHolder, "NOT (city.Name IS NULL)", "NOT NOT city.Name: *", null); - } - - @Test - public void testComplexPushDown() - { - SessionHolder sessionHolder = new SessionHolder(); - - // Complex AND/OR with partial pushdown - testPushDown( - sessionHolder, - "(fare > 0 OR city.Name like 'b%') AND (lower(city.Region.Name) = 'hello world' OR city.Name IS NULL)", - "((fare > 0 OR city.Name: \"b*\"))", - "(lower(city.Region.Name) = 'hello world' OR city.Name IS NULL)"); - - testPushDown( - sessionHolder, - "city.Region.Id = 1 AND (fare > 0 OR city.Name NOT like 'b%') AND (lower(city.Region.Name) = 'hello world' OR city.Name IS NULL)", - "((city.Region.Id: 1 AND (fare > 0 OR NOT city.Name: \"b*\")))", - "lower(city.Region.Name) = 'hello world' OR city.Name IS NULL"); - } - - @Test - public void testMetadataSqlGeneration() - { - SessionHolder sessionHolder = new SessionHolder(); - Set testMetadataFilterColumns = ImmutableSet.of("fare"); - - // Normal case - testPushDown( - sessionHolder, - "(fare > 0 AND city.Name like 'b%')", - "(fare > 0 AND city.Name: \"b*\")", - "(\"fare\" > 0)", - testMetadataFilterColumns); - - // With BETWEEN - testPushDown( - sessionHolder, - "((fare BETWEEN 0 AND 5) AND city.Name like 'b%')", - "(fare >= 0 AND fare <= 5 AND city.Name: \"b*\")", - "(\"fare\" >= 0 AND \"fare\" <= 5)", - testMetadataFilterColumns); - - // The cases of that the metadata filter column exist but cannot be push down - testPushDown( - sessionHolder, - "(fare > 0 OR city.Name like 'b%')", - "(fare > 0 OR city.Name: \"b*\")", - null, - testMetadataFilterColumns); - testPushDown( - sessionHolder, - "(fare > 0 AND city.Name like 'b%') OR city.Region.Id = 1", - "((fare > 0 AND city.Name: \"b*\") OR city.Region.Id: 1)", - null, - testMetadataFilterColumns); - - // Complicated case - testPushDown( - sessionHolder, - "fare = 0 AND (city.Name like 'b%' OR city.Region.Id = 1)", - "(fare: 0 AND (city.Name: \"b*\" OR city.Region.Id: 1))", - "(\"fare\" = 0)", - testMetadataFilterColumns); - } - - @Test - public void testClpWildcardUdf() - { - SessionHolder sessionHolder = new SessionHolder(); - - testPushDown(sessionHolder, "CLP_WILDCARD_STRING_COLUMN() = 'Beijing'", "*: \"Beijing\"", null); - testPushDown(sessionHolder, "CLP_WILDCARD_INT_COLUMN() = 1", "*: 1", null); - testPushDown(sessionHolder, "CLP_WILDCARD_FLOAT_COLUMN() > 0", "* > 0", null); - testPushDown(sessionHolder, "CLP_WILDCARD_BOOL_COLUMN() = true", "*: true", null); - - testPushDown(sessionHolder, "CLP_WILDCARD_STRING_COLUMN() like 'hello%'", "*: \"hello*\"", null); - testPushDown(sessionHolder, "substr(CLP_WILDCARD_STRING_COLUMN(), 1, 2) = 'he'", "*: \"he*\"", null); - testPushDown(sessionHolder, "CLP_WILDCARD_INT_COLUMN() BETWEEN 0 AND 5", "* >= 0 AND * <= 5", null); - testPushDown(sessionHolder, "CLP_WILDCARD_STRING_COLUMN() IN ('hello world', 'hello world 2')", "(*: \"hello world\" OR *: \"hello world 2\")", null); - - testPushDown(sessionHolder, "NOT CLP_WILDCARD_FLOAT_COLUMN() > 0", "NOT * > 0", null); - testPushDown( - sessionHolder, - "CLP_WILDCARD_STRING_COLUMN() = 'Beijing' AND CLP_WILDCARD_INT_COLUMN() = 1 AND city.Region.Id = 1", - "((*: \"Beijing\" AND *: 1) AND city.Region.Id: 1)", - null); - testPushDown( - sessionHolder, - "CLP_WILDCARD_STRING_COLUMN() = 'Toronto' OR CLP_WILDCARD_INT_COLUMN() = 2", - "(*: \"Toronto\" OR *: 2)", - null); - testPushDown( - sessionHolder, - "CLP_WILDCARD_STRING_COLUMN() = 'Shanghai' AND (CLP_WILDCARD_INT_COLUMN() = 3 OR city.Region.Id = 5)", - "(*: \"Shanghai\" AND (*: 3 OR city.Region.Id: 5))", - null); - } - - private void testPushDown(SessionHolder sessionHolder, String sql, String expectedKql, String expectedRemaining) - { - ClpExpression clpExpression = tryPushDown(sql, sessionHolder, ImmutableSet.of()); - testFilter(clpExpression, expectedKql, expectedRemaining, sessionHolder); - } - - private void testPushDown(SessionHolder sessionHolder, String sql, String expectedKql, String expectedMetadataSqlQuery, Set metadataFilterColumns) - { - ClpExpression clpExpression = tryPushDown(sql, sessionHolder, metadataFilterColumns); - testFilter(clpExpression, expectedKql, null, sessionHolder); - if (expectedMetadataSqlQuery != null) { - assertTrue(clpExpression.getMetadataSqlQuery().isPresent()); - assertEquals(clpExpression.getMetadataSqlQuery().get(), expectedMetadataSqlQuery); - } - else { - assertFalse(clpExpression.getMetadataSqlQuery().isPresent()); - } - } - - private ClpExpression tryPushDown( - String sqlExpression, - SessionHolder sessionHolder, - Set metadataFilterColumns) - { - RowExpression pushDownExpression = getRowExpression(sqlExpression, sessionHolder); - Map assignments = new HashMap<>(variableToColumnHandleMap); - return pushDownExpression.accept( - new ClpFilterToKqlConverter( - standardFunctionResolution, - functionAndTypeManager, - assignments, - metadataFilterColumns), - null); - } - - private void testFilter( - ClpExpression clpExpression, - String expectedKqlExpression, - String expectedRemainingExpression, - SessionHolder sessionHolder) - { - Optional kqlExpression = clpExpression.getPushDownExpression(); - Optional remainingExpression = clpExpression.getRemainingExpression(); - if (expectedKqlExpression != null) { - assertTrue(kqlExpression.isPresent()); - assertEquals(kqlExpression.get(), expectedKqlExpression); - } - else { - assertFalse(kqlExpression.isPresent()); - } - - if (expectedRemainingExpression != null) { - assertTrue(remainingExpression.isPresent()); - assertEquals(remainingExpression.get(), getRowExpression(expectedRemainingExpression, sessionHolder)); - } - else { - assertFalse(remainingExpression.isPresent()); - } - } -} diff --git a/presto-clp/src/test/java/com/facebook/presto/plugin/clp/TestingClpSessionContext.java b/presto-clp/src/test/java/com/facebook/presto/plugin/clp/TestingClpSessionContext.java new file mode 100644 index 000000000000..883cc8475c6c --- /dev/null +++ b/presto-clp/src/test/java/com/facebook/presto/plugin/clp/TestingClpSessionContext.java @@ -0,0 +1,168 @@ +/* + * 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 com.facebook.presto.plugin.clp; + +import com.facebook.presto.Session; +import com.facebook.presto.common.RuntimeStats; +import com.facebook.presto.common.transaction.TransactionId; +import com.facebook.presto.server.SessionContext; +import com.facebook.presto.spi.ConnectorId; +import com.facebook.presto.spi.function.SqlFunctionId; +import com.facebook.presto.spi.function.SqlInvokedFunction; +import com.facebook.presto.spi.security.Identity; +import com.facebook.presto.spi.session.ResourceEstimates; +import com.facebook.presto.spi.tracing.Tracer; +import com.google.common.collect.ImmutableMap; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static java.util.Map.Entry; +import static java.util.Objects.requireNonNull; + +public class TestingClpSessionContext + implements SessionContext +{ + private final Session session; + + public TestingClpSessionContext(Session session) + { + this.session = requireNonNull(session, "session is null"); + } + + @Override + public Identity getIdentity() + { + return session.getIdentity(); + } + + @Override + public String getCatalog() + { + return session.getCatalog().orElse("clp"); + } + + @Override + public String getSchema() + { + return session.getSchema().orElse("default"); + } + + @Override + public String getSource() + { + return session.getSource().orElse(null); + } + + @Override + public Optional getTraceToken() + { + return session.getTraceToken(); + } + + @Override + public String getRemoteUserAddress() + { + return session.getRemoteUserAddress().orElse(null); + } + + @Override + public String getUserAgent() + { + return session.getUserAgent().orElse(null); + } + + @Override + public String getClientInfo() + { + return session.getClientInfo().orElse(null); + } + + @Override + public Set getClientTags() + { + return session.getClientTags(); + } + + @Override + public ResourceEstimates getResourceEstimates() + { + return session.getResourceEstimates(); + } + + @Override + public String getTimeZoneId() + { + return session.getTimeZoneKey().getId(); + } + + @Override + public String getLanguage() + { + return session.getLocale().getLanguage(); + } + + @Override + public Optional getTracer() + { + return session.getTracer(); + } + + @Override + public Map getSystemProperties() + { + return session.getSystemProperties(); + } + + @Override + public Map> getCatalogSessionProperties() + { + ImmutableMap.Builder> catalogSessionProperties = ImmutableMap.builder(); + for (Entry> entry : session.getConnectorProperties().entrySet()) { + catalogSessionProperties.put(entry.getKey().getCatalogName(), entry.getValue()); + } + return catalogSessionProperties.build(); + } + + @Override + public Map getPreparedStatements() + { + return session.getPreparedStatements(); + } + + @Override + public Optional getTransactionId() + { + return session.getTransactionId(); + } + + @Override + public boolean supportClientTransaction() + { + return session.isClientTransactionSupport(); + } + + @Override + public Map getSessionFunctions() + { + return session.getSessionFunctions(); + } + + @Override + public RuntimeStats getRuntimeStats() + { + return session.getRuntimeStats(); + } +}