Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.execute.MutationState;
Expand All @@ -52,6 +53,7 @@
import org.apache.phoenix.expression.SingleCellColumnExpression;
import org.apache.phoenix.expression.visitor.StatelessTraverseNoExpressionVisitor;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData;
import org.apache.phoenix.jdbc.PhoenixStatement;
import org.apache.phoenix.jdbc.PhoenixStatement.Operation;
import org.apache.phoenix.parse.BindParseNode;
Expand Down Expand Up @@ -231,9 +233,17 @@ public MutationPlan compile(CreateTableStatement create) throws SQLException {
}
if (viewTypeToBe == ViewType.MAPPED && parentToBe.getPKColumns().isEmpty()) {
validateCreateViewCompilation(connection, parentToBe, columnDefs, pkConstraint);
} else if (where != null && viewTypeToBe == ViewType.UPDATABLE) {
} else if (viewTypeToBe == ViewType.UPDATABLE) {
rowKeyMatcher =
WhereOptimizer.getRowKeyMatcher(context, create.getTableName(), parentToBe, where);
// For no-WHERE tenant views on a multi-tenant base table, for a given tenant we allow
// EITHER any number of no-WHERE views without TTL, OR exactly one no-WHERE view with
// TTL. Multiple no-WHERE TTL views share the same ROW_KEY_MATCHER (tenant-id bytes)
// and conflict in the compaction RowKeyMatcher trie.
if (where == null) {
ViewUtil.validateTenantViewWithoutWhereTTLCoexistence(connection, parentToBe,
hasTTLProperty(create), null);
}
}
verifyIfAnyParentHasIndexesAndViewExtendsPk(parentToBe, columnDefs, pkConstraint);
}
Expand Down Expand Up @@ -435,6 +445,18 @@ private void verifyIfAnyParentHasIndexesAndViewExtendsPk(PTable parentToBe,
}
}

/**
* Returns true if the {@code CREATE VIEW} statement has a {@code TTL} property set.
*/
private boolean hasTTLProperty(CreateTableStatement create) {
for (Pair<String, Object> prop : create.getProps().values()) {
if (PhoenixDatabaseMetaData.TTL.equalsIgnoreCase(prop.getFirst())) {
return true;
}
}
return false;
}

