This is an automated email from the ASF dual-hosted git repository.

sanjeet006py pushed a commit to branch 5.3
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/5.3 by this push:
     new 270de27713 PHOENIX-7788 CREATE TABLE fails with 
TableNotEnabledException when underlying HBase table exists but is disabled 
(#2594) (#2636)
270de27713 is described below

commit 270de277134ba49e5b27504aa6ab6736fbad51da
Author: ishitadixit-dot <[email protected]>
AuthorDate: Thu Oct 29 12:15:54 2026 +0530

    PHOENIX-7788 CREATE TABLE fails with TableNotEnabledException when 
underlying HBase table exists but is disabled (#2594) (#2636)
---
 .../phoenix/query/ConnectionQueryServicesImpl.java |  61 ++++++++++
 .../org/apache/phoenix/end2end/CreateTableIT.java  | 124 +++++++++++++++++++++
 .../query/ConnectionQueryServicesImplTest.java     |  72 ++++++++++++
 3 files changed, 257 insertions(+)

diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java
index 1f504a8298..426570b274 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/query/ConnectionQueryServicesImpl.java
@@ -165,6 +165,7 @@ import org.apache.hadoop.hbase.NamespaceNotFoundException;
 import org.apache.hadoop.hbase.ServerName;
 import org.apache.hadoop.hbase.TableExistsException;
 import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.TableNotDisabledException;
 import org.apache.hadoop.hbase.TableNotEnabledException;
 import org.apache.hadoop.hbase.client.Admin;
 import org.apache.hadoop.hbase.client.Append;
@@ -1912,6 +1913,23 @@ public class ConnectionQueryServicesImpl extends 
DelegateQueryServices
         }
       }
 
+      // PHOENIX-7788: recover an orphaned disabled physical table before 
modifyTable runs on it.
+      if (
+        tableExist && tableType == PTableType.TABLE
+          && admin.isTableDisabled(TableName.valueOf(physicalTableName))
+      ) {
+        byte[] viewIndexMarker = existingDesc == null
+          ? null
+          : existingDesc.getValue(MetaDataUtil.IS_VIEW_INDEX_TABLE_PROP_BYTES);
+        // Classify by the descriptor marker, falling back to the _IDX_ name 
prefix for older
+        // view-index tables that predate the marker.
+        boolean hasViewIndexMarker = viewIndexMarker != null
+          && Boolean.TRUE.equals(PBoolean.INSTANCE.toObject(viewIndexMarker));
+        boolean isViewIndexTable =
+          hasViewIndexMarker || 
MetaDataUtil.isViewIndex(Bytes.toString(physicalTableName));
+        reenableOrphanedDisabledHBaseTable(physicalTableName, 
isViewIndexTable, admin);
+      }
+
       TableDescriptorBuilder newDesc =
         generateTableDescriptor(physicalTableName, parentPhysicalTableName, 
existingDesc, tableType,
           props, families, splits, isNamespaceMapped);
@@ -2449,6 +2467,49 @@ public class ConnectionQueryServicesImpl extends 
DelegateQueryServices
     }
   }
 
