blob: bea5aa3759696c693587f0404176d11abe87e975 [file] [log] [blame]
/*
* 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 static com.google.common.collect.Lists.newArrayListWithExpectedSize;
import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
import static org.apache.phoenix.util.TestUtil.analyzeTable;
import static org.apache.phoenix.util.TestUtil.getAllSplits;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import org.apache.hadoop.hbase.DoNotRetryIOException;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.coprocessor.ObserverContext;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.compile.QueryPlan;
import org.apache.phoenix.coprocessor.BaseMetaDataEndpointObserver;
import org.apache.phoenix.coprocessor.MetaDataEndpointObserver;
import org.apache.phoenix.coprocessor.PhoenixMetaDataCoprocessorHost;
import org.apache.phoenix.coprocessor.PhoenixMetaDataCoprocessorHost.PhoenixMetaDataControllerEnvironment;
import org.apache.phoenix.exception.PhoenixIOException;
import org.apache.phoenix.exception.SQLExceptionCode;
import org.apache.phoenix.jdbc.PhoenixStatement;
import org.apache.phoenix.query.KeyRange;
import org.apache.phoenix.query.QueryServices;
import org.apache.phoenix.schema.ConcurrentTableMutationException;
import org.apache.phoenix.schema.PTable;
import org.apache.phoenix.schema.PTableType;
import org.apache.phoenix.schema.ReadOnlyTableException;
import org.apache.phoenix.schema.TableNotFoundException;
import org.apache.phoenix.transaction.PhoenixTransactionProvider.Feature;
import org.apache.phoenix.transaction.TransactionFactory;
import org.apache.phoenix.util.MetaDataUtil;
import org.apache.phoenix.util.PhoenixRuntime;
import org.apache.phoenix.util.PropertiesUtil;
import org.apache.phoenix.util.QueryUtil;
import org.apache.phoenix.util.ReadOnlyProps;
import org.apache.phoenix.util.SchemaUtil;
import org.apache.phoenix.util.TestUtil;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@RunWith(Parameterized.class)
public class ViewIT extends SplitSystemCatalogIT {
protected String tableDDLOptions;
protected String transactionProvider;
protected boolean columnEncoded;
private static final String FAILED_VIEWNAME = SchemaUtil.getTableName(SCHEMA2, "FAILED_VIEW");
private static final String SLOW_VIEWNAME_PREFIX = SchemaUtil.getTableName(SCHEMA2, "SLOW_VIEW");
private static volatile CountDownLatch latch1 = null;
private static volatile CountDownLatch latch2 = null;
private static volatile boolean throwExceptionInChildLinkPreHook = false;
private static volatile boolean slowDownAddingChildLink = false;
public ViewIT(String transactionProvider, boolean columnEncoded) {
StringBuilder optionBuilder = new StringBuilder();
this.transactionProvider = transactionProvider;
this.columnEncoded = columnEncoded;
if (transactionProvider != null) {
optionBuilder.append(" TRANSACTION_PROVIDER='" + transactionProvider + "'");
}
if (!columnEncoded) {
if (optionBuilder.length()!=0)
optionBuilder.append(",");
optionBuilder.append("COLUMN_ENCODED_BYTES=0");
}
this.tableDDLOptions = optionBuilder.toString();
}
@Parameters(name="ViewIT_transactionProvider={0}, columnEncoded={1}") // name is used by failsafe as file name in reports
public static synchronized Collection<Object[]> data() {
return TestUtil.filterTxParamData(Arrays.asList(new Object[][] {
{ "TEPHRA", false }, { "TEPHRA", true },
{ "OMID", false },
{ null, false }, { null, true }}),0);
}
@BeforeClass
public static synchronized void doSetup() throws Exception {
NUM_SLAVES_BASE = 6;
Map<String, String> props = Collections.emptyMap();
boolean splitSystemCatalog = (driver == null);
Map<String, String> serverProps = Maps.newHashMapWithExpectedSize(1);
serverProps.put(QueryServices.PHOENIX_ACLS_ENABLED, "true");
serverProps.put(PhoenixMetaDataCoprocessorHost.PHOENIX_META_DATA_COPROCESSOR_CONF_KEY,
TestMetaDataRegionObserver.class.getName());
serverProps.put("hbase.coprocessor.abortonerror", "false");
setUpTestDriver(new ReadOnlyProps(serverProps.entrySet().iterator()), new ReadOnlyProps(props.entrySet().iterator()));
// Split SYSTEM.CATALOG once after the mini-cluster is started
if (splitSystemCatalog) {
splitSystemCatalog();
}
}
@After
public void cleanup() {
latch1 = null;
latch2 = null;
throwExceptionInChildLinkPreHook = false;
slowDownAddingChildLink = false;
}
public static class TestMetaDataRegionObserver extends BaseMetaDataEndpointObserver {
@Override
public Optional<MetaDataEndpointObserver> getPhoenixObserver() {
return Optional.of(this);
}
@Override
public void preAlterTable(ObserverContext<PhoenixMetaDataControllerEnvironment> ctx,
String tenantId, String tableName, TableName physicalTableName,
TableName parentPhysicalTableName, PTableType type) throws IOException {
processTable(tableName);
}
@Override
public void preCreateTable(ObserverContext<PhoenixMetaDataControllerEnvironment> ctx,
String tenantId, String tableName, TableName physicalTableName,
TableName parentPhysicalTableName, PTableType tableType, Set<byte[]> familySet,
Set<TableName> indexes) throws IOException {
processTable(tableName);
}
@Override
public void preCreateViewAddChildLink(
final ObserverContext<PhoenixMetaDataControllerEnvironment> ctx,
final String tableName) throws IOException {
if (throwExceptionInChildLinkPreHook) {
throw new IOException();
}
processTable(tableName);
}
@Override
public void preDropTable(ObserverContext<PhoenixMetaDataControllerEnvironment> ctx,
String tenantId, String tableName, TableName physicalTableName,
TableName parentPhysicalTableName, PTableType tableType, List<PTable> indexes)
throws IOException {
processTable(tableName);
}
private void processTable(String tableName) throws DoNotRetryIOException {
if (tableName.equals(FAILED_VIEWNAME)) {
// throwing anything other than instances of IOException result
// in this coprocessor being unloaded
// DoNotRetryIOException tells HBase not to retry this mutation
// multiple times
throw new DoNotRetryIOException();
} else if (tableName.startsWith(SLOW_VIEWNAME_PREFIX) || slowDownAddingChildLink) {
// simulate a slow write to SYSTEM.CATALOG or SYSTEM.CHILD_LINK
if (latch1 != null) {
latch1.countDown();
}
if (latch2 != null) {
try {
// wait till the second task is complete before completing the first task
boolean result = latch2.await(2, TimeUnit.MINUTES);
if (!result) {
throw new RuntimeException("Second task took took long to complete");
}
} catch (InterruptedException e) {
}
}
}
}
}
@Test
public void testReadOnlyOnUpdatableView() throws Exception {
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String fullViewName1 = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
String fullViewName2 = SchemaUtil.getTableName(SCHEMA3, generateUniqueName());
String ddl =
"CREATE VIEW " + fullViewName2 + " AS SELECT * FROM " + fullViewName1
+ " WHERE k3 > 1 and k3 < 50";
testUpdatableView(fullTableName, fullViewName1, fullViewName2, ddl, null, tableDDLOptions);
Connection conn = DriverManager.getConnection(getUrl());
ResultSet rs = conn.createStatement().executeQuery("SELECT k1, k2, k3 FROM " + fullViewName2);
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
assertEquals(109, rs.getInt(2));
assertEquals(2, rs.getInt(3));
assertFalse(rs.next());
try {
conn.createStatement().execute("UPSERT INTO " + fullViewName2 + " VALUES(1)");
fail();
} catch (ReadOnlyTableException e) {
}
conn.createStatement().execute("UPSERT INTO " + fullTableName + "(k1, k2,k3) VALUES(1, 122, 5)");
conn.commit();
rs = conn.createStatement().executeQuery("SELECT k1, k2, k3 FROM " + fullViewName2 + " WHERE k2 >= 120");
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
assertEquals(122, rs.getInt(2));
assertEquals(5, rs.getInt(3));
assertFalse(rs.next());
}
@Test
public void testReadOnlyViewWithCaseSensitiveTableNames() throws Exception {
Connection earlierCon = DriverManager.getConnection(getUrl());
Connection conn = DriverManager.getConnection(getUrl());
String schemaName = TestUtil.DEFAULT_SCHEMA_NAME + "_" + generateUniqueName();
String caseSensitiveTableName = "\"t_" + generateUniqueName() + "\"" ;
String fullTableName = SchemaUtil.getTableName(schemaName, caseSensitiveTableName);
String caseSensitiveViewName = "\"v_" + generateUniqueName() + "\"" ;
String ddl = "CREATE TABLE " + fullTableName + " (k INTEGER NOT NULL PRIMARY KEY, v1 DATE)" + tableDDLOptions;
conn.createStatement().execute(ddl);
ddl = "CREATE VIEW " + caseSensitiveViewName + " (v2 VARCHAR) AS SELECT * FROM " + fullTableName + " WHERE k > 5";
conn.createStatement().execute(ddl);
try {
conn.createStatement().execute("UPSERT INTO " + caseSensitiveViewName + " VALUES(1)");
fail();
} catch (ReadOnlyTableException e) {
}
for (int i = 0; i < 10; i++) {
conn.createStatement().execute("UPSERT INTO " + fullTableName + " VALUES(" + i + ")");
}
conn.commit();
int count = 0;
ResultSet rs = conn.createStatement().executeQuery("SELECT k FROM " + caseSensitiveViewName);
while (rs.next()) {
count++;
assertEquals(count + 5, rs.getInt(1));
}
assertEquals(4, count);
count = 0;
rs = earlierCon.createStatement().executeQuery("SELECT k FROM " + caseSensitiveViewName);
while (rs.next()) {
count++;
assertEquals(count + 5, rs.getInt(1));
}
assertEquals(4, count);
}
@Test
public void testReadOnlyViewWithCaseSensitiveColumnNames() throws Exception {
Connection conn = DriverManager.getConnection(getUrl());
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String viewName = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
String ddl = "CREATE TABLE " + fullTableName + " (\"k\" INTEGER NOT NULL PRIMARY KEY, \"v1\" INTEGER, \"a\".v2 VARCHAR)" + tableDDLOptions;
conn.createStatement().execute(ddl);
ddl = "CREATE VIEW " + viewName + " (v VARCHAR) AS SELECT * FROM " + fullTableName + " WHERE \"k\" > 5 and \"v1\" > 1";
conn.createStatement().execute(ddl);
try {
conn.createStatement().execute("UPSERT INTO " + viewName + " VALUES(1)");
fail();
} catch (ReadOnlyTableException e) {
}
for (int i = 0; i < 10; i++) {
conn.createStatement().execute("UPSERT INTO " + fullTableName + " VALUES(" + i + ", " + (i+10) + ",'A')");
}
conn.commit();
int count = 0;
ResultSet rs = conn.createStatement().executeQuery("SELECT \"k\", \"v1\",\"a\".v2 FROM " + viewName);
while (rs.next()) {
count++;
assertEquals(count + 5, rs.getInt(1));
}
assertEquals(4, count);
}
@Test
public void testViewWithCurrentDate() throws Exception {
Connection conn = DriverManager.getConnection(getUrl());
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String viewName = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
String ddl = "CREATE TABLE " + fullTableName + " (k INTEGER NOT NULL PRIMARY KEY, v1 INTEGER, v2 DATE)" + tableDDLOptions;
conn.createStatement().execute(ddl);
ddl = "CREATE VIEW " + viewName + " (v VARCHAR) AS SELECT * FROM " + fullTableName + " WHERE v2 > CURRENT_DATE()-5 AND v2 > DATE '2010-01-01'";
conn.createStatement().execute(ddl);
try {
conn.createStatement().execute("UPSERT INTO " + viewName + " VALUES(1)");
fail();
} catch (ReadOnlyTableException e) {
}
for (int i = 0; i < 10; i++) {
conn.createStatement().execute("UPSERT INTO " + fullTableName + " VALUES(" + i + ", " + (i+10) + ",CURRENT_DATE()-" + i + ")");
}
conn.commit();
int count = 0;
ResultSet rs = conn.createStatement().executeQuery("SELECT k FROM " + viewName);
while (rs.next()) {
assertEquals(count, rs.getInt(1));
count++;
}
assertEquals(5, count);
}
@Test
public void testViewUsesTableGlobalIndex() throws Exception {
testViewUsesTableIndex(false);
}
@Test
public void testViewUsesTableLocalIndex() throws Exception {
if (transactionProvider == null ||
!TransactionFactory.getTransactionProvider(
TransactionFactory.Provider.valueOf(transactionProvider)).isUnsupported(Feature.ALLOW_LOCAL_INDEX)) {
testViewUsesTableIndex(true);
}
}
private void testViewUsesTableIndex(boolean localIndex) throws Exception {
ResultSet rs;
// Use unique name for table with local index as otherwise we run into issues
// when we attempt to drop the table (with the drop metadata option set to false
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName()) + (localIndex ? "_WITH_LI" : "_WITHOUT_LI");
Connection conn = DriverManager.getConnection(getUrl());
String ddl = "CREATE TABLE " + fullTableName + " (k1 INTEGER NOT NULL, k2 INTEGER NOT NULL, k3 DECIMAL, s1 VARCHAR, s2 VARCHAR CONSTRAINT pk PRIMARY KEY (k1, k2, k3))" + tableDDLOptions;
conn.createStatement().execute(ddl);
String indexName1 = "I_" + generateUniqueName();
String fullIndexName1 = SchemaUtil.getTableName(SCHEMA1, indexName1);
conn.createStatement().execute("CREATE " + (localIndex ? "LOCAL " : "") + " INDEX " + indexName1 + " ON " + fullTableName + "(k3, k2) INCLUDE(s1, s2)");
String indexName2 = "I_" + generateUniqueName();
conn.createStatement().execute("CREATE INDEX " + indexName2 + " ON " + fullTableName + "(k3, k2, s2)");
String fullViewName = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
ddl = "CREATE VIEW " + fullViewName + " AS SELECT * FROM " + fullTableName + " WHERE s1 = 'foo'";
conn.createStatement().execute(ddl);
String[] s1Values = {"foo","bar"};
for (int i = 0; i < 10; i++) {
conn.createStatement().execute("UPSERT INTO " + fullTableName + " VALUES(" + (i % 4) + "," + (i+100) + "," + (i > 5 ? 2 : 1) + ",'" + s1Values[i%2] + "','bas')");
}
conn.commit();
rs = conn.createStatement().executeQuery("SELECT count(*) FROM " + fullViewName);
assertTrue(rs.next());
assertEquals(5, rs.getLong(1));
assertFalse(rs.next());
String viewIndexName = "I_" + generateUniqueName();
conn.createStatement().execute("CREATE INDEX " + viewIndexName + " on " + fullViewName + "(k2)");
String query = "SELECT k2 FROM " + fullViewName + " WHERE k2 IN (100,109) AND k3 IN (1,2) AND s2='bas'";
rs = conn.createStatement().executeQuery(query);
assertTrue(rs.next());
assertEquals(100, rs.getInt(1));
assertFalse(rs.next());
rs = conn.createStatement().executeQuery("EXPLAIN " + query);
String queryPlan = QueryUtil.getExplainPlan(rs);
// Assert that in either case (local & global) that index from physical table used for query on view.
if (localIndex) {
assertEquals("CLIENT PARALLEL 1-WAY SKIP SCAN ON 4 KEYS OVER " + fullTableName + " [1,1,100] - [1,2,109]\n" +
" SERVER FILTER BY (\"S2\" = 'bas' AND \"S1\" = 'foo')\n" +
"CLIENT MERGE SORT", queryPlan);
} else {
assertEquals(
"CLIENT PARALLEL 1-WAY SKIP SCAN ON 4 KEYS OVER " + fullIndexName1 + " [1,100] - [2,109]\n" +
" SERVER FILTER BY (\"S2\" = 'bas' AND \"S1\" = 'foo')", queryPlan);
}
}
@Test
public void testCreateChildViewWithBaseTableLocalIndex() throws Exception {
testCreateChildViewWithBaseTableIndex(true);
}
@Test
public void testCreateChildViewWithBaseTableGlobalIndex() throws Exception {
testCreateChildViewWithBaseTableIndex(false);
}
public void testCreateChildViewWithBaseTableIndex(boolean localIndex) throws Exception {
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String fullViewName = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
String indexName = "I_" + generateUniqueName();
String fullChildViewName = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
try (Connection conn = DriverManager.getConnection(getUrl())) {
String sql =
"CREATE TABLE " + fullTableName
+ " (ID INTEGER NOT NULL PRIMARY KEY, HOST VARCHAR(10), FLAG BOOLEAN)";
conn.createStatement().execute(sql);
sql =
"CREATE VIEW " + fullViewName
+ " (COL1 INTEGER, COL2 INTEGER, COL3 INTEGER, COL4 INTEGER) AS SELECT * FROM "
+ fullTableName + " WHERE ID > 5";
conn.createStatement().execute(sql);
sql =
"CREATE " + (localIndex ? "LOCAL " : "") + " INDEX " + indexName + " ON "
+ fullTableName + "(HOST)";
conn.createStatement().execute(sql);
sql =
"CREATE VIEW " + fullChildViewName + " AS SELECT * FROM " + fullViewName
+ " WHERE COL1 > 2";
conn.createStatement().execute(sql);
// Sanity upserts in baseTable, view, child view
conn.createStatement()
.executeUpdate("upsert into " + fullTableName + " values (1, 'host1', TRUE)");
conn.createStatement()
.executeUpdate("upsert into " + fullTableName + " values (5, 'host5', FALSE)");
conn.createStatement()
.executeUpdate("upsert into " + fullTableName + " values (7, 'host7', TRUE)");
conn.commit();
// View is not updateable
try {
conn.createStatement().executeUpdate("upsert into " + fullViewName
+ " (ID, HOST, FLAG, COL1) values (7, 'host7', TRUE, 1)");
fail();
} catch (Exception e) {
}
// Check view inherits index, but child view doesn't
PTable table = PhoenixRuntime.getTable(conn, fullViewName);
assertEquals(1, table.getIndexes().size());
table = PhoenixRuntime.getTable(conn, fullChildViewName);
assertEquals(0, table.getIndexes().size());
ResultSet rs =
conn.createStatement().executeQuery("select count(*) from " + fullTableName);
assertTrue(rs.next());
assertEquals(3, rs.getInt(1));
rs = conn.createStatement().executeQuery("select count(*) from " + fullViewName);
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
}
}
@Test
public void testCreateViewDefinesPKColumn() throws Exception {
Connection conn = DriverManager.getConnection(getUrl());
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String fullViewName = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
String ddl = "CREATE TABLE " + fullTableName + " (k1 INTEGER NOT NULL, k2 INTEGER NOT NULL, v1 DECIMAL, CONSTRAINT pk PRIMARY KEY (k1, k2))" + tableDDLOptions;
conn.createStatement().execute(ddl);
ddl = "CREATE VIEW " + fullViewName + "(v2 VARCHAR, k3 VARCHAR PRIMARY KEY) AS SELECT * FROM " + fullTableName + " WHERE K1 = 1";
conn.createStatement().execute(ddl);
// assert PK metadata
ResultSet rs =
conn.getMetaData().getPrimaryKeys(null,
SchemaUtil.getSchemaNameFromFullName(fullViewName),
SchemaUtil.getTableNameFromFullName(fullViewName));
assertPKs(rs, new String[] {"K1", "K2", "K3"});
// sanity check upserts into base table and view
conn.createStatement().executeUpdate("upsert into " + fullTableName + " (k1, k2, v1) values (1, 1, 1)");
conn.createStatement().executeUpdate("upsert into " + fullViewName + " (k1, k2, k3, v2) values (1, 1, 'abc', 'def')");
conn.commit();
// expect 2 rows in the base table
rs = conn.createStatement().executeQuery("select count(*) from " + fullTableName);
assertTrue(rs.next());
assertEquals(2, rs.getInt(1));
// expect 2 row in the view
rs = conn.createStatement().executeQuery("select count(*) from " + fullViewName);
assertTrue(rs.next());
assertEquals(2, rs.getInt(1));
}
@Test
public void testQueryViewStatementOptimization() throws Exception {
Connection conn = DriverManager.getConnection(getUrl());
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String fullViewName1 = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
String fullViewName2 = SchemaUtil.getTableName(SCHEMA3, generateUniqueName());
String sql = "CREATE TABLE " + fullTableName + " (k1 INTEGER NOT NULL, k2 INTEGER NOT NULL, v1 DECIMAL, CONSTRAINT pk PRIMARY KEY (k1, k2))" + tableDDLOptions;
conn.createStatement().execute(sql);
sql = "CREATE VIEW " + fullViewName1 + " AS SELECT * FROM " + fullTableName;
conn.createStatement().execute(sql);
sql = "CREATE VIEW " + fullViewName2 + " AS SELECT * FROM " + fullTableName + " WHERE k1 = 1.0";
conn.createStatement().execute(sql);
sql = "SELECT * FROM " + fullViewName1 + " order by k1, k2";
PreparedStatement stmt = conn.prepareStatement(sql);
QueryPlan plan = PhoenixRuntime.getOptimizedQueryPlan(stmt);
assertEquals(0, plan.getOrderBy().getOrderByExpressions().size());
sql = "SELECT * FROM " + fullViewName2 + " order by k1, k2";
stmt = conn.prepareStatement(sql);
plan = PhoenixRuntime.getOptimizedQueryPlan(stmt);
assertEquals(0, plan.getOrderBy().getOrderByExpressions().size());
}
private void assertPKs(ResultSet rs, String[] expectedPKs) throws SQLException {
List<String> pkCols = newArrayListWithExpectedSize(expectedPKs.length);
while (rs.next()) {
pkCols.add(rs.getString("COLUMN_NAME"));
}
String[] actualPKs = pkCols.toArray(new String[0]);
assertArrayEquals(expectedPKs, actualPKs);
}
@Test
public void testCompositeDescPK() throws Exception {
Properties props = new Properties();
try (Connection globalConn = DriverManager.getConnection(getUrl(), props)) {
String tableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String viewName1 = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
String viewName2 = SchemaUtil.getTableName(SCHEMA3, generateUniqueName());
String viewName3 = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String viewName4 = SchemaUtil.getTableName(SCHEMA2, generateUniqueName());
String myTableDDLOptions = tableDDLOptions;
if (myTableDDLOptions.length() != 0) myTableDDLOptions += ",";
myTableDDLOptions += "VERSIONS=1, MULTI_TENANT=true, IMMUTABLE_ROWS=TRUE, REPLICATION_SCOPE=1";
// create global base table
globalConn.createStatement().execute("CREATE TABLE " + tableName
+ " (TENANT_ID CHAR(15) NOT NULL, KEY_PREFIX CHAR(3) NOT NULL, CREATED_DATE DATE, CREATED_BY CHAR(15), SYSTEM_MODSTAMP DATE CONSTRAINT PK PRIMARY KEY (TENANT_ID, KEY_PREFIX)) " + myTableDDLOptions);
String tenantId = "tenantId";
Properties tenantProps = new Properties();
tenantProps.setProperty(PhoenixRuntime.TENANT_ID_ATTRIB, tenantId);
// create a tenant specific view
try (Connection tenantConn = DriverManager.getConnection(getUrl(), tenantProps)) {
// create various tenant specific views
// view with composite PK with multiple PK values of VARCHAR values DESC
tenantConn.createStatement()
.execute("CREATE VIEW " + viewName1
+ " (pk1 VARCHAR(10) NOT NULL, pk2 VARCHAR(10) NOT NULL, col1 DATE, col3 DECIMAL CONSTRAINT PK PRIMARY KEY (pk1 DESC, pk2 DESC)) AS SELECT * FROM "
+ tableName + " WHERE KEY_PREFIX = 'abc' ");
// view with composite PK with single pk value DESC
tenantConn.createStatement()
.execute("CREATE VIEW " + viewName2
+ " (pk1 VARCHAR(10) NOT NULL, pk2 VARCHAR(10) NOT NULL, col1 DATE, col3 DECIMAL CONSTRAINT PK PRIMARY KEY (pk1 DESC, pk2 DESC)) AS SELECT * FROM "
+ tableName + " WHERE KEY_PREFIX = 'abc' ");
// view with composite PK with multiple Date PK values DESC
tenantConn.createStatement()
.execute("CREATE VIEW " + viewName3
+ " (pk1 DATE(10) NOT NULL, pk2 DATE(10) NOT NULL, col1 VARCHAR(10), col3 DECIMAL CONSTRAINT PK PRIMARY KEY (pk1 DESC, pk2 DESC)) AS SELECT * FROM "
+ tableName + " WHERE KEY_PREFIX = 'ab3' ");
tenantConn.createStatement()
.execute("CREATE VIEW " + viewName4
+ " (pk1 DATE(10) NOT NULL, pk2 DECIMAL NOT NULL, pk3 VARCHAR(10) NOT NULL, col3 DECIMAL CONSTRAINT PK PRIMARY KEY (pk1 DESC, pk2 DESC, pk3 DESC)) AS SELECT * FROM "
+ tableName + " WHERE KEY_PREFIX = 'ab4' ");
// upsert rows
upsertRows(tableName, viewName1, tenantConn);
upsertRows(tableName, viewName2, tenantConn);
// run queries
String[] whereClauses =
new String[] { "pk1 = 'testa'", "", "pk1 >= 'testa'", "pk1 <= 'testa'",
"pk1 > 'testa'", "pk1 < 'testa'" };
long[] expectedArray = new long[] { 4, 5, 5, 4, 1, 0 };
validate(viewName1, tenantConn, whereClauses, expectedArray);
validate(viewName2, tenantConn, whereClauses, expectedArray);
tenantConn.createStatement().execute("UPSERT INTO " + viewName3
+ " (pk1, pk2, col1, col3) VALUES (TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), TO_DATE('2017-10-16 21:00:00', 'yyyy-MM-dd HH:mm:ss'), 'txt1', 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName3
+ " (pk1, pk2, col1, col3) VALUES (TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), TO_DATE('2017-10-16 21:01:00', 'yyyy-MM-dd HH:mm:ss'), 'txt1', 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName3
+ " (pk1, pk2, col1, col3) VALUES (TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), TO_DATE('2017-10-16 21:02:00', 'yyyy-MM-dd HH:mm:ss'), 'txt1', 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName3
+ " (pk1, pk2, col1, col3) VALUES (TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), TO_DATE('2017-10-16 21:03:00', 'yyyy-MM-dd HH:mm:ss'), 'txt1', 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName3
+ " (pk1, pk2, col1, col3) VALUES (TO_DATE('2017-10-16 23:00:00', 'yyyy-MM-dd HH:mm:ss'), TO_DATE('2017-10-16 21:04:00', 'yyyy-MM-dd HH:mm:ss'), 'txt1', 10)");
tenantConn.commit();
String[] view3WhereClauses =
new String[] {
"pk1 = TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')", "",
"pk1 >= TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')",
"pk1 <= TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')",
"pk1 > TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')",
"pk1 < TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')" };
validate(viewName3, tenantConn, view3WhereClauses, expectedArray);
tenantConn.createStatement().execute("UPSERT INTO " + viewName4
+ " (pk1, pk2, pk3, col3) VALUES (TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), 1, 'txt1', 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName4
+ " (pk1, pk2, pk3, col3) VALUES (TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), 2, 'txt2', 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName4
+ " (pk1, pk2, pk3, col3) VALUES (TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), 3, 'txt3', 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName4
+ " (pk1, pk2, pk3, col3) VALUES (TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), 4, 'txt4', 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName4
+ " (pk1, pk2, pk3, col3) VALUES (TO_DATE('2017-10-16 23:00:00', 'yyyy-MM-dd HH:mm:ss'), 1, 'txt1', 10)");
tenantConn.commit();
String[] view4WhereClauses =
new String[] {
"pk1 = TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')",
"pk1 = TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss') AND pk2 = 2",
"pk1 = TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss') AND pk2 > 2",
"", "pk1 >= TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')",
"pk1 <= TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')",
"pk1 > TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')",
"pk1 < TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss')" };
long[] view4ExpectedArray = new long[] { 4, 1, 2, 5, 5, 4, 1, 0 };
validate(viewName4, tenantConn, view4WhereClauses, view4ExpectedArray);
}
}
}
private void validate(String viewName, Connection tenantConn, String[] whereClauseArray,
long[] expectedArray) throws SQLException {
for (int i = 0; i < whereClauseArray.length; ++i) {
String where = !whereClauseArray[i].isEmpty() ? (" WHERE " + whereClauseArray[i]) : "";
ResultSet rs =
tenantConn.createStatement()
.executeQuery("SELECT count(*) FROM " + viewName + where);
assertTrue(rs.next());
assertEquals(expectedArray[i], rs.getLong(1));
assertFalse(rs.next());
}
}
private void upsertRows(String tableName, String viewName1, Connection tenantConn) throws SQLException, IOException {
tenantConn.createStatement().execute("UPSERT INTO " + viewName1
+ " (pk1, pk2, col1, col3) VALUES ('testa', 'testb', TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName1
+ " (pk1, pk2, col1, col3) VALUES ('testa', 'testc', TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName1
+ " (pk1, pk2, col1, col3) VALUES ('testa', 'testd', TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName1
+ " (pk1, pk2, col1, col3) VALUES ('testa', 'teste', TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), 10)");
tenantConn.createStatement().execute("UPSERT INTO " + viewName1
+ " (pk1, pk2, col1, col3) VALUES ('testb', 'testa', TO_DATE('2017-10-16 22:00:00', 'yyyy-MM-dd HH:mm:ss'), 10)");
tenantConn.commit();
}
public static String testUpdatableView(String fullTableName, String fullViewName, String fullChildViewName,
String childViewDDL, Integer saltBuckets, String tableDDLOptions) throws Exception {
Connection conn = DriverManager.getConnection(getUrl());
if (saltBuckets != null) {
if (tableDDLOptions.length() != 0)
tableDDLOptions += ",";
tableDDLOptions += (" SALT_BUCKETS=" + saltBuckets);
}
String ddl = "CREATE TABLE " + fullTableName
+ " (k1 INTEGER NOT NULL, k2 INTEGER NOT NULL, k3 DECIMAL, s VARCHAR CONSTRAINT pk PRIMARY KEY (k1, k2, k3))"
+ tableDDLOptions;
conn.createStatement().execute(ddl);
ddl = "CREATE VIEW " + fullViewName + " AS SELECT * FROM " + fullTableName + " WHERE k1 = 1";
conn.createStatement().execute(ddl);
ArrayList<String> splitPoints = Lists.newArrayList(fullTableName, fullViewName);
if (fullChildViewName != null) {
conn.createStatement().execute(childViewDDL);
splitPoints.add(fullChildViewName);
}
for (int i = 0; i < 10; i++) {
conn.createStatement().execute("UPSERT INTO " + fullTableName + " VALUES(" + (i % 4) + "," + (i + 100) + ","
+ (i > 5 ? 2 : 1) + ")");
}
conn.commit();
ResultSet rs;
rs = conn.createStatement().executeQuery("SELECT count(*) FROM " + fullTableName);
assertTrue(rs.next());
assertEquals(10, rs.getInt(1));
rs = conn.createStatement().executeQuery("SELECT count(*) FROM " + fullViewName);
assertTrue(rs.next());
assertEquals(3, rs.getInt(1));
rs = conn.createStatement().executeQuery("SELECT k1, k2, k3 FROM " + fullViewName);
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
assertEquals(101, rs.getInt(2));
assertEquals(1, rs.getInt(3));
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
assertEquals(105, rs.getInt(2));
assertEquals(1, rs.getInt(3));
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
assertEquals(109, rs.getInt(2));
assertEquals(2, rs.getInt(3));
assertFalse(rs.next());
conn.createStatement().execute("UPSERT INTO " + fullViewName + "(k2,S,k3) VALUES(120,'foo',50.0)");
conn.createStatement().execute("UPSERT INTO " + fullViewName + "(k2,S,k3) VALUES(121,'bar',51.0)");
conn.commit();
rs = conn.createStatement().executeQuery("SELECT k1, k2 FROM " + fullViewName + " WHERE k2 >= 120");
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
assertEquals(120, rs.getInt(2));
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
assertEquals(121, rs.getInt(2));
assertFalse(rs.next());
conn.close();
return fullViewName;
}
public static Pair<String, Scan> testUpdatableViewIndex(String fullTableName, Integer saltBuckets,
boolean localIndex, String viewName) throws Exception {
ResultSet rs;
Connection conn = DriverManager.getConnection(getUrl());
String viewIndexName1 = "I_" + generateUniqueName();
String viewIndexPhysicalName = MetaDataUtil.getViewIndexPhysicalName(fullTableName);
if (localIndex) {
conn.createStatement().execute("CREATE LOCAL INDEX " + viewIndexName1 + " on " + viewName + "(k3)");
} else {
conn.createStatement().execute("CREATE INDEX " + viewIndexName1 + " on " + viewName + "(k3) include (s)");
}
conn.createStatement().execute("UPSERT INTO " + viewName + "(k2,S,k3) VALUES(120,'foo',50.0)");
conn.commit();
analyzeTable(conn, viewName);
List<KeyRange> splits = getAllSplits(conn, viewIndexName1);
// More guideposts with salted, since it's already pre-split at salt
// buckets
assertEquals(saltBuckets == null ? 6 : 8, splits.size());
String query = "SELECT k1, k2, k3, s FROM " + viewName + " WHERE k3 = 51.0";
rs = conn.createStatement().executeQuery(query);
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
assertEquals(121, rs.getInt(2));
assertTrue(BigDecimal.valueOf(51.0).compareTo(rs.getBigDecimal(3)) == 0);
assertEquals("bar", rs.getString(4));
assertFalse(rs.next());
rs = conn.createStatement().executeQuery("EXPLAIN " + query);
String queryPlan = QueryUtil.getExplainPlan(rs);
if (localIndex) {
assertEquals("CLIENT PARALLEL " + (saltBuckets == null ? 1 : saltBuckets) + "-WAY RANGE SCAN OVER "
+ fullTableName + " [1,51]\n" + " SERVER FILTER BY FIRST KEY ONLY\n" + "CLIENT MERGE SORT",
queryPlan);
} else {
assertEquals(saltBuckets == null
? "CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + viewIndexPhysicalName + " [" + Short.MIN_VALUE + ",51]"
: "CLIENT PARALLEL " + saltBuckets + "-WAY RANGE SCAN OVER " + viewIndexPhysicalName + " [0,"
+ Short.MIN_VALUE + ",51] - [" + (saltBuckets.intValue() - 1) + "," + Short.MIN_VALUE
+ ",51]\nCLIENT MERGE SORT",
queryPlan);
}
String viewIndexName2 = "I_" + generateUniqueName();
if (localIndex) {
conn.createStatement().execute("CREATE LOCAL INDEX " + viewIndexName2 + " on " + viewName + "(s)");
} else {
conn.createStatement().execute("CREATE INDEX " + viewIndexName2 + " on " + viewName + "(s)");
}
// new index hasn't been analyzed yet
splits = getAllSplits(conn, viewIndexName2);
assertEquals(saltBuckets == null ? 1 : 3, splits.size());
// analyze table should analyze all view data
analyzeTable(conn, fullTableName);
splits = getAllSplits(conn, viewIndexName2);
assertEquals(saltBuckets == null ? 6 : 8, splits.size());
query = "SELECT k1, k2, s FROM " + viewName + " WHERE s = 'foo'";
Statement statement = conn.createStatement();
rs = statement.executeQuery(query);
Scan scan = statement.unwrap(PhoenixStatement.class).getQueryPlan().getContext().getScan();
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
assertEquals(120, rs.getInt(2));
assertEquals("foo", rs.getString(3));
assertFalse(rs.next());
rs = conn.createStatement().executeQuery("EXPLAIN " + query);
String physicalTableName;
if (localIndex) {
physicalTableName = fullTableName;
assertEquals("CLIENT PARALLEL " + (saltBuckets == null ? 1 : saltBuckets) + "-WAY RANGE SCAN OVER "
+ fullTableName + " [" + (2) + ",'foo']\n" + " SERVER FILTER BY FIRST KEY ONLY\n"
+ "CLIENT MERGE SORT", QueryUtil.getExplainPlan(rs));
} else {
physicalTableName = viewIndexPhysicalName;
assertEquals(
saltBuckets == null
? "CLIENT PARALLEL 1-WAY RANGE SCAN OVER " + viewIndexPhysicalName + " ["
+ (Short.MIN_VALUE + 1) + ",'foo']\n" + " SERVER FILTER BY FIRST KEY ONLY"
: "CLIENT PARALLEL " + saltBuckets + "-WAY RANGE SCAN OVER " + viewIndexPhysicalName
+ " [0," + (Short.MIN_VALUE + 1) + ",'foo'] - [" + (saltBuckets.intValue() - 1)
+ "," + (Short.MIN_VALUE + 1) + ",'foo']\n"
+ " SERVER FILTER BY FIRST KEY ONLY\n" + "CLIENT MERGE SORT",
QueryUtil.getExplainPlan(rs));
}
conn.close();
return new Pair<>(physicalTableName, scan);
}
@Test
public void testChildViewCreationFails() throws Exception {
Connection conn = DriverManager.getConnection(getUrl());
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String fullViewName1 = FAILED_VIEWNAME;
String fullViewName2 = SchemaUtil.getTableName(SCHEMA3, generateUniqueName());
String tableDdl = "CREATE TABLE " + fullTableName + " (k INTEGER NOT NULL PRIMARY KEY, v1 DATE)" + tableDDLOptions;
conn.createStatement().execute(tableDdl);
String ddl = "CREATE VIEW " + fullViewName1 + " (v2 VARCHAR) AS SELECT * FROM " + fullTableName + " WHERE k > 5";
try {
conn.createStatement().execute(ddl);
fail();
}
catch (PhoenixIOException e){
}
ddl = "CREATE VIEW " + fullViewName2 + "(v2 VARCHAR) AS SELECT * FROM " + fullTableName + " WHERE k > 10";
conn.createStatement().execute(ddl);
// the first child view should not exist
try {
PhoenixRuntime.getTableNoCache(conn, fullViewName1);
fail();
} catch (SQLException e) {
}
// we should be able to load the table
PhoenixRuntime.getTableNoCache(conn, fullTableName);
// we should be able to load the second view
PhoenixRuntime.getTableNoCache(conn, fullViewName2);
}
@Test
public void testConcurrentViewCreationAndTableDrop() throws Exception {
try (Connection conn = DriverManager.getConnection(getUrl())) {
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String fullViewName1 = SLOW_VIEWNAME_PREFIX + "_" + generateUniqueName();
latch1 = new CountDownLatch(1);
latch2 = new CountDownLatch(1);
String tableDdl =
"CREATE TABLE " + fullTableName + " (k INTEGER NOT NULL PRIMARY KEY, v1 DATE)"
+ tableDDLOptions;
conn.createStatement().execute(tableDdl);
ExecutorService executorService = Executors.newFixedThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
t.setPriority(Thread.MIN_PRIORITY);
return t;
}
});
// When dropping a table, we check the parent->child links in the SYSTEM.CHILD_LINK
// table and check that cascade is set, if it isn't, we throw an exception (see
// ViewUtil.hasChildViews). After PHOENIX-4810, we first send a client-server RPC to add
// parent->child links to SYSTEM.CHILD_LINK and then add metadata for the view in
// SYSTEM.CATALOG, so we must delay link creation so that the drop table does not fail
slowDownAddingChildLink = true;
// create the view in a separate thread (which will take some time
// to complete)
Future<Exception> future =
executorService.submit(new CreateViewRunnable(fullTableName, fullViewName1));
// wait till the thread makes the rpc to create the view
latch1.await();
tableDdl = "DROP TABLE " + fullTableName;
// Revert this flag since we don't want to wait in preDropTable
slowDownAddingChildLink = false;
// drop table goes through first and so the view creation should fail
conn.createStatement().execute(tableDdl);
latch2.countDown();
Exception e = future.get();
assertTrue("Expected TableNotFoundException since drop table goes through first",
e instanceof TableNotFoundException &&
fullTableName.equals(((TableNotFoundException) e).getTableName()));
}
}
@Test
public void testChildLinkCreationFailThrowsException() throws Exception {
try (Connection conn = DriverManager.getConnection(getUrl())) {
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String fullViewName1 = SchemaUtil.getTableName(SCHEMA3, generateUniqueName());
// create base table
String tableDdl = "CREATE TABLE " + fullTableName
+ " (k INTEGER NOT NULL PRIMARY KEY, v1 DATE)" + tableDDLOptions;
conn.createStatement().execute(tableDdl);
// Throw an exception in ChildLinkMetaDataEndpoint while adding parent->child links
// to simulate a failure
throwExceptionInChildLinkPreHook = true;
// create a view
String ddl = "CREATE VIEW " + fullViewName1 + " (v2 VARCHAR) AS SELECT * FROM "
+ fullTableName + " WHERE k = 6";
try {
conn.createStatement().execute(ddl);
fail("Should have thrown an exception");
} catch(SQLException sqlE) {
assertEquals("Expected a different Error code",
SQLExceptionCode.UNABLE_TO_CREATE_CHILD_LINK.getErrorCode(),
sqlE.getErrorCode());
}
}
}
@Test
public void testConcurrentAddSameColumnDifferentType() throws Exception {
try (Connection conn = DriverManager.getConnection(getUrl())) {
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String fullViewName1 = SLOW_VIEWNAME_PREFIX + "_" + generateUniqueName();
String fullViewName2 = SchemaUtil.getTableName(SCHEMA3, generateUniqueName());
// create base table
String tableDdl =
"CREATE TABLE " + fullTableName + " (k INTEGER NOT NULL PRIMARY KEY, v1 DATE)"
+ tableDDLOptions;
conn.createStatement().execute(tableDdl);
// create a view
String ddl =
"CREATE VIEW " + fullViewName1 + " (v2 VARCHAR) AS SELECT * FROM "
+ fullTableName + " WHERE k = 6";
conn.createStatement().execute(ddl);
latch1 = new CountDownLatch(1);
latch2 = new CountDownLatch(1);
ExecutorService executorService = Executors.newFixedThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
t.setPriority(Thread.MIN_PRIORITY);
return t;
}
});
// add a column with the same name and different type to the view in a separate thread
// (which will take some time to complete)
Future<Exception> future = executorService.submit(new AddColumnRunnable(fullViewName1));
// wait till the thread makes the rpc to add the column
boolean result = latch1.await(2, TimeUnit.MINUTES);
if (!result) {
fail("The create view rpc look too long");
}
tableDdl = "ALTER TABLE " + fullTableName + " ADD v3 INTEGER";
try {
// add the same column to the base table with a different type
conn.createStatement().execute(tableDdl);
fail("Creating a view while concurrently dropping the base table should fail");
} catch (ConcurrentTableMutationException e) {
}
latch2.countDown();
Exception e = future.get();
assertNull(e);
// add a the same column to the another view to ensure that the cell used
// to prevent concurrent modifications was removed
ddl = "CREATE VIEW " + fullViewName2 + " (v2 VARCHAR) AS SELECT * FROM "
+ fullTableName + " WHERE k = 6";
conn.createStatement().execute(ddl);
tableDdl = "ALTER VIEW " + fullViewName2 + " ADD v3 INTEGER";
conn.createStatement().execute(tableDdl);
}
}
@Test
public void testConcurrentAddDifferentColumn() throws Exception {
try (Connection conn = DriverManager.getConnection(getUrl())) {
String fullTableName = SchemaUtil.getTableName(SCHEMA1, generateUniqueName());
String fullViewName1 = SLOW_VIEWNAME_PREFIX + "_" + generateUniqueName();
String fullViewName2 = SchemaUtil.getTableName(SCHEMA3, generateUniqueName());
String fullViewName3 = SchemaUtil.getTableName(SCHEMA4, generateUniqueName());
// create base table
String tableDdl =
"CREATE TABLE " + fullTableName + " (k INTEGER NOT NULL PRIMARY KEY, v1 DATE)"
+ tableDDLOptions;
conn.createStatement().execute(tableDdl);
// create two views
String ddl =
"CREATE VIEW " + fullViewName1 + " (v2 VARCHAR) AS SELECT * FROM "
+ fullTableName + " WHERE k = 6";
conn.createStatement().execute(ddl);
ddl =
"CREATE VIEW " + fullViewName3 + " (v2 VARCHAR) AS SELECT * FROM "
+ fullTableName + " WHERE k = 7";
conn.createStatement().execute(ddl);
latch1 = new CountDownLatch(1);
latch2 = new CountDownLatch(1);
ExecutorService executorService = Executors.newFixedThreadPool(1, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
t.setPriority(Thread.MIN_PRIORITY);
return t;
}
});
// add a column to a view in a separate thread (we slow this operation down)
Future<Exception> future = executorService.submit(new AddColumnRunnable(fullViewName1));
// wait till the thread makes the rpc to add the column
boolean result = latch1.await(2, TimeUnit.MINUTES);
if (!result) {
fail("The alter view rpc look too long");
}
tableDdl = "ALTER VIEW " + fullViewName3 + " ADD v4 INTEGER";
try {
// add a column to another view
conn.createStatement().execute(tableDdl);
if (columnEncoded) {
// this should fail as the previous add column is still not complete
fail(
"Adding columns to two different views concurrently where the base table uses encoded column should fail");
}
} catch (ConcurrentTableMutationException e) {
if (!columnEncoded) {
// this should not fail as we don't need to update the parent table for non
// column encoded tables
fail(
"Adding columns to two different views concurrently where the base table does not use encoded columns should succeed");
}
}
latch2.countDown();
Exception e = future.get();
// if the base table uses column encoding then the add column operation for fullViewName1 fails
assertNull(e);
// add a the same column to the another view to ensure that the cell used
// to prevent concurrent modifications was removed
ddl = "CREATE VIEW " + fullViewName2 + " (v2 VARCHAR) AS SELECT * FROM "
+ fullTableName + " WHERE k = 6";
conn.createStatement().execute(ddl);
tableDdl = "ALTER VIEW " + fullViewName2 + " ADD v3 INTEGER";
conn.createStatement().execute(tableDdl);
}
}
@Test
public void testDisallowCreatingViewsOnSystemTable() throws SQLException {
try (Connection conn = DriverManager.getConnection(getUrl())) {
String viewDDL = "CREATE VIEW " + generateUniqueName() + " AS SELECT * FROM " +
"SYSTEM.CATALOG";
try {
conn.createStatement().execute(viewDDL);
fail("Should have thrown an exception");
} catch (SQLException sqlE) {
assertEquals("Expected a different Error code",
SQLExceptionCode.CANNOT_CREATE_VIEWS_ON_SYSTEM_TABLES.getErrorCode(),
sqlE.getErrorCode());
}
}
}
private class CreateViewRunnable implements Callable<Exception> {
private final String fullTableName;
private final String fullViewName;
public CreateViewRunnable(String fullTableName, String fullViewName) {
this.fullTableName = fullTableName;
this.fullViewName = fullViewName;
}
@Override
public Exception call() throws Exception {
Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
String ddl =
"CREATE VIEW " + fullViewName + " (v2 VARCHAR) AS SELECT * FROM "
+ fullTableName + " WHERE k = 5";
conn.createStatement().execute(ddl);
} catch (SQLException e) {
return e;
}
return null;
}
}
private class AddColumnRunnable implements Callable<Exception> {
private final String fullViewName;
public AddColumnRunnable(String fullViewName) {
this.fullViewName = fullViewName;
}
@Override
public Exception call() throws Exception {
Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
String ddl = "ALTER VIEW " + fullViewName + " ADD v3 CHAR(15)";
conn.createStatement().execute(ddl);
} catch (SQLException e) {
return e;
}
return null;
}
}
}