/**
* Validate View creation compilation. 1. If view creation syntax does not specify primary key,
* the method throws SQLException with PRIMARY_KEY_MISSING code. 2. If parent table does not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,12 +498,28 @@ public static byte[] getRowKeyMatcher(final StatementContext context,
PName tenantId = context.getConnection().getTenantId();
boolean isMultiTenant = tenantId != null && parentTable.isMultiTenant();

byte[] tenantIdBytes = tenantId == null
? ByteUtil.EMPTY_BYTE_ARRAY
: ScanUtil.getTenantIdBytes(schema, isSalted, tenantId, isMultiTenant, false);
// Gracefully handle tenant-id encoding failures (e.g., tenant-id type mismatch)
// so that view creation is not blocked; the view will simply have no ROW_KEY_MATCHER.
byte[] tenantIdBytes;
try {
tenantIdBytes = tenantId == null
? ByteUtil.EMPTY_BYTE_ARRAY
: ScanUtil.getTenantIdBytes(schema, isSalted, tenantId, isMultiTenant, false);
} catch (SQLException e) {
tenantIdBytes = ByteUtil.EMPTY_BYTE_ARRAY;
}
if (tenantIdBytes.length != 0) {
rowKeySlotRangesList.add(Arrays.asList(KeyRange.POINT.apply(tenantIdBytes)));
}
// For tenant views without a WHERE clause, return the tenant-id bytes as the
// ROW_KEY_MATCHER so that CompactionScanner can match rows to this view.
if (viewWhereExpression == null) {
if (rowKeySlotRangesList.isEmpty()) {
return ByteUtil.EMPTY_BYTE_ARRAY;
}
ScanRanges scanRange = ScanRanges.createSingleSpan(schema, rowKeySlotRangesList, null, false);
return scanRange.getScanRange().getLowerRange();
}
KeyExpressionVisitor visitor = new KeyExpressionVisitor(context, parentTable);
KeyExpressionVisitor.KeySlots keySlots = viewWhereExpression.accept(visitor);
if (keySlots == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@ public SQLException newException(SQLExceptionInfo info) {
"CDC on this table is either enabled or is in the process of being enabled."),
CANNOT_SET_OR_ALTER_MAX_LOOKBACK_FOR_INDEX(10964, "44A46",
"Cannot set or alter " + PHOENIX_MAX_LOOKBACK_AGE_CONF_KEY + " on an index"),
TENANT_VIEW_WITHOUT_WHERE_TTL_CONFLICT(10965, "44A47",
"On a multi-tenant base table, among a tenant's views without a WHERE clause we allow "
+ "either any number of such views without TTL, or exactly one such view with TTL. "
+ "The requested operation would violate this rule."),

/** Sequence related */
SEQUENCE_ALREADY_EXIST(1200, "42Z00", "Sequence already exists.", new Factory() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6620,6 +6620,24 @@ private boolean evaluateStmtProperties(MetaProperties metaProperties,
boolean isStrictTTL =
metaProperties.isStrictTTL() != null ? metaProperties.isStrictTTL : table.isStrictTTL();
newTTL.validateTTLOnAlter(connection, table, isStrictTTL);
// For a no-WHERE tenant view on a multi-tenant base table, prevent setting TTL when any
// other no-WHERE sibling view exists for the same tenant (to avoid ROW_KEY_MATCHER
// conflicts in the compaction trie).
String currentViewStmt = table.getViewStatement();
boolean currentIsNoWhere = currentViewStmt == null || currentViewStmt.isEmpty();
if (
!newTTL.equals(TTL_EXPRESSION_NOT_DEFINED) && table.getType() == PTableType.VIEW
&& table.getTenantId() != null && currentIsNoWhere
) {
PTable parent = resolveParentTable(table);
if (parent != null) {
String selfFullName = SchemaUtil.getTableName(
table.getSchemaName() == null ? null : table.getSchemaName().getString(),
table.getTableName().getString());
ViewUtil.validateTenantViewWithoutWhereTTLCoexistence(connection, parent, true,
selfFullName);
}
}
metaPropertiesEvaluated.setTTL(getCompatibleTTLExpression(metaProperties.getTTL(),
table.getType(), table.getViewType(), table.getName().toString()));
changingPhoenixTableProperty = true;
Expand Down Expand Up @@ -6669,6 +6687,27 @@ private boolean evaluateStmtProperties(MetaProperties metaProperties,
return changingPhoenixTableProperty;
}

/**
* Resolves the immediate parent {@link PTable} of the given view, or {@code null} if it cannot be
* resolved.
*/
private PTable resolveParentTable(PTable view) {
PName parentName = view.getParentTableName();
if (parentName == null) {
return null;
}
PName parentSchema = view.getParentSchemaName();
String parentFullName = SchemaUtil
.getTableName(parentSchema == null ? null : parentSchema.getString(), parentName.getString());
try {
return connection.getTable(parentFullName);
} catch (TableNotFoundException e) {
return null;
} catch (SQLException e) {
return null;
}
}

public static class MetaProperties {
private Boolean isImmutableRowsProp = null;
private Boolean multiTenantProp = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import static org.apache.phoenix.jdbc.PhoenixDatabaseMetaData.TABLE_FAMILY_BYTES;
import static org.apache.phoenix.jdbc.PhoenixDatabaseMetaData.TABLE_TYPE_BYTES;
import static org.apache.phoenix.jdbc.PhoenixDatabaseMetaData.TTL_BYTES;
import static org.apache.phoenix.schema.LiteralTTLExpression.TTL_EXPRESSION_NOT_DEFINED;
import static org.apache.phoenix.schema.PTableImpl.getColumnsToClone;
import static org.apache.phoenix.util.PhoenixRuntime.CURRENT_SCN_ATTRIB;
import static org.apache.phoenix.util.PhoenixRuntime.TENANT_ID_ATTRIB;
Expand Down Expand Up @@ -67,11 +68,14 @@
import org.apache.phoenix.coprocessorclient.MetaDataProtocol;
import org.apache.phoenix.coprocessorclient.TableInfo;
import org.apache.phoenix.coprocessorclient.WhereConstantParser;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.exception.SQLExceptionInfo;
import org.apache.phoenix.index.IndexMaintainer;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData;
import org.apache.phoenix.parse.ParseNode;
import org.apache.phoenix.parse.SQLParser;
import org.apache.phoenix.query.ConnectionlessQueryServicesImpl;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.schema.ColumnNotFoundException;
import org.apache.phoenix.schema.PColumn;
Expand Down Expand Up @@ -432,6 +436,94 @@ public static TableViewFinderResult findChildViews(PhoenixConnection connection,
return childViewsResult;
}

/**
* Validates tenant TTL view coexistence rules for NO-WHERE tenant views on a multi-tenant base
* table. For a given tenant on such a base table, among that tenant's no-WHERE views we allow
* EITHER any number of no-WHERE views without TTL, OR exactly one no-WHERE view with TTL.
* Multiple no-WHERE TTL views share the same ROW_KEY_MATCHER (tenant-id bytes) and conflict in
* the compaction RowKeyMatcher trie. Views with WHERE clauses are outside the scope of this check
* and are ignored (prefix conflicts involving WHERE-scoped matchers are a separate pre-existing
* concern).
* <p>
* This is a no-op when:
* <ul>
* <li>the connection has no tenant id,</li>
* <li>query services are connectionless (unit test mode),</li>
* <li>the parent is not a multi-tenant base {@link PTableType#TABLE}.</li>
* </ul>
* @param connection phoenix connection (must be a tenant connection for the check to
* fire)
* @param parent the parent table the view is being created/altered against
* @param newViewHasTTL true if the view being created or altered will have TTL
* @param viewToExcludeFullName full name of the view to skip from sibling iteration (used on the
* ALTER path to exclude the view being altered itself); pass
* {@code null} on the CREATE path
* @throws SQLException {@link SQLExceptionCode#TENANT_VIEW_WITHOUT_WHERE_TTL_CONFLICT} if the
* operation would create a TTL / non-TTL coexistence conflict among no-WHERE
* tenant views
*/
public static void validateTenantViewWithoutWhereTTLCoexistence(PhoenixConnection connection,
PTable parent, boolean newViewHasTTL, String viewToExcludeFullName) throws SQLException {
if (connection.getTenantId() == null) {
return;
}
if (connection.getQueryServices() instanceof ConnectionlessQueryServicesImpl) {
return;
}
if (parent.getType() != PTableType.TABLE || !parent.isMultiTenant()) {
return;
}
byte[] myTenantIdBytes = connection.getTenantId().getBytes();
TableViewFinderResult childViews;
try {
childViews = findChildViews(connection,
parent.getTenantId() == null ? null : parent.getTenantId().getString(),
parent.getSchemaName() == null ? null : parent.getSchemaName().getString(),
parent.getTableName().getString());
} catch (IOException e) {
// CHILD_LINK may be unavailable (namespace mapping edge cases, partial setup, etc.).
// Skip validation rather than fail the DDL.
return;
}
boolean foundAnyNoWhere = false;
for (TableInfo info : childViews.getLinks()) {
if (!Arrays.equals(info.getTenantId(), myTenantIdBytes)) {
continue;
}
String childFullName = SchemaUtil.getTableName(
info.getSchemaName() == null ? null : Bytes.toString(info.getSchemaName()),
Bytes.toString(info.getTableName()));
if (childFullName.equals(viewToExcludeFullName)) {
continue;
}
try {
PTable existing = connection.getTable(childFullName);
String existingViewStmt = existing.getViewStatement();
boolean existingIsNoWhere = existingViewStmt == null || existingViewStmt.isEmpty();
if (!existingIsNoWhere) {
// Views with WHERE clauses are outside the scope of this check.
continue;
}
foundAnyNoWhere = true;
boolean existingHasTTL = existing.getTTLExpression() != null
&& !existing.getTTLExpression().equals(TTL_EXPRESSION_NOT_DEFINED);
if (existingHasTTL) {
// An existing no-WHERE TTL view blocks any additional no-WHERE view (TTL or not).
throw new SQLExceptionInfo.Builder(
SQLExceptionCode.TENANT_VIEW_WITHOUT_WHERE_TTL_CONFLICT).build().buildException();
}
} catch (TableNotFoundException e) {
// Orphan child link, ignore.
}
}
if (newViewHasTTL && foundAnyNoWhere) {
// The new / altered no-WHERE view has TTL but sibling no-WHERE views already exist; the TTL
// view's trie entry would silently apply to the existing views' rows.
throw new SQLExceptionInfo.Builder(SQLExceptionCode.TENANT_VIEW_WITHOUT_WHERE_TTL_CONFLICT)
.build().buildException();
}
}

/**
* Check metadata to find if a given table/view has any immediate child views. Note that this is
* not resilient to orphan {@code parent->child } links.
Expand Down
Loading