+  private void enableTable(Admin admin, TableName tableName) throws 
IOException {
+    try {
+      admin.enableTable(tableName);
+    } catch (TableNotDisabledException e) {
+      LOGGER.info("Table already enabled, continuing with next steps", e);
+    }
+  }
+
+  /**
+   * PHOENIX-7788: re-enable a disabled physical HBase table if SYSTEM.CATALOG 
has no row for it. If
+   * metadata exists, leave it disabled — an admin may have disabled the 
registered table. Caller
+   * must have already confirmed the physical table exists and is disabled.
+   */
+  private void reenableOrphanedDisabledHBaseTable(byte[] 
physicalTableNameBytes,
+    boolean isViewIndexTable, Admin admin) throws SQLException {
+    TableName physicalTableName = TableName.valueOf(physicalTableNameBytes);
+    // For a view-index physical table, resolve to the base table name; 
otherwise the physical
+    // name is itself the metadata key.
+    String physicalName = Bytes.toString(physicalTableNameBytes);
+    String metadataName =
+      isViewIndexTable ? MetaDataUtil.getViewIndexUserTableName(physicalName) 
: physicalName;
+    byte[] schemaBytes = 
Bytes.toBytes(SchemaUtil.getSchemaNameFromFullName(metadataName));
+    byte[] tableBytes = 
Bytes.toBytes(SchemaUtil.getTableNameFromFullName(metadataName));
+    // No-cache read straight from the server; a stale/absent client-cache 
entry must not drive
+    // the orphan decision.
+    PTable existingTable = getTable(null, schemaBytes, tableBytes, 
HConstants.LATEST_TIMESTAMP,
+      HConstants.LATEST_TIMESTAMP).getTable();
+    if (existingTable != null) {
+      LOGGER.info(
+        "Physical HBase table {} is disabled but {} has metadata for it; "
+          + "leaving it disabled to preserve any intentional admin action.",
+        physicalTableName, PhoenixDatabaseMetaData.SYSTEM_CATALOG_NAME);
+      return;
+    }
+    LOGGER.info("Re-enabling orphaned disabled HBase table {} during CREATE 
TABLE",
+      physicalTableName);
+    try {
+      enableTable(admin, physicalTableName);
+    } catch (IOException e) {
+      throw ClientUtil.parseServerException(e);
+    }
+  }
+
   private boolean ensureViewIndexTableDropped(byte[] physicalTableName, long 
timestamp)
     throws SQLException {
     byte[] physicalIndexName = 
MetaDataUtil.getViewIndexPhysicalName(physicalTableName);
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CreateTableIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CreateTableIT.java
index f4f57bafc8..8955288dac 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CreateTableIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CreateTableIT.java
@@ -82,6 +82,7 @@ import org.apache.phoenix.schema.TableNotFoundException;
 import org.apache.phoenix.util.ByteUtil;
 import org.apache.phoenix.util.EnvironmentEdgeManager;
 import org.apache.phoenix.util.IndexUtil;
+import org.apache.phoenix.util.MetaDataUtil;
 import org.apache.phoenix.util.PropertiesUtil;
 import org.apache.phoenix.util.QueryUtil;
 import org.apache.phoenix.util.ReadOnlyProps;
@@ -1739,6 +1740,129 @@ public class CreateTableIT extends 
ParallelStatsDisabledIT {
     }
   }
 
