This is an automated email from the ASF dual-hosted git repository.
vjasani 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 a02a90520b PHOENIX-4555 Only mark view as updatable if rows cannot
overlap with other updatable views (#1844)
a02a90520b is described below
commit a02a90520b37eccdfcd26e1dc439946eb4e5e01c
Author: Jing Yu <[email protected]>
AuthorDate: Wed Aug 14 12:21:33 2024 -0700
PHOENIX-4555 Only mark view as updatable if rows cannot overlap with other
updatable views (#1844)
---
.../phoenix/compile/CreateTableCompiler.java | 283 ++++++++-
.../org/apache/phoenix/query/QueryServices.java | 9 +
.../end2end/UpdatableViewRestrictionsIT.java | 682 +++++++++++++++++++++
3 files changed, 973 insertions(+), 1 deletion(-)
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/CreateTableCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/CreateTableCompiler.java
index bcc672b224..ccda4659c4 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/CreateTableCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/CreateTableCompiler.java
@@ -22,10 +22,15 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.CompareOperator;
+import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
@@ -54,9 +59,12 @@ import org.apache.phoenix.parse.PrimaryKeyConstraint;
import org.apache.phoenix.parse.SQLParser;
import org.apache.phoenix.parse.SelectStatement;
import org.apache.phoenix.parse.TableName;
+import org.apache.phoenix.query.ConnectionQueryServices;
+import org.apache.phoenix.query.ConnectionlessQueryServicesImpl;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.schema.ColumnRef;
import org.apache.phoenix.schema.MetaDataClient;
+import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.PDatum;
import org.apache.phoenix.schema.PName;
import org.apache.phoenix.schema.PTable;
@@ -72,9 +80,18 @@ import org.apache.phoenix.util.ByteUtil;
import org.apache.phoenix.util.MetaDataUtil;
import org.apache.phoenix.util.QueryUtil;
import org.apache.phoenix.util.SchemaUtil;
+import org.apache.phoenix.util.ViewUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static
org.apache.phoenix.jdbc.PhoenixDatabaseMetaData.SYSTEM_CHILD_LINK_NAMESPACE_BYTES;
+import static
org.apache.phoenix.jdbc.PhoenixDatabaseMetaData.SYSTEM_CHILD_LINK_NAME_BYTES;
+import static
org.apache.phoenix.query.QueryServices.DEFAULT_PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED;
+import static
org.apache.phoenix.query.QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED;
public class CreateTableCompiler {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(CreateTableCompiler.class);
private static final PDatum VARBINARY_DATUM = new VarbinaryDatum();
private final PhoenixStatement statement;
private final Operation operation;
@@ -97,6 +114,8 @@ public class CreateTableCompiler {
String viewStatementToBe = null;
byte[][] viewColumnConstantsToBe = null;
BitSet isViewColumnReferencedToBe = null;
+ Set<PColumn> pkColumnsInWhere = new HashSet<>();
+ Set<PColumn> nonPkColumnsInWhere = new HashSet<>();
byte[] rowKeyMatcher = ByteUtil.EMPTY_BYTE_ARRAY;
// Check whether column families having local index column family
suffix or not if present
@@ -172,9 +191,34 @@ public class CreateTableCompiler {
viewColumnConstantsToBe = new byte[nColumns][];
ViewWhereExpressionVisitor visitor = new
ViewWhereExpressionVisitor(parentToBe, viewColumnConstantsToBe);
where.accept(visitor);
+
+ viewTypeToBe = visitor.isUpdatable() ? ViewType.UPDATABLE
: ViewType.READ_ONLY;
+ boolean updatableViewRestrictionEnabled =
connection.getQueryServices()
+
.getProps().getBoolean(PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
+
DEFAULT_PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED);
+ if (viewTypeToBe == ViewType.UPDATABLE &&
updatableViewRestrictionEnabled) {
+ ViewWhereExpressionValidatorVisitor validatorVisitor =
+ new
ViewWhereExpressionValidatorVisitor(parentToBe,
+ pkColumnsInWhere, nonPkColumnsInWhere);
+ where.accept(validatorVisitor);
+ if (!(connection.getQueryServices()
+ instanceof ConnectionlessQueryServicesImpl)) {
+ try {
+ viewTypeToBe = setViewTypeToBe(connection,
parentToBe,
+ pkColumnsInWhere, nonPkColumnsInWhere);
+ LOGGER.info("VIEW type is set to {}. View
Statement: {}, " +
+ "View Name: {}, " +
+ "Parent Table/View Name: {}",
+ viewTypeToBe, viewStatementToBe,
+ create.getTableName(),
parentToBe.getName());
+ } catch (IOException e) {
+ throw new SQLException(e);
+ }
+ }
+ }
+
// If view is not updatable, viewColumnConstants should be
empty. We will still
// inherit our parent viewConstants, but we have no
additional ones.
- viewTypeToBe = visitor.isUpdatable() ? ViewType.UPDATABLE
: ViewType.READ_ONLY;
if (viewTypeToBe != ViewType.UPDATABLE) {
viewColumnConstantsToBe = null;
}
@@ -221,6 +265,150 @@ public class CreateTableCompiler {
viewColumnConstants, isViewColumnReferenced, connection);
}
+ /**
+ * Restrict view to be UPDATABLE if the view specification:
+ * 1. uses only the PK columns;
+ * 2. starts from the first PK column (ignore the prefix PK columns,
TENANT_ID and/or
+ * _SALTED, if the parent table is multi-tenant and/or salted);
+ * 3. PK columns should be in the order they are defined;
+ * 4. uses the same set of PK columns as its sibling views' specification;
+ * Otherwise, mark the view as READ_ONLY.
+ *
+ * @param connection The client connection
+ * @param parentToBe To be parent for given view
+ * @param pkColumnsInWhere Set of primary key in where clause
+ * @param nonPkColumnsInWhere Set of non-primary key columns in where
clause
+ * @throws IOException thrown if there is an error finding sibling views
+ * @throws SQLException
+ */
+ private ViewType setViewTypeToBe(final PhoenixConnection connection, final
PTable parentToBe,
+ final Set<PColumn> pkColumnsInWhere,
+ final Set<PColumn> nonPkColumnsInWhere)
+ throws IOException, SQLException {
+ // 1. Check the view specification WHERE clause uses only the PK
columns
+ if (!nonPkColumnsInWhere.isEmpty()) {
+ LOGGER.info("Setting the view type as READ_ONLY because the view
statement contains " +
+ "non-PK columns: {}", nonPkColumnsInWhere);
+ return ViewType.READ_ONLY;
+ }
+ if (pkColumnsInWhere.isEmpty()) {
+ return ViewType.UPDATABLE;
+ }
+
+ // 2. Check the WHERE clause starts from the first PK column (ignore
the prefix PK
+ // columns, TENANT_ID and/or _SALTED, if the parent table is
multi-tenant and/or salted)
+ List<Integer> tablePkPositions = new ArrayList<>();
+ List<Integer> viewPkPositions = new ArrayList<>();
+ List<PColumn> tablePkColumns = parentToBe.getPKColumns();
+ tablePkColumns.forEach(tablePkColumn ->
+ tablePkPositions.add(tablePkColumn.getPosition()));
+ pkColumnsInWhere.forEach(pkColumn ->
viewPkPositions.add(pkColumn.getPosition()));
+ Collections.sort(viewPkPositions);
+ int tablePkStartIdx = 0;
+ if (parentToBe.isMultiTenant()) {
+ tablePkStartIdx++;
+ }
+ if (parentToBe.getBucketNum() != null) {
+ tablePkStartIdx++;
+ }
+ if (!Objects.equals(viewPkPositions.get(0),
tablePkPositions.get(tablePkStartIdx))) {
+ LOGGER.info("Setting the view type as READ_ONLY because the view
statement WHERE " +
+ "clause does not start from the first PK column (ignore
the prefix PKs " +
+ "if the parent table is multi-tenant and/or salted). View
PK Columns: " +
+ "{}, Table PK Columns: {}", pkColumnsInWhere,
tablePkColumns);
+ return ViewType.READ_ONLY;
+ }
+
+ // 3. Check PK columns are in the order they are defined
+ if (!isPkColumnsInOrder(viewPkPositions, tablePkPositions,
tablePkStartIdx)) {
+ LOGGER.info("Setting the view type as READ_ONLY because the PK
columns is not in the " +
+ "order they are defined. View PK Columns: {}, Table PK
Columns: {}",
+ pkColumnsInWhere, tablePkColumns);
+ return ViewType.READ_ONLY;
+ }
+
+ // 4. Check the view specification has the same set of PK column(s) as
its sibling view
+ byte[] parentTenantIdInBytes = parentToBe.getTenantId() != null
+ ? parentToBe.getTenantId().getBytes() : null;
+ byte[] parentSchemaNameInBytes = parentToBe.getSchemaName() != null
+ ? parentToBe.getSchemaName().getBytes() : null;
+ ConnectionQueryServices queryServices = connection.getQueryServices();
+ Configuration config = queryServices.getConfiguration();
+ byte[] systemChildLinkTable =
SchemaUtil.isNamespaceMappingEnabled(null, config)
+ ? SYSTEM_CHILD_LINK_NAMESPACE_BYTES
+ : SYSTEM_CHILD_LINK_NAME_BYTES;
+ try (Table childLinkTable =
queryServices.getTable(systemChildLinkTable)) {
+ List<PTable> legitimateSiblingViewList =
+ ViewUtil.findAllDescendantViews(childLinkTable, config,
parentTenantIdInBytes,
+ parentSchemaNameInBytes,
parentToBe.getTableName().getBytes(),
+ HConstants.LATEST_TIMESTAMP, true).getFirst();
+ if (!legitimateSiblingViewList.isEmpty()) {
+ PTable siblingView = legitimateSiblingViewList.get(0);
+ Expression siblingViewWhere = getWhereFromView(connection,
siblingView);
+ Set<PColumn> siblingViewPkColsInWhere = new HashSet<>();
+ if (siblingViewWhere != null) {
+ ViewWhereExpressionValidatorVisitor
siblingViewValidatorVisitor =
+ new ViewWhereExpressionValidatorVisitor(parentToBe,
+ siblingViewPkColsInWhere, null);
+ siblingViewWhere.accept(siblingViewValidatorVisitor);
+ }
+ if (!pkColumnsInWhere.equals(siblingViewPkColsInWhere)) {
+ LOGGER.info("Setting the view type as READ_ONLY because
its set of PK " +
+ "columns is different from its sibling
view {}'s. View PK " +
+ "Columns: {}, Sibling View PK Columns: {}",
+ siblingView.getName(), pkColumnsInWhere,
siblingViewPkColsInWhere);
+ return ViewType.READ_ONLY;
+ }
+ }
+ }
+ return ViewType.UPDATABLE;
+ }
+
+ /**
+ * Get the where Expression of given view.
+ * @param connection The client connection
+ * @param view PTable of the view
+ * @return A where Expression
+ * @throws SQLException
+ */
+ private Expression getWhereFromView(final PhoenixConnection connection,
final PTable view)
+ throws SQLException {
+ String viewStatement = view.getViewStatement();
+ if (viewStatement == null) {
+ return null;
+ }
+ SelectStatement select = new SQLParser(viewStatement).parseQuery();
+ ColumnResolver resolver = FromCompiler.getResolverForQuery(select,
connection);
+ StatementContext context = new StatementContext(new
PhoenixStatement(connection), resolver);
+ BitSet isViewColumnReferencedToBe = new
BitSet(view.getColumns().size());
+ ExpressionCompiler expressionCompiler = new
ColumnTrackingExpressionCompiler(context,
+ isViewColumnReferencedToBe);
+ ParseNode whereNode = select.getWhere();
+ return whereNode.accept(expressionCompiler);
+ }
+
+ /**
+ * Check if the primary key columns are in order (consecutive in position)
as they are
+ * defined, providing their positions list
+ * @param viewPkPositions A positions list of view PK columns to be checked
+ * @param tablePkPositions The positions list of the table's PK columns to
be compared
+ * @param tablePkStartIdx The start index of table PK position, depending
on whether the
+ * table is multi-tenant and/or salted
+ * @return true if the PK columns are in order, otherwise false
+ */
+ private boolean isPkColumnsInOrder(final List<Integer> viewPkPositions,
+ final List<Integer> tablePkPositions,
+ final int tablePkStartIdx) {
+ for (int i = 1; i < viewPkPositions.size(); i++) {
+ if (!Objects.equals(
+ viewPkPositions.get(i),
+ tablePkPositions.get(tablePkStartIdx + i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* If any of the parent table/view has indexes in the parent hierarchy,
and the current
* view under creation extends the primary key of the parent, throw error.
@@ -432,6 +620,99 @@ public class CreateTableCompiler {
}
}
+
+ /**
+ * Visitor for view's where expression, which updates primary key columns
and non-primary key
+ * columns for validating if the view is updatable
+ */
+ public static class ViewWhereExpressionValidatorVisitor extends
+ StatelessTraverseNoExpressionVisitor<Boolean> {
+ private boolean isUpdatable = true;
+ private final PTable table;
+ private final Set<PColumn> pkColumns;
+ private final Set<PColumn> nonPKColumns;
+
+ public ViewWhereExpressionValidatorVisitor(PTable table, Set<PColumn>
pkColumns,
+ Set<PColumn> nonPKColumns)
{
+ this.table = table;
+ this.pkColumns = pkColumns;
+ this.nonPKColumns = nonPKColumns;
+ }
+
+ public boolean isUpdatable() {
+ return isUpdatable;
+ }
+
+ @Override
+ public Boolean defaultReturn(Expression node, List<Boolean> l) {
+ // We only hit this if we're trying to traverse somewhere
+ // in which we don't have a visitLeave that returns non null
+ isUpdatable = false;
+ return null;
+ }
+
+ @Override
+ public Iterator<Expression> visitEnter(AndExpression node) {
+ return node.getChildren().iterator();
+ }
+
+ @Override
+ public Boolean visitLeave(AndExpression node, List<Boolean> l) {
+ return l.isEmpty() ? null : Boolean.TRUE;
+ }
+
+ @Override
+ public Iterator<Expression> visitEnter(ComparisonExpression node) {
+ if (node.getFilterOp() == CompareOperator.EQUAL
+ && node.getChildren().get(1).isStateless()
+ && node.getChildren().get(1).getDeterminism() ==
Determinism.ALWAYS) {
+ return Iterators.singletonIterator(node.getChildren().get(0));
+ }
+ return super.visitEnter(node);
+ }
+
+ @Override
+ public Boolean visitLeave(ComparisonExpression node, List<Boolean> l) {
+ return l.isEmpty() ? null : Boolean.TRUE;
+ }
+
+ @Override
+ public Iterator<Expression> visitEnter(IsNullExpression node) {
+ return node.isNegate() ? super.visitEnter(node) :
node.getChildren().iterator();
+ }
+
+ @Override
+ public Boolean visitLeave(IsNullExpression node, List<Boolean> l) {
+ // Nothing to do as we've already set the position to an empty
byte array
+ return l.isEmpty() ? null : Boolean.TRUE;
+ }
+
+ @Override
+ public Boolean visit(RowKeyColumnExpression node) {
+ pkColumns.add(table.getPKColumns().get(node.getPosition()));
+ return Boolean.TRUE;
+ }
+
+ @Override
+ public Boolean visit(KeyValueColumnExpression node) {
+ try {
+ if (nonPKColumns != null) {
+ nonPKColumns.add(
+ table.getColumnFamily(node.getColumnFamily())
+
.getPColumnForColumnQualifier(node.getColumnQualifier()));
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e); // Impossible
+ }
+ return Boolean.TRUE;
+ }
+
+ @Override
+ public Boolean visit(SingleCellColumnExpression node) {
+ return visit(node.getKeyValueExpression());
+ }
+ }
+
private static class VarbinaryDatum implements PDatum {
@Override
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServices.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServices.java
index 1a42cf5d3b..24c72521d8 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServices.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/QueryServices.java
@@ -472,6 +472,15 @@ public interface QueryServices extends SQLCloseable {
boolean DEFAULT_DISABLE_VIEW_SUBTREE_VALIDATION = false;
+ /**
+ * Param to enable updatable view restriction that only mark view as
updatable if rows
+ * cannot overlap with other updatable views.
+ */
+ String PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED =
+ "phoenix.updatable.view.restriction.enabled";
+
+ boolean DEFAULT_PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED = false;
+
/**
* Only used by tests: parameter to determine num of regionservers to be
created by
* MiniHBaseCluster.
diff --git
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UpdatableViewRestrictionsIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UpdatableViewRestrictionsIT.java
new file mode 100644
index 0000000000..b90ccb2d38
--- /dev/null
+++
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UpdatableViewRestrictionsIT.java
@@ -0,0 +1,682 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.phoenix.end2end;
+
+import org.apache.phoenix.query.QueryServices;
+import org.apache.phoenix.schema.ReadOnlyTableException;
+import org.apache.phoenix.schema.TableProperty;
+import org.apache.phoenix.thirdparty.com.google.common.collect.Maps;
+import org.apache.phoenix.util.PhoenixRuntime;
+import org.apache.phoenix.util.ReadOnlyProps;
+import org.apache.phoenix.util.SchemaUtil;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import static
org.apache.phoenix.coprocessor.PhoenixMetaDataCoprocessorHost.PHOENIX_META_DATA_COPROCESSOR_CONF_KEY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for restrictions associated with updatable view.
+ */
+@Category(NeedsOwnMiniClusterTest.class)
+public class UpdatableViewRestrictionsIT extends SplitSystemCatalogIT {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(UpdatableViewRestrictionsIT.class);
+
+ @BeforeClass
+ public static synchronized void doSetup() throws Exception {
+ NUM_SLAVES_BASE = 6;
+ boolean splitSystemCatalog = (driver == null);
+ Map<String, String> serverProps = Maps.newHashMapWithExpectedSize(1);
+ serverProps.put(QueryServices.PHOENIX_ACLS_ENABLED, "true");
+ serverProps.put(PHOENIX_META_DATA_COPROCESSOR_CONF_KEY,
+ ViewConcurrencyAndFailureIT.TestMetaDataRegionObserver.class
+ .getName());
+ serverProps.put("hbase.coprocessor.abortonerror", "false");
+ setUpTestDriver(new ReadOnlyProps(serverProps.entrySet().iterator()),
+ ReadOnlyProps.EMPTY_PROPS);
+ // Split SYSTEM.CATALOG once after the mini-cluster is started
+ if (splitSystemCatalog) {
+ // splitSystemCatalog is incompatible with the balancer chore
+ getUtility().getHBaseCluster().getMaster().balanceSwitch(false);
+ splitSystemCatalog();
+ }
+ }
+
+ private void createTable(
+ Connection conn, String tableSQL, Map<String, Object> tableProps)
throws Exception {
+ List<String> props = new ArrayList<>();
+ Boolean multitenant = (Boolean)
TableProperty.MULTI_TENANT.getValue(tableProps);
+ if (multitenant != null && multitenant) {
+ props.add(TableProperty.MULTI_TENANT.getPropertyName() + "=" +
multitenant);
+ }
+ Integer nSaltBuckets = (Integer)
TableProperty.SALT_BUCKETS.getValue(tableProps);
+ if (nSaltBuckets != null) {
+ props.add(TableProperty.SALT_BUCKETS.getPropertyName() + "=" +
nSaltBuckets);
+ }
+ tableSQL += " " + String.join(", ", props);
+ LOGGER.debug("Creating table with SQL: " + tableSQL);
+ conn.createStatement().execute(tableSQL);
+ }
+
+ private void createTable(Connection conn, String tableName,
+ boolean multitenant, Integer nSaltBuckets) throws
Exception {
+ String tableSQL = "CREATE TABLE " + tableName + " ("
+ + (multitenant ? "TENANT_ID VARCHAR NOT NULL, " : "")
+ + "k1 INTEGER NOT NULL, k2 DECIMAL, k3 INTEGER NOT NULL, s
VARCHAR "
+ + "CONSTRAINT pk PRIMARY KEY ("
+ + (multitenant ? "TENANT_ID, " : "")
+ + "k1, k2, k3))";
+ createTable(conn, tableSQL, new HashMap<String, Object>() {{
+ put(TableProperty.MULTI_TENANT.getPropertyName(), multitenant);
+ put(TableProperty.SALT_BUCKETS.getPropertyName(), nSaltBuckets);
+ }});
+ }
+
+ private void createTable(Connection conn, String tableName) throws
Exception {
+ createTable(conn, tableName, false, null);
+ }
+
+ private Connection getTenantConnection(final String tenantId) throws
Exception {
+ Properties tenantProps = new Properties();
+ tenantProps.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
+
tenantProps.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ return DriverManager.getConnection(getUrl(), tenantProps);
+ }
+
+ private void verifyNumberOfRows(String tableName, String tenantId, int
expectedRows,
+ Connection conn) throws Exception {
+ String query = "SELECT COUNT(*) FROM " + tableName;
+ if (tenantId != null) {
+ query = query + " WHERE TENANT_ID = '" + tenantId + "'";
+ }
+ try (Statement stm = conn.createStatement()) {
+ ResultSet rs = stm.executeQuery(query);
+ assertTrue(rs.next());
+ assertEquals(expectedRows, rs.getInt(1));
+ }
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if there are non-PK columns in the
WHERE clause.
+ */
+ @Test
+ public void testReadOnlyViewWithNonPkInWhere() throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ conn.setAutoCommit(true);
+ createTable(conn, fullTableName);
+
+ Statement stmt = conn.createStatement();
+ String viewDDL = "CREATE VIEW " + fullViewName + " AS SELECT *
FROM " + fullTableName +
+ " WHERE s = 'a'";
+ stmt.execute(viewDDL);
+ try {
+ stmt.execute("UPSERT INTO " + fullViewName + " VALUES(1, 2, 3,
'a')");
+ fail();
+ } catch (ReadOnlyTableException ignored) {
+ }
+ }
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if PK columns in the WHERE clause
are not in the order
+ * they are defined: primary key k2 is missing in this case.
+ */
+ @Test
+ public void testReadOnlyViewWithPkNotInOrderInWhere1() throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ conn.setAutoCommit(true);
+ createTable(conn, fullTableName);
+
+ Statement stmt = conn.createStatement();
+ String viewDDL = "CREATE VIEW " + fullViewName + " AS SELECT *
FROM " + fullTableName +
+ " WHERE k1 = 1 AND k3 = 3";
+ stmt.execute(viewDDL);
+ try {
+ stmt.execute("UPSERT INTO " + fullViewName + " VALUES(1, 2, 3,
'a')");
+ fail();
+ } catch (ReadOnlyTableException ignored) {
+ }
+ }
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if PK columns in the WHERE clause
are not in the order
+ * they are defined: primary key k3 is missing in this case.
+ */
+ @Test
+ public void testReadOnlyViewWithPkNotInOrderInWhere2() throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ conn.setAutoCommit(true);
+ conn.createStatement().execute("CREATE TABLE " + fullTableName
+ + " (k1 INTEGER NOT NULL, k2 DECIMAL, k3 INTEGER NOT NULL,
s VARCHAR "
+ + "CONSTRAINT pk PRIMARY KEY (k1, k2, k3, s))");
+
+ Statement stmt = conn.createStatement();
+ String viewDDL = "CREATE VIEW " + fullViewName + " AS SELECT *
FROM " + fullTableName +
+ " WHERE k1 = 1 AND k2 = 2 AND s = 'a'";
+ stmt.execute(viewDDL);
+ try {
+ stmt.execute("UPSERT INTO " + fullViewName + " VALUES(1, 2, 3,
'a')");
+ fail();
+ } catch (ReadOnlyTableException ignored) {
+ }
+ }
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if the set of PK columns in the
WHERE clause are not
+ * same as its sibling view's: primary key k3 is redundant in this case.
+ */
+ @Test
+ public void testReadOnlyViewWithPkNotSameInWhere1() throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullGlobalViewName = SchemaUtil.getTableName(SCHEMA2,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+ String fullSiblingViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ conn.setAutoCommit(true);
+ createTable(conn, fullTableName);
+
+ Statement stmt = conn.createStatement();
+ String globalViewDDL =
+ "CREATE VIEW " + fullGlobalViewName + " AS SELECT * FROM "
+ fullTableName +
+ " WHERE k1 = 1";
+ stmt.execute(globalViewDDL);
+
+ String siblingViewDDL = "CREATE VIEW " + fullSiblingViewName + "
AS SELECT * FROM " +
+ fullGlobalViewName + " WHERE k2 = 1";
+ stmt.execute(siblingViewDDL);
+ String viewDDL =
+ "CREATE VIEW " + fullViewName + " AS SELECT * FROM " +
fullGlobalViewName
+ + " WHERE k2 = 2 AND k3 = 109";
+ stmt.execute(viewDDL);
+ try {
+ stmt.execute("UPSERT INTO " + fullViewName + " VALUES(1, 2,
109, 'a')");
+ fail();
+ } catch (ReadOnlyTableException ignored) {
+ }
+ }
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if the set of PK columns in the
WHERE clause are not
+ * same as its sibling view's: primary key k3 is missing in this case.
+ */
+ @Test
+ public void testReadOnlyViewWithPkNotSameInWhere2() throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullGlobalViewName = SchemaUtil.getTableName(SCHEMA2,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+ String fullSiblingViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ conn.setAutoCommit(true);
+ createTable(conn, fullTableName);
+
+ Statement stmt = conn.createStatement();
+ String globalViewDDL =
+ "CREATE VIEW " + fullGlobalViewName + " AS SELECT * FROM "
+ fullTableName +
+ " WHERE k1 = 1";
+ stmt.execute(globalViewDDL);
+
+ String siblingViewDDL = "CREATE VIEW " + fullSiblingViewName + "
AS SELECT * FROM " +
+ fullGlobalViewName + " WHERE k2 = 1 AND k3 = 109";
+ stmt.execute(siblingViewDDL);
+ String viewDDL =
+ "CREATE VIEW " + fullViewName + " AS SELECT * FROM " +
fullGlobalViewName
+ + " WHERE k2 = 2";
+ stmt.execute(viewDDL);
+ try {
+ stmt.execute("UPSERT INTO " + fullViewName + " VALUES(1, 2,
109, 'a')");
+ fail();
+ } catch (ReadOnlyTableException ignored) {
+ }
+ }
+ }
+
+ /**
+ * Test that the view type is UPDATABLE if the view statement WHERE clause
+ * starts from the first PK column (ignore the prefix PK columns,
TENANT_ID and/or _SALTED,
+ * if the parent table is multi-tenant and/or salted),
+ * and satisfies all other criteria.
+ * @param multitenant Whether the parent table is multi-tenant
+ * @param nSaltBuckets Number of salt buckets
+ * @throws Exception
+ */
+ private void testUpdatableViewStartFromFirstPK(boolean multitenant,
Integer nSaltBuckets) throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ conn.setAutoCommit(true);
+ createTable(conn, fullTableName, multitenant, nSaltBuckets);
+ String tenantId = null;
+ if (multitenant) {
+ tenantId = TENANT1;
+ try (Connection tenantConn = getTenantConnection(tenantId)) {
+ createAndVerifyUpdatableView(fullTableName, tenantConn);
+ }
+ } else {
+ createAndVerifyUpdatableView(fullTableName, conn);
+ }
+ verifyNumberOfRows(fullTableName, tenantId, 1, conn);
+ }
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if the view statement WHERE clause
+ * does not start from the first PK column (ignore the prefix PK columns,
TENANT_ID and/or
+ * _SALTED, if the parent table is multi-tenant and/or salted).
+ * @param multitenant Whether the parent table is multi-tenant
+ * @param nSaltBuckets Number of salt buckets
+ * @throws Exception
+ */
+ private void testReadOnlyViewNotStartFromFirstPK(boolean multitenant,
Integer nSaltBuckets) throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA2,
generateUniqueName());
+ String tenantId = TENANT1;
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ try (Connection globalConn = DriverManager.getConnection(getUrl(),
props);
+ Connection tenantConn = getTenantConnection(tenantId)) {
+ globalConn.setAutoCommit(true);
+ tenantConn.setAutoCommit(true);
+
+ createTable(globalConn, fullTableName, multitenant, nSaltBuckets);
+
+ final Statement stmt = multitenant ? tenantConn.createStatement() :
+ globalConn.createStatement();
+ stmt.execute("CREATE VIEW " + fullViewName + " AS SELECT * FROM " +
+ fullTableName + " WHERE k2 = 2");
+ try {
+ stmt.execute(String.format("UPSERT INTO %s VALUES" +
+ "(1, 2, 3, 's')", fullViewName));
+ fail();
+ } catch (ReadOnlyTableException ignored) {
+ }
+ }
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if the view statement WHERE clause
does not start
+ * from the first PK column when the parent table is neither multi-tenant
nor salted.
+ */
+ @Test
+ public void
testReadOnlyViewOnNonMultitenantNonSaltedTableNotStartFromFirstPK() throws
Exception {
+ testReadOnlyViewNotStartFromFirstPK(false, null);
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if the view statement WHERE clause
does not start
+ * from the first PK column when the parent table is multi-tenant and not
salted (ignore the
+ * prefix PK column TENANT_ID).
+ */
+ @Test
+ public void
testReadOnlyViewOnMultitenantNonSaltedTableNotStartFromFirstPK() throws
Exception {
+ testReadOnlyViewNotStartFromFirstPK(true, null);
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if the view statement WHERE clause
does not start
+ * from the first PK column when the parent table is not multi-tenant but
salted (ignore
+ * the prefix PK column _SALTED).
+ */
+ @Test
+ public void
testReadOnlyViewOnNonMultitenantSaltedTableNotStartFromFirstPK() throws
Exception {
+ testReadOnlyViewNotStartFromFirstPK(false, 3);
+ }
+
+ /**
+ * Test that the view type is READ_ONLY if the view statement WHERE clause
does not start
+ * from the first PK column when the parent table is both multi-tenant and
salted (ignore
+ * the prefix PK columns TENANT_ID and _SALTED).
+ */
+ @Test
+ public void testReadOnlyViewOnMultitenantSaltedTableNotStartFromFirstPK()
throws Exception {
+ testReadOnlyViewNotStartFromFirstPK(true, 3);
+ }
+
+ /**
+ * Test that the view type is UPDATABLE if the view statement WHERE clause
+ * starts from the first PK column when the parent table is neither
multi-tenant nor salted,
+ * and satisfies all other criteria.
+ */
+ @Test
+ public void
testUpdatableViewOnNonMultitenantNonSaltedTableStartFromFirstPK() throws
Exception {
+ testUpdatableViewStartFromFirstPK(false, null);
+ }
+
+ /**
+ * Test that the view type is UPDATABLE if the view statement WHERE clause
+ * starts from the first PK column when the parent table is multi-tenant
and not salted
+ * (ignore the prefix PK column TENANT_ID),
+ * and satisfies all other criteria.
+ */
+ @Test
+ public void testUpdatableViewOnMultitenantNonSaltedTableStartFromFirstPK()
throws Exception {
+ testUpdatableViewStartFromFirstPK(true, null);
+ }
+
+ /**
+ * Test that the view type is UPDATABLE if the view statement WHERE clause
+ * starts from the first PK column when the parent table is not
multi-tenant but salted
+ * (ignore the prefix PK column _SALTED),
+ * and satisfies all other criteria.
+ */
+ @Test
+ public void testUpdatableViewOnNonMultitenantSaltedTableStartFromFirstPK()
throws Exception {
+ testUpdatableViewStartFromFirstPK(false, 3);
+ }
+
+ /**
+ * Test that the view type is UPDATABLE if the view statement WHERE clause
+ * starts from the first PK column when the parent table is both
multi-tenant and salted
+ * (ignore the prefix PK columns TENANT_ID and _SALTED),
+ * and satisfies all other criteria.
+ */
+ @Test
+ public void testUpdatableViewOnMultitenantSaltedTableStartFromFirstPK()
throws Exception {
+ testUpdatableViewStartFromFirstPK(true, 3);
+ }
+
+ private void createAndVerifyUpdatableView(
+ String fullTableName, Connection conn) throws Exception {
+ String fullViewName = SchemaUtil.getTableName(SCHEMA2,
generateUniqueName());
+ Statement stmt = conn.createStatement();
+
+ stmt.execute("CREATE VIEW " + fullViewName + " AS SELECT * FROM " +
fullTableName +
+ " WHERE k1 = 1");
+ stmt.execute("UPSERT INTO " + fullViewName + " VALUES(1, 2, 109,
'a')");
+ conn.commit();
+
+ ResultSet rs = stmt.executeQuery("SELECT k1, k2, k3 FROM " +
fullViewName);
+
+ assertTrue(rs.next());
+ assertEquals(1, rs.getInt(1));
+ assertEquals(2, rs.getInt(2));
+ assertEquals(109, rs.getInt(3));
+ assertFalse(rs.next());
+ }
+
+ /**
+ * Test that the view type is READ_ONLY when it's created on an updatable
view and PK columns
+ * in the WHERE clause are not in the order they are defined.
+ */
+ @Test
+ public void testReadOnlyViewOnUpdatableView1() throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA2,
generateUniqueName());
+ String fullChildViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ Connection conn = DriverManager.getConnection(getUrl(), props);
+ conn.setAutoCommit(true);
+
+ createTable(conn, fullTableName);
+ Statement stmt = conn.createStatement();
+
+ String viewDDL = "CREATE VIEW " + fullViewName +
+ " AS SELECT * FROM " + fullTableName + " WHERE k1 = 1";
+ stmt.execute(viewDDL);
+
+ String childViewDDL = "CREATE VIEW " + fullChildViewName +
+ " AS SELECT * FROM " + fullViewName + " WHERE k3 = 3";
+ stmt.execute(childViewDDL);
+
+ try {
+ stmt.execute("UPSERT INTO " + fullChildViewName + " VALUES(1, 2,
3, 'a')");
+ fail();
+ } catch (ReadOnlyTableException ignored) {
+ }
+ }
+
+ /**
+ * Test that the view type is READ_ONLY when it's created on an updatable
view and there are
+ * non-PK columns in the WHERE clause.
+ */
+ @Test
+ public void testReadOnlyViewOnUpdatableView2() throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA2,
generateUniqueName());
+ String fullChildViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ Connection conn = DriverManager.getConnection(getUrl(), props);
+ conn.setAutoCommit(true);
+
+ createTable(conn, fullTableName);
+ Statement stmt = conn.createStatement();
+
+ String viewDDL = "CREATE VIEW " + fullViewName +
+ " AS SELECT * FROM " + fullTableName + " WHERE k1 = 1";
+ stmt.execute(viewDDL);
+
+ String childViewDDL = "CREATE VIEW " + fullChildViewName +
+ " AS SELECT * FROM " + fullViewName + " WHERE k2 = 2 AND k3 =
3 AND s = 'a'";
+ stmt.execute(childViewDDL);
+
+ try {
+ stmt.execute("UPSERT INTO " + fullChildViewName + " VALUES(1, 2,
3, 'a')");
+ fail();
+ } catch (ReadOnlyTableException ignored) {
+ }
+ }
+
+ @Test
+ public void testUpdatableViewOnUpdatableView() throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA2,
generateUniqueName());
+ String fullChildViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ Connection conn = DriverManager.getConnection(getUrl(), props);
+ conn.setAutoCommit(true);
+
+ createTable(conn, fullTableName);
+ Statement stmt = conn.createStatement();
+
+ String viewDDL = "CREATE VIEW " + fullViewName +
+ " AS SELECT * FROM " + fullTableName + " WHERE k1 = 1";
+ stmt.execute(viewDDL);
+
+ String childViewDDL = "CREATE VIEW " + fullChildViewName +
+ " AS SELECT * FROM " + fullViewName + " WHERE k2 = 2 AND k3 =
3";
+ stmt.execute(childViewDDL);
+
+ stmt.execute("UPSERT INTO " + fullChildViewName + " VALUES(1, 2, 3,
'a')");
+
+ ResultSet rs = stmt.executeQuery("SELECT * FROM " + fullChildViewName);
+ assertTrue(rs.next());
+ assertEquals(1, rs.getInt(1));
+ assertEquals(2, rs.getInt(2));
+ assertEquals(3, rs.getInt(3));
+ assertEquals("a", rs.getString(4));
+ assertFalse(rs.next());
+ }
+
+ @Test
+ public void testSiblingsUpdatableOnUpdatableView() throws Exception {
+ String fullTableName = SchemaUtil.getTableName(SCHEMA1,
generateUniqueName());
+ String fullGlobalViewName = SchemaUtil.getTableName(SCHEMA2,
generateUniqueName());
+ String fullViewName = SchemaUtil.getTableName(SCHEMA3,
generateUniqueName());
+ String fullLeafViewName1 = SchemaUtil.getTableName(SCHEMA4,
generateUniqueName());
+ String fullLeafViewName2 = SchemaUtil.getTableName(SCHEMA4,
generateUniqueName());
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ createTable(conn, fullTableName);
+
+ Statement stmt = conn.createStatement();
+ String globalViewDDL = "CREATE VIEW " + fullGlobalViewName + " AS
SELECT * FROM "
+ + fullTableName + " WHERE k1 = 1";
+ stmt.execute(globalViewDDL);
+ String viewDDL =
+ "CREATE VIEW " + fullViewName + " AS SELECT * FROM " +
fullGlobalViewName
+ + " WHERE k2 = 1";
+ stmt.execute(viewDDL);
+ String leafView1DDL = "CREATE VIEW " + fullLeafViewName1 + " AS
SELECT * FROM "
+ + fullViewName + " WHERE k3 = 101";
+ stmt.execute(leafView1DDL);
+ String leafView2DDL = "CREATE VIEW " + fullLeafViewName2 + " AS
SELECT * FROM "
+ + fullViewName + " WHERE k3 = 105";
+ stmt.execute(leafView2DDL);
+
+ for (int i = 0; i < 10; i++) {
+ stmt.execute("UPSERT INTO " + fullTableName
+ + " VALUES(" + (i % 4) + "," + (i > 5 ? 2 : 1) + "," +
(i + 100) + ")");
+ }
+ conn.commit();
+
+ ResultSet rs;
+ rs = stmt.executeQuery("SELECT count(*) FROM " + fullTableName);
+ assertTrue(rs.next());
+ assertEquals(10, rs.getInt(1));
+ rs = stmt.executeQuery("SELECT count(*) FROM " +
fullGlobalViewName);
+ assertTrue(rs.next());
+ assertEquals(3, rs.getInt(1));
+ rs = stmt.executeQuery("SELECT count(*) FROM " + fullViewName);
+ assertTrue(rs.next());
+ assertEquals(2, rs.getInt(1));
+
+ rs = stmt.executeQuery("SELECT k1, k2, k3 FROM " +
fullLeafViewName1);
+ assertTrue(rs.next());
+ assertEquals(1, rs.getInt(1));
+ assertEquals(1, rs.getInt(2));
+ assertEquals(101, rs.getInt(3));
+ assertFalse(rs.next());
+ rs = stmt.executeQuery("SELECT k1, k2, k3 FROM " +
fullLeafViewName2);
+ assertTrue(rs.next());
+ assertEquals(1, rs.getInt(1));
+ assertEquals(1, rs.getInt(2));
+ assertEquals(105, rs.getInt(3));
+ assertFalse(rs.next());
+
+ stmt.execute("UPSERT INTO " + fullLeafViewName1 + " VALUES(1, 1,
101, 'leaf1')");
+ stmt.execute("UPSERT INTO " + fullLeafViewName2 + " VALUES(1, 1,
105, 'leaf2')");
+ conn.commit();
+ rs = stmt.executeQuery("SELECT s FROM " + fullLeafViewName1);
+ assertTrue(rs.next());
+ assertEquals("leaf1", rs.getString(1));
+ assertFalse(rs.next());
+ rs = stmt.executeQuery("SELECT s FROM " + fullLeafViewName2);
+ assertTrue(rs.next());
+ assertEquals("leaf2", rs.getString(1));
+ assertFalse(rs.next());
+ }
+ }
+
+ @Test
+ public void testUpdatableViewWithMultiTenantTable() throws Exception {
+ final String tableName = generateUniqueName();
+ final String fullTableName = SchemaUtil.getTableName(SCHEMA1,
tableName);
+ final String view01 = SchemaUtil.getTableName(SCHEMA2, "v01_" +
tableName);
+ final String view02 = SchemaUtil.getTableName(SCHEMA3, "v02_" +
tableName);
+ final String view03 = SchemaUtil.getTableName(SCHEMA4, "v03_" +
tableName);
+
+ Properties props = new Properties();
+
props.setProperty(QueryServices.PHOENIX_UPDATABLE_VIEW_RESTRICTION_ENABLED,
"true");
+ try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+ final Statement stmt = conn.createStatement();
+
+ stmt.execute("CREATE TABLE " + fullTableName
+ + " (TENANT_ID VARCHAR NOT NULL, COL1 CHAR(10) NOT NULL,
COL2 CHAR(5) NOT "
+ + "NULL, COL3 VARCHAR, COL4 VARCHAR CONSTRAINT pk PRIMARY
KEY(TENANT_ID, "
+ + "COL1, COL2)) MULTI_TENANT = true");
+
+ try (Connection tenantConn = getTenantConnection(TENANT1)) {
+ tenantConn.setAutoCommit(true);
+ final Statement tenantStmt = tenantConn.createStatement();
+
+ stmt.execute("CREATE VIEW " + view01
+ + " (VCOL1 CHAR(8) NOT NULL, COL5 VARCHAR CONSTRAINT
pk PRIMARY KEY(VCOL1))"
+ + " AS SELECT * FROM " + fullTableName + " WHERE COL1
= 'col1'");
+ tenantStmt.execute("CREATE VIEW " + view02 + " AS SELECT *
FROM " + view01
+ + " WHERE COL2 = 'col2'");
+ tenantStmt.execute("CREATE VIEW " + view03 + " AS SELECT *
FROM " + view02
+ + " WHERE VCOL1 = 'vcol1'");
+
+ tenantStmt.execute(String.format("UPSERT INTO %s
(VCOL1,COL3,COL4,COL5)" +
+ " VALUES('vcol2', 'col3', 'col4', 'col5')", view02));
+ tenantStmt.execute(String.format("UPSERT INTO %s
(COL3,COL4,COL5)" +
+ " VALUES('col3', 'col4', 'col5')", view03));
+
+ verifyNumberOfRows(fullTableName, TENANT1, 2, conn);
+ ResultSet rs = tenantConn.createStatement().executeQuery(
+ "SELECT COL1, COL2, VCOL1 FROM " + view02);
+ assertTrue(rs.next());
+ assertEquals("col1", rs.getString(1));
+ assertEquals("col2", rs.getString(2));
+ assertEquals("vcol1", rs.getString(3));
+ assertTrue(rs.next());
+ assertEquals("col1", rs.getString(1));
+ assertEquals("col2", rs.getString(2));
+ assertEquals("vcol2", rs.getString(3));
+ assertFalse(rs.next());
+
+ rs = tenantConn.createStatement().executeQuery(
+ "SELECT COL1, COL2, VCOL1 FROM " + view03);
+ assertTrue(rs.next());
+ assertEquals("col1", rs.getString(1));
+ assertEquals("col2", rs.getString(2));
+ assertEquals("vcol1", rs.getString(3));
+ assertFalse(rs.next());
+ }
+ }
+ }
+}