This is an automated email from the ASF dual-hosted git repository.
tkhurana pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/master by this push:
new 6ac1c4bc89 PHOENIX-7170 Conditional TTL (addendum)
6ac1c4bc89 is described below
commit 6ac1c4bc89cd99beb30d2e109694bd67bc75b39f
Author: tkhurana <[email protected]>
AuthorDate: Mon Apr 14 16:35:14 2025 -0700
PHOENIX-7170 Conditional TTL (addendum)
---
.../schema/CompiledConditionalTTLExpression.java | 98 ++++++++-----
.../phoenix/schema/ConditionalTTLExpressionIT.java | 19 ++-
.../schema/ConditionalTTLExpressionTest.java | 158 +++++++++++++++++++++
3 files changed, 227 insertions(+), 48 deletions(-)
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/CompiledConditionalTTLExpression.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/CompiledConditionalTTLExpression.java
index 334ddd662e..2219bcb75b 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/CompiledConditionalTTLExpression.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/CompiledConditionalTTLExpression.java
@@ -29,7 +29,6 @@ import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -38,7 +37,6 @@ import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.ByteStringer;
-import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.WritableUtils;
import org.apache.phoenix.coprocessor.generated.PTableProtos;
import org.apache.phoenix.coprocessor.generated.ServerCachingProtos;
@@ -49,7 +47,6 @@ import org.apache.phoenix.schema.tuple.MultiKeyValueTuple;
import org.apache.phoenix.schema.types.PBoolean;
import
org.apache.phoenix.thirdparty.com.google.common.annotations.VisibleForTesting;
import org.apache.phoenix.thirdparty.com.google.common.base.Preconditions;
-import org.apache.phoenix.thirdparty.com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -63,6 +60,10 @@ public class CompiledConditionalTTLExpression implements
CompiledTTLExpression {
private final Expression compiledExpr;
// columns referenced in the ttl expression to be added to scan
private final Set<ColumnReference> conditionExprColumns;
+ // used to determine the latest version of a row in case of raw scan
+ List<Cell> latestRowVersion = new ArrayList<>();
+ // record DeleteFamilyVersion cells in a row
+ List<Cell> deleteFamilyVersionCells = new ArrayList<>();
public CompiledConditionalTTLExpression(String ttlExpr,
Expression compiledExpression,
@@ -108,48 +109,69 @@ public class CompiledConditionalTTLExpression implements
CompiledTTLExpression {
* the oldest. The method leverages this ordering and groups the cells
into their columns
* based on the pair of family name and column qualifier.
*/
- private List<Cell> getLatestRowVersion(List<Cell> result) {
- List<Cell> latestRowVersion = new ArrayList<>();
- Cell currentColumnCell = null;
- Cell maxDeleteFamily = null;
- Cell maxDeleteFamilyVersion = null;
- for (Cell cell : result) {
+ @VisibleForTesting
+ public List<Cell> getLatestRowVersion(List<Cell> result) {
+ latestRowVersion.clear();
+ deleteFamilyVersionCells.clear();
+ Cell maxDeleteFamily = null; // record the DeleteFamily cell with the
highest timestamp
+ int index = 0;
+ while (index < result.size()) {
+ Cell cell = result.get(index);
if (cell.getType() == Cell.Type.DeleteFamily) {
- if (maxDeleteFamily == null) {
- maxDeleteFamily = cell;
- }
- // no need to add the DeleteFamily cell since it can't be part
of
- // an expression
- continue;
+ // record the first DeleteFamily cell
+ maxDeleteFamily = cell;
+ // we can now skip the delete family column
+ index = skipColumn(result, cell, index);
}
- if (cell.getType() == Cell.Type.DeleteFamilyVersion) {
- if (maxDeleteFamilyVersion == null) {
- maxDeleteFamilyVersion = cell;
- }
- // no need to add the DeleteFamilyVersion cell since it can't
be part of
- // an expression
- continue;
+ else if (cell.getType() == Cell.Type.DeleteFamilyVersion) {
+ // record and skip this cell
+ deleteFamilyVersionCells.add(cell);
+ index++;
}
- if (currentColumnCell == null ||
- !CellUtil.matchingColumn(cell, currentColumnCell)) {
- if (maxDeleteFamilyVersion != null &&
- cell.getTimestamp() ==
maxDeleteFamilyVersion.getTimestamp()) {
- // only add the cell if it is not masked by the
DeleteFamilyVersion
- continue;
- }
- // found a new column cell which has the latest timestamp
- currentColumnCell = cell;
- if (maxDeleteFamily != null &&
+ else if (cell.getType() == Cell.Type.DeleteColumn) {
+ // we can skip all the remaining cells of this column
+ index = skipColumn(result, cell, index);
+ } else if (cell.getType() == Cell.Type.Delete) {
+ index++;
+ } else if (cell.getType() == Cell.Type.Put) {
+ // check if this cell is masked by DeleteFamilyVersion
+ if
(deleteFamilyVersionCells.stream().anyMatch(deleteFamilyCell ->
+ cell.getTimestamp() ==
deleteFamilyCell.getTimestamp())) {
+ // skip the cell
+ index++;
+ } else if (maxDeleteFamily != null &&
cell.getTimestamp() <= maxDeleteFamily.getTimestamp())
{
- // only add the cell if it is not masked by the
DeleteFamily
- continue;
+ // this cell is masked by the DeleteFamily
+ index = skipColumn(result, cell, index);
+ } else {
+ latestRowVersion.add(cell);
+ // we have found the latest cell, skip the remaining cells
of this column
+ index = skipColumn(result, cell, index);
}
- latestRowVersion.add(currentColumnCell);
}
}
return latestRowVersion;
}
+ /**
+ * Skip the remaining cells of the current column
+ * @param result The list of cells (input)
+ * @param currentColumnCell The cell indicates the current column (input)
+ * @param index The index of the current cell in result (input)
+ * @return the index of the first cell of the next column
+ */
+ private int skipColumn(List<Cell> result, Cell currentColumnCell, int
index) {
+ int next = index + 1;
+ while (next < result.size()) {
+ Cell cell = result.get(next);
+ if (!CellUtil.matchingColumn(cell, currentColumnCell)) {
+ break;
+ }
+ next++;
+ }
+ return next;
+ }
+
@Override
/**
* @param result row to be evaluated against the conditional ttl
expression. The cells
@@ -184,11 +206,11 @@ public class CompiledConditionalTTLExpression implements
CompiledTTLExpression {
return false;
}
ImmutableBytesWritable ptr = new ImmutableBytesWritable();
- List<Cell> latestRowVersion = !isRaw ? result :
getLatestRowVersion(result);
- if (latestRowVersion.isEmpty()) {
+ List<Cell> latestRow = !isRaw ? result : getLatestRowVersion(result);
+ if (latestRow.isEmpty()) {
return false;
}
- MultiKeyValueTuple row = new MultiKeyValueTuple(latestRowVersion);
+ MultiKeyValueTuple row = new MultiKeyValueTuple(latestRow);
if (!compiledExpr.evaluate(row, ptr)) {
LOGGER.info("Expression evaluation failed for expr {}", ttlExpr);
return false;
diff --git
a/phoenix-core/src/it/java/org/apache/phoenix/schema/ConditionalTTLExpressionIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/schema/ConditionalTTLExpressionIT.java
index 4e3fa275de..4b73197631 100644
---
a/phoenix-core/src/it/java/org/apache/phoenix/schema/ConditionalTTLExpressionIT.java
+++
b/phoenix-core/src/it/java/org/apache/phoenix/schema/ConditionalTTLExpressionIT.java
@@ -748,7 +748,7 @@ public class ConditionalTTLExpressionIT extends
ParallelStatsDisabledIT {
TestUtil.dumpTable(conn, TableName.valueOf(fullIndexName));
assertEquals(rowCount - 1, actual);
// First read row 0 which is not expired
- String dql = String.format("select VAL2, VAL5 from %s where
VAL1='%s'",
+ String dql = String.format("select VAL2, VAL5 from %s where
VAL1='%s' AND ID2=0",
fullDataTableName, val1_0);
try (ResultSet rs1 = conn.createStatement().executeQuery(dql)) {
PhoenixResultSet prs = rs1.unwrap(PhoenixResultSet.class);
@@ -759,7 +759,7 @@ public class ConditionalTTLExpressionIT extends
ParallelStatsDisabledIT {
assertFalse(rs1.getBoolean(ttlCol));
}
// row 1 is expired
- dql = String.format("select VAL2, VAL5 from %s where VAL1='%s'",
+ dql = String.format("select VAL2, VAL5 from %s where VAL1='%s' AND
ID2=1",
fullDataTableName, val1_1);
try (ResultSet rs1 = conn.createStatement().executeQuery(dql)) {
PhoenixResultSet prs = rs1.unwrap(PhoenixResultSet.class);
@@ -792,14 +792,13 @@ public class ConditionalTTLExpressionIT extends
ParallelStatsDisabledIT {
IndexToolIT.dumpMRJobCounters(mrJobCounters);
throw e;
}
- // TODO: Broken because of PHOENIX-7574
- //doMajorCompaction(fullIndexName);
- //TestUtil.dumpTable(conn, TableName.valueOf(fullIndexName));
- //actual = TestUtil.getRowCountFromIndex(conn, fullDataTableName,
fullIndexName);
- //assertEquals(rowCount - 1, actual);
- //CellCount expectedCellCount = new CellCount();
- //expectedCellCount.insertRow(indexRowPosToKey.get(0), 2);
- //validateTable(conn, fullIndexName, expectedCellCount,
indexRowPosToKey.values());
+ doMajorCompaction(fullIndexName);
+ TestUtil.dumpTable(conn, TableName.valueOf(fullIndexName));
+ actual = TestUtil.getRowCountFromIndex(conn, fullDataTableName,
fullIndexName);
+ assertEquals(rowCount - 1, actual);
+ CellCount expectedCellCount = new CellCount();
+ expectedCellCount.insertRow(indexRowPosToKey.get(0),
includedColumns.size() + 1);
+ validateTable(conn, fullIndexName, expectedCellCount,
indexRowPosToKey.values());
}
}
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/schema/ConditionalTTLExpressionTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/schema/ConditionalTTLExpressionTest.java
index d20dd901c3..fddae2229f 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/schema/ConditionalTTLExpressionTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/schema/ConditionalTTLExpressionTest.java
@@ -29,22 +29,37 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
+import java.sql.PreparedStatement;
import java.sql.SQLException;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
+import org.apache.hadoop.hbase.Cell;
+import org.apache.hadoop.hbase.CellComparator;
+import org.apache.hadoop.hbase.CellUtil;
+import org.apache.hadoop.hbase.KeyValue;
+import org.apache.hadoop.hbase.client.Mutation;
+import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Scan;
+import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.hadoop.hbase.util.Pair;
+import org.apache.phoenix.hbase.index.util.GenericKeyValueBuilder;
+import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
import org.apache.phoenix.thirdparty.com.google.common.collect.Lists;
import org.apache.phoenix.compile.QueryPlan;
import org.apache.phoenix.exception.PhoenixParserException;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixPreparedStatement;
import org.apache.phoenix.query.BaseConnectionlessQueryTest;
+import org.apache.phoenix.util.EnvironmentEdgeManager;
import org.junit.Test;
@@ -639,4 +654,147 @@ public class ConditionalTTLExpressionTest extends
BaseConnectionlessQueryTest {
validateScan(conn, tableName, query, ttl, false,
Lists.newArrayList("col1", "col2"));
}
}
+
+ @Test
+ public void testLatestRowVersion() throws Exception {
+ String ddlTemplate = "create table %s (id varchar not null primary
key, " +
+ "col1 integer, col2 integer, col3 varchar) TTL = '%s'";
+ String tableName = generateUniqueName();
+ String ttl = "col2 > 100 AND col3='expired'";
+ long currentTS = EnvironmentEdgeManager.currentTimeMillis();
+ try (Connection conn = DriverManager.getConnection(getUrl())) {
+ String ddl = String.format(ddlTemplate, tableName,
retainSingleQuotes(ttl));
+ conn.createStatement().execute(ddl);
+ PhoenixConnection pconn = conn.unwrap(PhoenixConnection.class);
+ PTable table = pconn.getTable(tableName);
+ int updateCount = 5;
+ String id = "a";
+ ImmutableBytesPtr rowKey = new
ImmutableBytesPtr(Bytes.toBytes(id));
+ List<Cell> updates = new ArrayList<>();
+ for (int i = 0; i < updateCount; ++i) {
+ updates.addAll(generateUpdate(id, i, i*i, "val_" + i,
currentTS + i*10));
+ }
+ // updates [
+ // lastUpdateTS - 40,
+ // lastUpdateTS - 30,
+ // lastUpdateTS - 20,
+ // lastUpdateTS - 10,
+ // lastUpdateTS
+ // ]
+ long lastupdateTS = currentTS + (updateCount - 1)* 10;
+
+ CompiledConditionalTTLExpression ttlExpr =
+ (CompiledConditionalTTLExpression)
table.getCompiledTTLExpression(pconn);
+
+ // case 1: no Deletes just multiple Put mutations
+ List<Cell> row = Lists.newArrayList(updates);
+ row.sort(CellComparator.getInstance());
+ List<Cell> latestRowVersion = ttlExpr.getLatestRowVersion(row);
+ assertEquals(3, latestRowVersion.size()); // 3 columns
+ for (Cell cell : latestRowVersion) {
+ assertEquals(lastupdateTS, cell.getTimestamp());
+ }
+
+ // case 2 insert DeleteFamily at the top
+ row = Lists.newArrayList(updates);
+ KeyValue df = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"),
null,
+ lastupdateTS + 5, KeyValue.Type.DeleteFamily, null);
+ row.add(df);
+ row.sort(CellComparator.getInstance());
+ latestRowVersion = ttlExpr.getLatestRowVersion(row);
+ assertTrue(latestRowVersion.isEmpty());
+
+ // case 3 insert DeleteFamilyVersion at last TS
+ row = Lists.newArrayList(updates);
+ KeyValue dfv = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"),
null, lastupdateTS,
+ KeyValue.Type.DeleteFamilyVersion, null);
+ row.add(dfv);
+ row.sort(CellComparator.getInstance());
+ latestRowVersion = ttlExpr.getLatestRowVersion(row);
+ assertEquals(3, latestRowVersion.size()); // 3 columns
+ for (Cell cell : latestRowVersion) {
+ assertEquals(lastupdateTS - 10, cell.getTimestamp());
+ }
+
+ // case 4 insert DeleteFamilyVersion at last TS, DeleteFamily in
the middle
+ row = Lists.newArrayList(updates);
+ df = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"), null,
+ lastupdateTS - 25, KeyValue.Type.DeleteFamily, null);
+ row.add(df);
+ row.add(dfv);
+ row.sort(CellComparator.getInstance());
+ latestRowVersion = ttlExpr.getLatestRowVersion(row);
+ assertEquals(3, latestRowVersion.size()); // 3 columns
+ for (Cell cell : latestRowVersion) {
+ assertEquals(lastupdateTS - 10, cell.getTimestamp());
+ }
+
+ // case 5 multiple DeleteFamilyVersions
+ row = Lists.newArrayList(updates);
+ row.add(dfv);
+ dfv = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"), null,
lastupdateTS - 10,
+ KeyValue.Type.DeleteFamilyVersion, null);
+ row.add(dfv);
+ row.sort(CellComparator.getInstance());
+ latestRowVersion = ttlExpr.getLatestRowVersion(row);
+ assertEquals(3, latestRowVersion.size()); // 3 columns
+ for (Cell cell : latestRowVersion) {
+ assertEquals(lastupdateTS - 20, cell.getTimestamp());
+ }
+
+ // case 6 DeleteFamily in the middle
+ row = Lists.newArrayList(updates);
+ df = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"), null,
+ lastupdateTS - 5, KeyValue.Type.DeleteFamily, null);
+ row.add(df);
+ row.sort(CellComparator.getInstance());
+ latestRowVersion = ttlExpr.getLatestRowVersion(row);
+ assertEquals(3, latestRowVersion.size()); // 3 columns
+ for (Cell cell : latestRowVersion) {
+ assertEquals(lastupdateTS, cell.getTimestamp());
+ }
+
+ // case 7 DeleteColumn
+ row = Lists.newArrayList(updates);
+ row.addAll(generateUpdate(id, 12, null, null, lastupdateTS + 10));
+ row.sort(CellComparator.getInstance());
+ latestRowVersion = ttlExpr.getLatestRowVersion(row);
+ assertEquals(1, latestRowVersion.size()); // 1 column is non null
+ for (Cell cell : latestRowVersion) {
+ assertEquals(lastupdateTS + 10, cell.getTimestamp());
+ }
+ }
+ }
+
+ private List<Cell> generateUpdate(String id, Integer col1, Integer col2,
String col3, long ts) {
+ List<Cell> update = Lists.newArrayList();
+ if (col1 != null) {
+ KeyValue kv = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"),
Bytes.toBytes("col1"),
+ ts, KeyValue.Type.Put, Bytes.toBytes(col1));
+ update.add(kv);
+ } else {
+ KeyValue kv = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"),
Bytes.toBytes("col1"),
+ ts, KeyValue.Type.DeleteColumn, null);
+ update.add(kv);
+ }
+ if (col2 != null) {
+ KeyValue kv = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"),
Bytes.toBytes("col2"),
+ ts, KeyValue.Type.Put, Bytes.toBytes(col2));
+ update.add(kv);
+ } else {
+ KeyValue kv = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"),
Bytes.toBytes("col2"),
+ ts, KeyValue.Type.DeleteColumn, null);
+ update.add(kv);
+ }
+ if (col3 != null) {
+ KeyValue kv = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"),
Bytes.toBytes("col3"),
+ ts, KeyValue.Type.Put, Bytes.toBytes(col3));
+ update.add(kv);
+ } else {
+ KeyValue kv = new KeyValue(Bytes.toBytes(id), Bytes.toBytes("f"),
Bytes.toBytes("col3"),
+ ts, KeyValue.Type.DeleteColumn, null);
+ update.add(kv);
+ }
+ return update;
+ }
}