diff --git a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ddl/DdlCommandHandler.java b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ddl/DdlCommandHandler.java index e7c268d8918d0..ae0ee556f846b 100644 --- a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ddl/DdlCommandHandler.java +++ b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/ddl/DdlCommandHandler.java @@ -309,9 +309,10 @@ private void handle0(AlterTableDropCommand cmd) throws IgniteCheckedException { /** */ private QueryEntity toQueryEntity(CreateTableCommand cmd) { - QueryEntity res = new QueryEntity(); + QueryEntityEx res = new QueryEntityEx(); res.setTableName(cmd.tableName()); + res.sql(true); Set notNullFields = null; @@ -369,7 +370,7 @@ private QueryEntity toQueryEntity(CreateTableCommand cmd) { if (!F.isEmpty(cmd.primaryKeyColumns())) { res.setKeyFields(new LinkedHashSet<>(cmd.primaryKeyColumns())); - res = new QueryEntityEx(res).setPreserveKeysOrder(true); + res.setPreserveKeysOrder(true); } } else if (!F.isEmpty(cmd.primaryKeyColumns()) && cmd.primaryKeyColumns().size() == 1) { @@ -383,19 +384,14 @@ else if (!F.isEmpty(cmd.primaryKeyColumns()) && cmd.primaryKeyColumns().size() = // if pk is not explicitly set, we create it ourselves keyTypeName = IgniteUuid.class.getName(); - res = new QueryEntityEx(res).implicitPk(true); + res.implicitPk(true); } res.setValueType(F.isEmpty(cmd.valueTypeName()) ? valTypeName : cmd.valueTypeName()); res.setKeyType(keyTypeName); - if (!F.isEmpty(notNullFields)) { - QueryEntityEx res0 = new QueryEntityEx(res); - - res0.setNotNullFields(notNullFields); - - res = res0; - } + if (!F.isEmpty(notNullFields)) + res.setNotNullFields(notNullFields); return res; } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java index 92738a48b8ff8..9d1e6f8e16519 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/cache/query/index/sorted/SortedSegmentedIndex.java @@ -69,7 +69,7 @@ public GridCursor find( * Finds first index row for specified tree segment and cache filter. * * @param segment Number of tree segment to find. - * @param qryCtx External index qyery context. + * @param qryCtx External index query context. * @return Cursor of found index rows. */ public GridCursor findFirst(int segment, IndexQueryContext qryCtx) @@ -79,7 +79,7 @@ public GridCursor findFirst(int segment, IndexQueryContext qryCtx) * Finds last index row for specified tree segment and cache filter. * * @param segment Number of tree segment to find. - * @param qryCtx External index qyery context. + * @param qryCtx External index query context. * @return Cursor of found index rows. */ public GridCursor findLast(int segment, IndexQueryContext qryCtx) @@ -88,7 +88,7 @@ public GridCursor findLast(int segment, IndexQueryContext qryCtx) /** * Takes only one first or last index record. * - * @param qryCtx External index qyery context. + * @param qryCtx External index query context. * @param first {@code True} to take first index value. {@code False} to take last index value. */ public GridCursor findFirstOrLast(IndexQueryContext qryCtx, boolean first) throws IgniteCheckedException; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryTypeDescriptor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryTypeDescriptor.java index 02a6212848989..06d4e4e938bb2 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryTypeDescriptor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/GridQueryTypeDescriptor.java @@ -236,6 +236,23 @@ public interface GridQueryTypeDescriptor { */ public void implicitPk(boolean implicitPk); + /** + * Gets whether a type was created by SQL. + * + * NOTE: There is a difference between query type descriptor sql flag and cache descriptor sql flag. The same flag + * on cache descriptor specifies whether entire cache was created by sql, but query type descriptor sql flag can + * be set to true even when cache sql flag is false (for example, when cache was created by cache API, + * but query type was added by SQL). + */ + public boolean sql(); + + /** + * Sets whether a type was created by DDL. + * + * @param sql Whether a type was created by SQL. + */ + public void sql(boolean sql); + /** * @return {@code true} if absent PK parts should be filled with defaults, {@code false} otherwise. */ diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityEx.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityEx.java index 54085294c636a..3c615142cd002 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityEx.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryEntityEx.java @@ -50,6 +50,9 @@ public class QueryEntityEx extends QueryEntity { /** INLINE_SIZE for affinity field index. */ private Integer affKeyInlineSize; + /** Whether query entity was created by SQL. */ + private boolean sql; + /** * Default constructor. */ @@ -77,6 +80,7 @@ public QueryEntityEx(QueryEntity other) { fillAbsentPKsWithDefaults = other0.fillAbsentPKsWithDefaults; pkInlineSize = other0.pkInlineSize != null ? other0.pkInlineSize : -1; affKeyInlineSize = other0.affKeyInlineSize != null ? other0.affKeyInlineSize : -1; + sql = other0.sql; } } @@ -121,6 +125,18 @@ public QueryEntity implicitPk(boolean implicitPk) { return this; } + /** */ + public boolean sql() { + return sql; + } + + /** */ + public QueryEntityEx sql(boolean sql) { + this.sql = sql; + + return this; + } + /** * @return {@code true} if absent PK parts should be filled with defaults, {@code false} otherwise. */ @@ -204,7 +220,8 @@ public QueryEntity setAffinityKeyInlineSize(Integer affKeyInlineSize) { && preserveKeysOrder == entity.preserveKeysOrder && implicitPk == entity.implicitPk && Objects.equals(pkInlineSize, entity.pkInlineSize) - && Objects.equals(affKeyInlineSize, entity.affKeyInlineSize); + && Objects.equals(affKeyInlineSize, entity.affKeyInlineSize) + && sql == entity.sql; } /** {@inheritDoc} */ @@ -216,6 +233,7 @@ public QueryEntity setAffinityKeyInlineSize(Integer affKeyInlineSize) { res = 31 * res + (implicitPk ? 1 : 0); res = 31 * res + (pkInlineSize != null ? pkInlineSize.hashCode() : 0); res = 31 * res + (affKeyInlineSize != null ? affKeyInlineSize.hashCode() : 0); + res = 31 * res + (sql ? 1 : 0); return res; } diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java index 2ad7a194cc0dc..7b917e69a4d30 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryTypeDescriptorImpl.java @@ -162,6 +162,9 @@ public class QueryTypeDescriptorImpl implements GridQueryTypeDescriptor { /** @see SqlConfiguration#isValidationEnabled() */ private final boolean validateTypes; + /** */ + private boolean sql; + /** * Constructor. * @@ -834,6 +837,16 @@ else if (cacheObjects.typeId(expColType.getName()) != ((BinaryObject)val).type() this.implicitPk = implicitPk; } + /** {@inheritDoc} */ + @Override public boolean sql() { + return sql; + } + + /** {@inheritDoc} */ + @Override public void sql(boolean sql) { + this.sql = sql; + } + /** {@inheritDoc} */ @Override public boolean fillAbsentPKsWithDefaults() { return fillAbsentPKsWithDefaults; diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryUtils.java index e2f829b5187fc..782012cc9da2a 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryUtils.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/QueryUtils.java @@ -629,6 +629,7 @@ public static QueryTypeCandidate typeForQueryEntity(GridKernalContext ctx, Strin desc.primaryKeyInlineSize(qe.getPrimaryKeyInlineSize() != null ? qe.getPrimaryKeyInlineSize() : -1); desc.affinityFieldInlineSize(qe.getAffinityKeyInlineSize() != null ? qe.getAffinityKeyInlineSize() : -1); + desc.sql(qe.sql()); } else { desc.primaryKeyInlineSize(-1); diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/management/TableDescriptor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/management/TableDescriptor.java index 4c339e6c23fd0..938bc7b5836e0 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/management/TableDescriptor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/query/schema/management/TableDescriptor.java @@ -57,7 +57,7 @@ public class TableDescriptor { public TableDescriptor(GridCacheContextInfo cacheInfo, GridQueryTypeDescriptor typeDesc, boolean isSql) { this.cacheInfo = cacheInfo; this.typeDesc = typeDesc; - this.isSql = isSql; + this.isSql = isSql || typeDesc.sql(); if (F.isEmpty(typeDesc.affinityKey()) || Objects.equals(typeDesc.affinityKey(), typeDesc.keyFieldName())) affKey = QueryUtils.KEY_FIELD_NAME; diff --git a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java index d56a96fb02dfc..4f1091c8884f9 100644 --- a/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java +++ b/modules/indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/CommandProcessor.java @@ -450,6 +450,7 @@ private static QueryEntity toQueryEntity(GridSqlCreateTable createTbl) { QueryEntityEx res = new QueryEntityEx(); res.setTableName(createTbl.tableName()); + res.sql(true); Set notNullFields = null; diff --git a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/ComplexPrimaryKeyUnwrapSelfTest.java b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/ComplexPrimaryKeyUnwrapSelfTest.java index 9d890fcc47588..48c65fdc195cc 100644 --- a/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/ComplexPrimaryKeyUnwrapSelfTest.java +++ b/modules/indexing/src/test/java/org/apache/ignite/internal/processors/cache/index/ComplexPrimaryKeyUnwrapSelfTest.java @@ -25,7 +25,9 @@ import org.apache.ignite.cache.query.annotations.QuerySqlField; import org.apache.ignite.configuration.CacheConfiguration; import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.IgniteInternalFuture; import org.apache.ignite.internal.util.typedef.F; +import org.apache.ignite.internal.util.typedef.internal.CU; import org.junit.Test; /** @@ -55,19 +57,104 @@ public class ComplexPrimaryKeyUnwrapSelfTest extends AbstractIndexingCommonTest */ @Test public void testComplexPk() { + checkComplexPk(false); + } + + /** + * Test using PK indexes for complex primary key for DDL on existing cache. + */ + @Test + public void testComplexPkExistingCache() { + checkComplexPk(true); + } + + /** */ + private void checkComplexPk(boolean existingCache) { String tblName = createTableName(); + if (existingCache) + node().createCache(tblName); + executeSql("CREATE TABLE " + tblName + " (id int, name varchar, age int, company varchar, city varchar, " + - "primary key (id, name, city))"); + "primary key (id, name, city))" + (existingCache ? " WITH \"CACHE_NAME=" + tblName + "\"" : "")); checkUsingIndexes(tblName, "1", 2); } + /** + * Test using PK indexes for complex primary key and affinity key. + */ + @Test + public void testComplexPkWithAffinityKey() { + checkComplexPkWithAffinityKey(false); + } + + /** + * Test using PK indexes for complex primary key and affinity key for DDL on existing cache. + */ + @Test + public void testComplexPkWithAffinityKeyExistingCache() { + checkComplexPkWithAffinityKey(true); + } + + /** */ + private void checkComplexPkWithAffinityKey(boolean existingCache) { + String tblName = createTableName(); + + if (existingCache) + node().createCache(tblName); + + executeSql("CREATE TABLE " + tblName + " (id int, name varchar, age int, company varchar, city varchar, " + + "primary key (id, name, city)) " + + "WITH \"affinity_key=id" + + (existingCache ? ",CACHE_NAME=" + tblName : "") + "\""); + + // For the new cache unwrapped key field is used as affinity key, but for the existing cache + // affinity key can't be changed, so we can't prune partitions for query on ID field. + checkUsingIndexes(tblName, "1", existingCache ? 2 : 1); + } + + /** + * Test using PK indexes for complex primary key for DDL on existing cache on multiple nodes. + */ + @Test + public void testComplexPkExistingCacheMultiNode() throws Exception { + String tblName = createTableName(); + + try (IgniteEx ignite1 = startGrid(1)) { + node().createCache(tblName); + + executeSql("CREATE TABLE " + tblName + " (id int, name varchar, age int, company varchar, city varchar, " + + "primary key (id, name, city)) WITH \"CACHE_NAME=" + tblName + "\""); + + // Check on remote node, started before table created. + checkUsingIndexes(ignite1, tblName, "1", 2); + + try (IgniteEx ignite2 = startGrid(2)) { + // Check on remote node, started after table created. + checkUsingIndexes(ignite2, tblName, "1", 2); + } + } + } + /** * Test using PK indexes for simple primary key. */ @Test public void testSimplePk() { + checkSimplePk(false); + } + + /** + * Test using PK indexes for simple primary key for DDL on existing cache. + */ + @Test + public void testSimplePkExistingCache() { + checkSimplePk(true); + } + + /** */ + private void checkSimplePk(boolean existingCache) { //ToDo: IGNITE-8386: need to add DATE type into the test. HashMap types = new HashMap() { { @@ -85,21 +172,23 @@ public void testSimplePk() { put("bigint", "1"); put("varchar_ignorecase", "'1'"); put("time", "'11:11:11'"); - put("timestamp", "'20018-11-02 11:11:11'"); + put("timestamp", "'2018-11-02 11:11:11'"); put("uuid", "'1'"); } }; for (Map.Entry entry : types.entrySet()) { - String tblName = createTableName(); + if (existingCache) + node().createCache(tblName); + String type = entry.getKey(); String val = entry.getValue(); executeSql("CREATE TABLE " + tblName + " (id " + type + " , name varchar, age int, company varchar, city varchar," + - " primary key (id))"); + " primary key (id))" + (existingCache ? " WITH \"CACHE_NAME=" + tblName + "\"" : "")); checkUsingIndexes(tblName, val, 1); } @@ -110,6 +199,19 @@ public void testSimplePk() { */ @Test public void testSimplePkWithAffinityKey() { + checkSimplePkWithAffinityKey(false); + } + + /** + * Test using PK indexes for simple primary key and affinity key on existing cache. + */ + @Test + public void testSimplePkWithAffinityKeyExistingCache() { + checkSimplePkWithAffinityKey(true); + } + + /** */ + private void checkSimplePkWithAffinityKey(boolean existingCache) { //ToDo: IGNITE-8386: need to add DATE type into the test. HashMap types = new HashMap() { { @@ -127,21 +229,24 @@ public void testSimplePkWithAffinityKey() { put("bigint", "1"); put("varchar_ignorecase", "'1'"); put("time", "'11:11:11'"); - put("timestamp", "'20018-11-02 11:11:11'"); + put("timestamp", "'2018-11-02 11:11:11'"); put("uuid", "'1'"); } }; for (Map.Entry entry : types.entrySet()) { - String tblName = createTableName(); String type = entry.getKey(); String val = entry.getValue(); + if (existingCache) + node().createCache(tblName); + executeSql("CREATE TABLE " + tblName + " (id " + type + " , name varchar, age int, company varchar, city varchar," + - " primary key (id)) WITH \"affinity_key=id\""); + " primary key (id)) WITH \"affinity_key=id" + + (existingCache ? ",CACHE_NAME=" + tblName : "") + "\""); checkUsingIndexes(tblName, val, 1); } @@ -152,12 +257,31 @@ public void testSimplePkWithAffinityKey() { */ @Test public void testWrappedPk() { + checkWrappedPk(false); + } + + /** + * Test using PK indexes for wrapped primary key on existsing cache. + */ + @Test + public void testWrappedPkExistingCache() { + checkWrappedPk(true); + } + + /** */ + private void checkWrappedPk(boolean existingCache) { String tblName = createTableName(); + if (existingCache) + node().createCache(tblName); + executeSql("CREATE TABLE " + tblName + " (id int, name varchar, age int, company varchar, city varchar, " + - "primary key (id)) WITH \"wrap_key=true\""); + "primary key (id)) WITH \"wrap_key=true" + + (existingCache ? ",CACHE_NAME=" + tblName : "") + "\""); - checkUsingIndexes(tblName, "1", 1); + // For the new cache unwrapped key field is used as affinity key, but for the existing cache + // affinity key can't be changed, so we can't prune partitions for query on ID field. + checkUsingIndexes(tblName, "1", existingCache ? 2 : 1); } /** @@ -165,13 +289,31 @@ public void testWrappedPk() { */ @Test public void testInlineSizeNoWrap() { - executeSql("DROP TABLE IF EXISTS TABLE1"); - executeSql("CREATE TABLE IF NOT EXISTS TABLE1 ( " + + checkInlineSizeNoWrap(false); + } + + /** + * Test single column PK without wrapping calculate correct inline size on existing cache. + */ + @Test + public void testInlineSizeNoWrapExistingCache() { + checkInlineSizeNoWrap(true); + } + + /** */ + private void checkInlineSizeNoWrap(boolean existingCache) { + String tblName = createTableName(); + + if (existingCache) + node().createCache(tblName); + + executeSql("CREATE TABLE IF NOT EXISTS " + tblName + "( " + " id varchar(15), " + " col varchar(100), " + - " PRIMARY KEY(id) ) "); - assertEquals(18, executeSql( - "select INLINE_SIZE from SYS.INDEXES where TABLE_NAME = 'TABLE1' and IS_PK = true").get(0).get(0)); + " PRIMARY KEY(id) ) " + + (existingCache ? "WITH \"CACHE_NAME=" + tblName + "\"" : "") + ); + assertEquals(18, pkInlineSize(tblName)); } /** @@ -179,13 +321,30 @@ public void testInlineSizeNoWrap() { */ @Test public void testInlineSizeWrap() { - executeSql("DROP TABLE IF EXISTS TABLE1"); - executeSql("CREATE TABLE IF NOT EXISTS TABLE1 ( " + + checkInlineSizeWrap(false); + } + + /** + * Test single column PK with wrapping calculate correct inline size on existing cache. + */ + @Test + public void testInlineSizeWrapExistingCache() { + checkInlineSizeWrap(true); + } + + /** */ + private void checkInlineSizeWrap(boolean existingCache) { + String tblName = createTableName(); + + if (existingCache) + node().createCache(tblName); + + executeSql("CREATE TABLE IF NOT EXISTS " + tblName + "( " + " id varchar(15), " + " col varchar(100), " + - " PRIMARY KEY(id) ) WITH \"wrap_key=true\""); - assertEquals(18, executeSql( - "select INLINE_SIZE from SYS.INDEXES where TABLE_NAME = 'TABLE1' and IS_PK = true").get(0).get(0)); + " PRIMARY KEY(id) ) " + + " WITH \"wrap_key=true" + (existingCache ? ",CACHE_NAME=" + tblName : "") + "\""); + assertEquals(18, pkInlineSize(tblName)); } /** @@ -193,34 +352,78 @@ public void testInlineSizeWrap() { */ @Test public void testInlineSizeWrap2() { - executeSql("DROP TABLE IF EXISTS TABLE1"); - executeSql("CREATE TABLE IF NOT EXISTS TABLE1 ( " + + checkInlineSizeWrap2(false); + } + + /** + * Test two column PK with wrapping calculate correct inline size on existing cache. + */ + @Test + public void testInlineSizeWrap2ExistingCache() { + checkInlineSizeWrap2(true); + } + + /** */ + private void checkInlineSizeWrap2(boolean existingCache) { + String tblName = createTableName(); + + if (existingCache) + node().createCache(tblName); + + executeSql("CREATE TABLE IF NOT EXISTS " + tblName + "( " + " id varchar(15), " + " id2 uuid, " + " col varchar(100), " + - " PRIMARY KEY(id, id2) ) WITH \"wrap_key=true\""); - assertEquals(35, executeSql( - "select INLINE_SIZE from SYS.INDEXES where TABLE_NAME = 'TABLE1' and IS_PK = true").get(0).get(0)); + " PRIMARY KEY(id, id2) ) " + + " WITH \"wrap_key=true" + (existingCache ? ",CACHE_NAME=" + tblName : "") + "\""); + assertEquals(35, pkInlineSize(tblName)); + } + + /** */ + private int pkInlineSize(String tblName) { + return (int)executeSql("select INLINE_SIZE from SYS.INDEXES " + + "where TABLE_NAME = '" + tblName + "' and IS_PK = true").get(0).get(0); } /** * Check using PK indexes for few cases. * * @param tblName Name of table which should be checked to using PK indexes. - * @param expResCnt Expceted result count. + * @param expResCnt Expected result count. */ private void checkUsingIndexes(String tblName, String idVal, int expResCnt) { + checkUsingIndexes(node(), tblName, idVal, expResCnt); + } + + /** + * Check using PK indexes for few cases. + * + * @param node Ignite node. + * @param tblName Name of table which should be checked to using PK indexes. + * @param expResCnt Expected result count. + */ + private void checkUsingIndexes(IgniteEx node, String tblName, String idVal, int expResCnt) { + IgniteInternalFuture rebuildFut = indexRebuildFuture(node, CU.cacheId(tblName)); + + try { + if (rebuildFut != null) + rebuildFut.get(5_000L); + } + catch (Exception e) { + throw new AssertionError(e); + } + String explainSQL = "explain SELECT * FROM " + tblName + " WHERE "; - List> results = executeSql(explainSQL + "id=" + idVal); + List> results = executeSql(node, explainSQL + "id=" + idVal); assertUsingPkIndex(results, expResCnt); - results = executeSql(explainSQL + "id=" + idVal + " and name=''"); + results = executeSql(node, explainSQL + "id=" + idVal + " and name=''"); assertUsingPkIndex(results, expResCnt); - results = executeSql(explainSQL + "id=" + idVal + " and name='' and city='' and age=0"); + results = executeSql(node, explainSQL + "id=" + idVal + " and name='' and city='' and age=0"); assertUsingPkIndex(results, expResCnt); } @@ -252,14 +455,14 @@ public void testIndexesForCachesCreatedThroughCashApi() { * Check that explain plan result shown using PK index and don't use scan. * * @param results Result of execut explain plan query. - * @param expResCnt Expceted result count. + * @param expResCnt Expected result count. */ private void assertUsingPkIndex(List> results, int expResCnt) { assertEquals(expResCnt, results.size()); String explainPlan = (String)results.get(0).get(0); - assertTrue(explainPlan.contains("\"_key_PK")); + assertTrue("Current plan=[" + explainPlan + ']', explainPlan.contains("\"_key_PK")); assertFalse(explainPlan.contains("_SCAN_")); }