+  @Test
+  public void testCreateTableReenablesExistingDisabledHBaseTable() throws 
Exception {
+    String tableName = generateUniqueName();
+    String ddl = "CREATE TABLE " + tableName
+      + " (K VARCHAR NOT NULL PRIMARY KEY, V VARCHAR) 
COLUMN_ENCODED_BYTES=NONE";
+    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      conn.createStatement().execute(ddl);
+    }
+
+    ConnectionQueryServices services = 
driver.getConnectionQueryServices(getUrl(), props);
+    TableName hbaseTableName = TableName.valueOf(tableName);
+
+    // Simulate the "failed drop" state: Phoenix metadata is gone but the 
physical HBase
+    // table still exists and has been left disabled.
+    try (Admin admin = services.getAdmin();
+      Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      admin.disableTable(hbaseTableName);
+      assertTrue(admin.isTableDisabled(hbaseTableName));
+
+      conn.createStatement()
+        .executeUpdate("DELETE FROM SYSTEM.CATALOG WHERE TABLE_NAME = '" + 
tableName + "'");
+      conn.commit();
+      conn.unwrap(PhoenixConnection.class).getQueryServices().clearCache();
+    }
+
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      conn.createStatement().execute(ddl);
+    }
+
+    try (Admin admin = services.getAdmin()) {
+      assertFalse("HBase table should have been re-enabled by CREATE TABLE",
+        admin.isTableDisabled(hbaseTableName));
+      assertTrue(admin.isTableEnabled(hbaseTableName));
+    }
+
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      conn.setAutoCommit(true);
+      conn.createStatement().execute("UPSERT INTO " + tableName + " VALUES 
('a', 'b')");
+      try (ResultSet rs =
+        conn.createStatement().executeQuery("SELECT V FROM " + tableName + " 
WHERE K = 'a'")) {
+        assertTrue(rs.next());
+        assertEquals("b", rs.getString(1));
+        assertFalse(rs.next());
+      }
+    }
+  }
+
+  // Test for PHOENIX-7788: guard must be gated on metadata absence, not on 
physical state
+  // alone, so an intentional admin disable of a Phoenix-registered table is 
not silently
+  // undone by CREATE TABLE IF NOT EXISTS.
+  @Test
+  public void 
testCreateTableIfNotExistsDoesNotReenableDisabledTableWithMetadata()
+    throws Exception {
+    String tableName = generateUniqueName();
+    String ddl = "CREATE TABLE " + tableName
+      + " (K VARCHAR NOT NULL PRIMARY KEY, V VARCHAR) 
COLUMN_ENCODED_BYTES=NONE";
+    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      conn.createStatement().execute(ddl);
+    }
+
+    ConnectionQueryServices services = 
driver.getConnectionQueryServices(getUrl(), props);
+    TableName hbaseTableName = TableName.valueOf(tableName);
+
+    // Simulate an admin disabling a registered Phoenix table for maintenance. 
Metadata
+    // rows in SYSTEM.CATALOG are left intact.
+    try (Admin admin = services.getAdmin()) {
+      admin.disableTable(hbaseTableName);
+      assertTrue(admin.isTableDisabled(hbaseTableName));
+    }
+
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      conn.unwrap(PhoenixConnection.class).getQueryServices().clearCache();
+      conn.createStatement().execute("CREATE TABLE IF NOT EXISTS " + tableName
+        + " (K VARCHAR NOT NULL PRIMARY KEY, V VARCHAR) 
COLUMN_ENCODED_BYTES=NONE");
+    }
+
+    try (Admin admin = services.getAdmin()) {
+      assertTrue(
+        "CREATE TABLE IF NOT EXISTS must not re-enable a disabled table with 
existing metadata",
+        admin.isTableDisabled(hbaseTableName));
+    }
+  }
+
+  // PHOENIX-7788: when the base table still has metadata, a disabled shared 
view-index physical
+  // table must be left disabled -- an admin may have disabled it 
intentionally, and re-creating
+  // the base table must not silently undo that (same conservative rule as 
plain base tables).
+  @Test
+  public void 
testCreateTableDoesNotReenableDisabledViewIndexTableWhenBaseTableExists()
+    throws Exception {
+    String baseTable = generateUniqueName();
+    String ddl = "CREATE TABLE IF NOT EXISTS " + baseTable + " (T_ID VARCHAR 
NOT NULL, "
+      + "K VARCHAR NOT NULL, V VARCHAR CONSTRAINT PK PRIMARY KEY (T_ID, K)) "
+      + "MULTI_TENANT=true, COLUMN_ENCODED_BYTES=NONE";
+    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      conn.createStatement().execute(ddl);
+    }
+
+    ConnectionQueryServices services = 
driver.getConnectionQueryServices(getUrl(), props);
+    TableName physicalIndexTable =
+      TableName.valueOf(MetaDataUtil.getViewIndexPhysicalName(baseTable));
+
+    try (Admin admin = services.getAdmin()) {
+      admin.disableTable(physicalIndexTable);
+      assertTrue(admin.isTableDisabled(physicalIndexTable));
+    }
+
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      conn.unwrap(PhoenixConnection.class).getQueryServices().clearCache();
+      conn.createStatement().execute(ddl);
+    }
+
+    try (Admin admin = services.getAdmin()) {
+      assertTrue("Shared view-index table with a live base table must stay 
disabled",
+        admin.isTableDisabled(physicalIndexTable));
+    }
+  }
+
   public static long verifyLastDDLTimestamp(String tableFullName, long 
startTS, Connection conn)
     throws SQLException {
     long endTS = EnvironmentEdgeManager.currentTimeMillis();
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java
index a6a55a5081..8acd7c9c6b 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/query/ConnectionQueryServicesImplTest.java
@@ -35,16 +35,20 @@ import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import java.io.IOException;
 import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
 import java.sql.SQLException;
 import java.util.Collections;
@@ -58,6 +62,7 @@ import java.util.concurrent.TimeUnit;
 import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.HRegionLocation;
 import org.apache.hadoop.hbase.TableName;
+import org.apache.hadoop.hbase.TableNotDisabledException;
 import org.apache.hadoop.hbase.TableNotEnabledException;
 import org.apache.hadoop.hbase.TableNotFoundException;
 import org.apache.hadoop.hbase.client.Admin;
@@ -69,10 +74,13 @@ import org.apache.hadoop.hbase.client.Table;
 import org.apache.hadoop.hbase.client.TableDescriptor;
 import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
 import org.apache.phoenix.SystemExitRule;
+import 
org.apache.phoenix.coprocessorclient.MetaDataProtocol.MetaDataMutationResult;
+import org.apache.phoenix.coprocessorclient.MetaDataProtocol.MutationCode;
 import org.apache.phoenix.exception.PhoenixIOException;
 import org.apache.phoenix.jdbc.ConnectionInfo;
 import org.apache.phoenix.jdbc.PhoenixDatabaseMetaData;
 import org.apache.phoenix.monitoring.GlobalClientMetrics;
+import org.apache.phoenix.schema.PName;
 import org.apache.phoenix.util.ReadOnlyProps;
 import org.junit.Before;
 import org.junit.ClassRule;
@@ -408,4 +416,68 @@ public class ConnectionQueryServicesImplTest {
     verify(mockAdmin, 
Mockito.times(1)).deleteTable(TableName.valueOf("TEST_TABLE"));
     verify(mockConn).getAdmin();
   }
+
+  @Test
+  public void testEnableTableAlreadyEnabledSwallowsException() throws 
Exception {
+    // PHOENIX-7788: enableTable helper must swallow TableNotDisabledException,
+    // so a concurrent client that already re-enabled the table does not fail 
the CREATE.
+    TableName tableName = TableName.valueOf("TEST_TABLE");
+    doThrow(new 
TableNotDisabledException(tableName)).when(mockAdmin).enableTable(tableName);
+    invokeEnableTable(mockCqs, mockAdmin, tableName);
+    verify(mockAdmin, Mockito.times(1)).enableTable(tableName);
+  }
+
+  private static void invokeEnableTable(ConnectionQueryServicesImpl cqs, Admin 
admin,
+    TableName tableName) throws Exception {
+    Method m = 
ConnectionQueryServicesImpl.class.getDeclaredMethod("enableTable", Admin.class,
+      TableName.class);
+    m.setAccessible(true);
+    m.invoke(cqs, admin, tableName);
+  }
+
+  @Test
+  public void 
testReenableOrphanedDisabledHBaseTableDerivesLogicalNameForNamespaceMapped()
+    throws Exception {
+    // PHOENIX-7788: for a namespace-mapped physical name "MYSCHEMA:MYTABLE" 
the metadata
+    // lookup must be against logical schema="MYSCHEMA", table="MYTABLE".
+    byte[] name = "MYSCHEMA:MYTABLE".getBytes(StandardCharsets.UTF_8);
+    MetaDataMutationResult notFound =
+      new MetaDataMutationResult(MutationCode.TABLE_NOT_FOUND, 0L, null);
+    doReturn(notFound).when(mockCqs).getTable(Mockito.<PName> any(), 
any(byte[].class),
+      any(byte[].class), anyLong(), anyLong());
+    invokeReenableOrphanedDisabledHBaseTable(mockCqs, name, mockAdmin);
+    verify(mockCqs).getTable(Mockito.<PName> any(), 
eq("MYSCHEMA".getBytes(StandardCharsets.UTF_8)),
+      eq("MYTABLE".getBytes(StandardCharsets.UTF_8)), anyLong(), anyLong());
+  }
+
+  @Test
+  public void 
testReenableOrphanedDisabledHBaseTableDerivesLogicalNameForNonNamespaceMapped()
+    throws Exception {
+    // PHOENIX-7788: for a non-namespace-mapped physical name 
"MYSCHEMA.MYTABLE" the metadata
+    // lookup must be against logical schema="MYSCHEMA", table="MYTABLE".
+    byte[] name = "MYSCHEMA.MYTABLE".getBytes(StandardCharsets.UTF_8);
+    MetaDataMutationResult notFound =
+      new MetaDataMutationResult(MutationCode.TABLE_NOT_FOUND, 0L, null);
+    doReturn(notFound).when(mockCqs).getTable(Mockito.<PName> any(), 
any(byte[].class),
+      any(byte[].class), anyLong(), anyLong());
+    invokeReenableOrphanedDisabledHBaseTable(mockCqs, name, mockAdmin);
+    verify(mockCqs).getTable(Mockito.<PName> any(), 
eq("MYSCHEMA".getBytes(StandardCharsets.UTF_8)),
+      eq("MYTABLE".getBytes(StandardCharsets.UTF_8)), anyLong(), anyLong());
+  }
+
+  private static void 
invokeReenableOrphanedDisabledHBaseTable(ConnectionQueryServicesImpl cqs,
+    byte[] physicalTableNameBytes, Admin admin) throws Exception {
+    Method m = ConnectionQueryServicesImpl.class.getDeclaredMethod(
+      "reenableOrphanedDisabledHBaseTable", byte[].class, boolean.class, 
Admin.class);
+    m.setAccessible(true);
+    try {
+      // These cases exercise base (non-view-index) physical tables, so 
isViewIndexTable is false.
+      m.invoke(cqs, physicalTableNameBytes, false, admin);
+    } catch (InvocationTargetException e) {
+      if (e.getCause() instanceof Exception) {
+        throw (Exception) e.getCause();
+      }
+      throw e;
+    }
+  }
 }

Reply via email to