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

yashmayya pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new c7ce654d81 Schema and table config deletion to validate with logical 
table ref table names (#15900)
c7ce654d81 is described below

commit c7ce654d819582a641dcede5b3e177e3a038d9da
Author: Abhishek Bafna <[email protected]>
AuthorDate: Tue Jun 3 19:34:09 2025 +0530

    Schema and table config deletion to validate with logical table ref table 
names (#15900)
---
 .../common/utils/LogicalTableConfigUtils.java      | 41 +++++++++++
 .../api/resources/PinotSchemaRestletResource.java  |  7 ++
 .../api/resources/PinotTableRestletResource.java   | 17 +++++
 .../api/resources/TableConfigsRestletResource.java | 20 ++++++
 .../api/PinotSchemaRestletResourceTest.java        | 40 +++++++++++
 .../api/PinotTableRestletResourceTest.java         | 80 ++++++++++++++++++++--
 .../api/TableConfigsRestletResourceTest.java       | 60 ++++++++++++++++
 .../resources/PinotLogicalTableResourceTest.java   | 62 ++++++++++++++---
 .../pinot/controller/helix/ControllerTest.java     |  9 +++
 9 files changed, 324 insertions(+), 12 deletions(-)

diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/LogicalTableConfigUtils.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/LogicalTableConfigUtils.java
index 42d5e43f9f..1f9d948728 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/utils/LogicalTableConfigUtils.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/utils/LogicalTableConfigUtils.java
@@ -31,6 +31,7 @@ import org.apache.helix.zookeeper.datamodel.ZNRecord;
 import org.apache.pinot.common.metadata.ZKMetadataProvider;
 import org.apache.pinot.spi.config.table.QueryConfig;
 import org.apache.pinot.spi.config.table.QuotaConfig;
+import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.data.LogicalTableConfig;
 import org.apache.pinot.spi.data.PhysicalTableConfig;
 import org.apache.pinot.spi.data.TimeBoundaryConfig;
@@ -164,12 +165,40 @@ public class LogicalTableConfigUtils {
       }
     }
 
+    // validate ref offline table name is offline table type
+    if (logicalTableConfig.getRefOfflineTableName() != null
+        && 
!TableNameBuilder.isOfflineTableResource(logicalTableConfig.getRefOfflineTableName()))
 {
+      throw new IllegalArgumentException(
+          "Invalid logical table. Reason: 'refOfflineTableName' should be an 
offline table type");
+    }
+
+    // validate ref realtime table name is realtime table type
+    if (logicalTableConfig.getRefRealtimeTableName() != null
+        && 
!TableNameBuilder.isRealtimeTableResource(logicalTableConfig.getRefRealtimeTableName()))
 {
+      throw new IllegalArgumentException(
+          "Invalid logical table. Reason: 'refRealtimeTableName' should be a 
realtime table type");
+    }
+
     // validate ref offline table name is not null or empty when offline 
tables exists
     if (!offlineTableNames.isEmpty() && 
StringUtils.isEmpty(logicalTableConfig.getRefOfflineTableName())) {
       throw new IllegalArgumentException(
           "Invalid logical table. Reason: 'refOfflineTableName' should not be 
null or empty when offline table exists");
     }
 
+    // validate ref offline table name is null when offline tables is empty
+    if (offlineTableNames.isEmpty() && 
!StringUtils.isEmpty(logicalTableConfig.getRefOfflineTableName())) {
+      throw new IllegalArgumentException(
+          "Invalid logical table. Reason: 'refOfflineTableName' should be null 
or empty when offline tables do not "
+              + "exist");
+    }
+
+    // validate ref realtime table name is null when realtime tables is empty
+    if (realtimeTableNames.isEmpty() && 
!StringUtils.isEmpty(logicalTableConfig.getRefRealtimeTableName())) {
+      throw new IllegalArgumentException(
+          "Invalid logical table. Reason: 'refRealtimeTableName' should be 
null or empty when realtime tables do not "
+              + "exist");
+    }
+
     // validate ref realtime table name is not null or empty when realtime 
tables exists
     if (!realtimeTableNames.isEmpty() && 
StringUtils.isEmpty(logicalTableConfig.getRefRealtimeTableName())) {
       throw new IllegalArgumentException(
@@ -229,4 +258,16 @@ public class LogicalTableConfigUtils {
           "Invalid logical table. Reason: 'timeBoundaryConfig.parameters' 
should not be null or empty");
     }
   }
+
+  public static boolean checkPhysicalTableRefExists(LogicalTableConfig 
logicalTableConfig, String tableName) {
+    Set<String> physicalTableNames = 
logicalTableConfig.getPhysicalTableConfigMap().keySet();
+    TableType tableType = 
TableNameBuilder.getTableTypeFromTableName(tableName);
+    if (tableType == null) {
+      String offlineTableName = 
TableNameBuilder.OFFLINE.tableNameWithType(tableName);
+      String realtimeTableName = 
TableNameBuilder.REALTIME.tableNameWithType(tableName);
+      return physicalTableNames.contains(offlineTableName) || 
physicalTableNames.contains(realtimeTableName);
+    } else {
+      return physicalTableNames.contains(tableName);
+    }
+  }
 }
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSchemaRestletResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSchemaRestletResource.java
index 2e1d0d2c88..f2fcaa5093 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSchemaRestletResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSchemaRestletResource.java
@@ -57,6 +57,7 @@ import 
org.apache.pinot.common.exception.SchemaAlreadyExistsException;
 import org.apache.pinot.common.exception.SchemaBackwardIncompatibleException;
 import org.apache.pinot.common.exception.SchemaNotFoundException;
 import org.apache.pinot.common.exception.TableNotFoundException;
+import org.apache.pinot.common.metadata.ZKMetadataProvider;
 import org.apache.pinot.common.metrics.ControllerMeter;
 import org.apache.pinot.common.metrics.ControllerMetrics;
 import org.apache.pinot.common.utils.DatabaseUtils;
@@ -551,6 +552,12 @@ public class PinotSchemaRestletResource {
             Response.Status.CONFLICT);
       }
     }
+    // If the schema is associated with logical table, we should not delete it.
+    if 
(ZKMetadataProvider.isLogicalTableExists(_pinotHelixResourceManager.getPropertyStore(),
 schemaName)) {
+      throw new ControllerApplicationException(LOGGER,
+          String.format("Cannot delete schema %s, as it is associated with 
logical table", schemaName),
+          Response.Status.CONFLICT);
+    }
 
     LOGGER.info("Trying to delete schema {}", schemaName);
     if (_pinotHelixResourceManager.deleteSchema(schemaName)) {
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
index 3302d9c7a5..30d8401165 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java
@@ -83,6 +83,7 @@ import 
org.apache.pinot.common.response.server.TableIndexMetadataResponse;
 import org.apache.pinot.common.restlet.resources.TableSegmentValidationInfo;
 import org.apache.pinot.common.restlet.resources.ValidDocIdsType;
 import org.apache.pinot.common.utils.DatabaseUtils;
+import org.apache.pinot.common.utils.LogicalTableConfigUtils;
 import org.apache.pinot.common.utils.helix.HelixHelper;
 import org.apache.pinot.controller.ControllerConf;
 import org.apache.pinot.controller.api.access.AccessControlFactory;
@@ -116,6 +117,7 @@ import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableStatsHumanReadable;
 import org.apache.pinot.spi.config.table.TableStatus;
 import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.LogicalTableConfig;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.utils.CommonConstants;
 import org.apache.pinot.spi.utils.Enablement;
@@ -422,6 +424,7 @@ public class PinotTableRestletResource {
     List<String> tablesDeleted = new LinkedList<>();
     try {
       tableName = DatabaseUtils.translateTableName(tableName, headers);
+      validateLogicalTableReference(tableName, tableType);
       boolean tableExist = false;
       if (verifyTableType(tableName, tableType, TableType.OFFLINE)) {
         tableExist = _pinotHelixResourceManager.hasOfflineTable(tableName);
@@ -466,6 +469,20 @@ public class PinotTableRestletResource {
     return typeFromTableName == null || typeFromTableName == expectedType;
   }
 
+  private void validateLogicalTableReference(String tableName, TableType 
tableType) {
+    String tableNameWithType =
+        tableType == null ? tableName : 
TableNameBuilder.forType(tableType).tableNameWithType(tableName);
+    List<LogicalTableConfig> allLogicalTableConfigs =
+        
ZKMetadataProvider.getAllLogicalTableConfigs(_pinotHelixResourceManager.getPropertyStore());
+    for (LogicalTableConfig logicalTableConfig : allLogicalTableConfigs) {
+      if 
(LogicalTableConfigUtils.checkPhysicalTableRefExists(logicalTableConfig, 
tableNameWithType)) {
+        throw new ControllerApplicationException(LOGGER,
+            String.format("Cannot delete table config: %s because it is 
referenced in logical table: %s",
+                tableName, logicalTableConfig.getTableName()), 
Response.Status.CONFLICT);
+      }
+    }
+  }
+
   @PUT
   @Path("/tables/{tableName}")
   @Authorize(targetType = TargetType.TABLE, paramName = "tableName", action = 
Actions.Table.UPDATE_TABLE_CONFIG)
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java
index 3c3d6e5b3d..3a15ecb30e 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java
@@ -49,9 +49,11 @@ import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.tuple.Pair;
+import org.apache.pinot.common.metadata.ZKMetadataProvider;
 import org.apache.pinot.common.metrics.ControllerMeter;
 import org.apache.pinot.common.metrics.ControllerMetrics;
 import org.apache.pinot.common.utils.DatabaseUtils;
+import org.apache.pinot.common.utils.LogicalTableConfigUtils;
 import org.apache.pinot.controller.ControllerConf;
 import org.apache.pinot.controller.api.access.AccessControl;
 import org.apache.pinot.controller.api.access.AccessControlFactory;
@@ -73,6 +75,7 @@ import org.apache.pinot.segment.local.utils.SchemaUtils;
 import org.apache.pinot.segment.local.utils.TableConfigUtils;
 import org.apache.pinot.spi.config.TableConfigs;
 import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.data.LogicalTableConfig;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
@@ -282,7 +285,24 @@ public class TableConfigsRestletResource {
       @ApiParam(value = "TableConfigs name i.e. raw table name", required = 
true) @PathParam("tableName")
       String tableName, @Context HttpHeaders headers) {
     try {
+      if (TableNameBuilder.isOfflineTableResource(tableName) || 
TableNameBuilder.isRealtimeTableResource(tableName)) {
+        throw new ControllerApplicationException(LOGGER, "Invalid table name: 
" + tableName + ". Use raw table name.",
+            Response.Status.BAD_REQUEST);
+      }
+
       tableName = DatabaseUtils.translateTableName(tableName, headers);
+
+      // Validate the table is not referenced in any logical table config.
+      List<LogicalTableConfig> allLogicalTableConfigs =
+          
ZKMetadataProvider.getAllLogicalTableConfigs(_pinotHelixResourceManager.getPropertyStore());
+      for (LogicalTableConfig logicalTableConfig : allLogicalTableConfigs) {
+        if 
(LogicalTableConfigUtils.checkPhysicalTableRefExists(logicalTableConfig, 
tableName)) {
+          throw new ControllerApplicationException(LOGGER,
+              String.format("Cannot delete table config: %s because it is 
referenced in logical table: %s",
+                  tableName, logicalTableConfig.getTableName()), 
Response.Status.CONFLICT);
+        }
+      }
+
       boolean tableExists =
           _pinotHelixResourceManager.hasRealtimeTable(tableName) || 
_pinotHelixResourceManager.hasOfflineTable(
               tableName);
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotSchemaRestletResourceTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotSchemaRestletResourceTest.java
index 15fa17c436..10af6b046b 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotSchemaRestletResourceTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotSchemaRestletResourceTest.java
@@ -19,17 +19,23 @@
 package org.apache.pinot.controller.api;
 
 import java.io.IOException;
+import java.util.List;
 import org.apache.pinot.common.utils.SimpleHttpResponse;
 import org.apache.pinot.controller.helix.ControllerTest;
+import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.data.DimensionFieldSpec;
 import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.LogicalTableConfig;
 import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.utils.builder.ControllerRequestURLBuilder;
+import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
 import static org.testng.Assert.fail;
 
 
@@ -247,6 +253,40 @@ public class PinotSchemaRestletResourceTest {
         
"{\"unrecognizedProperties\":{\"/illegalKey1\":1},\"status\":\"transcript2 
successfully added\"}");
   }
 
+  @Test
+  public void testSchemaDeletionWithLogicalTable()
+      throws IOException {
+    String logicalTableName = "logical_table";
+    String physicalTable = "physical_table";
+    ControllerRequestURLBuilder urlBuilder = 
TEST_INSTANCE.getControllerRequestURLBuilder();
+    TEST_INSTANCE.addDummySchema(physicalTable);
+    
TEST_INSTANCE.addTableConfig(ControllerTest.createDummyTableConfig(physicalTable,
 TableType.OFFLINE));
+    TEST_INSTANCE.addDummySchema(logicalTableName);
+
+    // Create a logical table
+    String logicalTableUrl = urlBuilder.forLogicalTableCreate();
+    LogicalTableConfig logicalTableConfig =
+        ControllerTest.getDummyLogicalTableConfig(logicalTableName,
+            
List.of(TableNameBuilder.OFFLINE.tableNameWithType(physicalTable)), 
"DefaultTenant");
+    String response = ControllerTest.sendPostRequest(logicalTableUrl, 
logicalTableConfig.toSingleLineJsonString());
+    assertEquals(response,
+        "{\"unrecognizedProperties\":{},\"status\":\"logical_table logical 
table successfully added.\"}");
+
+    // Delete schema should fail because logical table exists
+    String deleteSchemaUrl = urlBuilder.forSchemaDelete(logicalTableName);
+    String msg = expectThrows(IOException.class, () -> 
ControllerTest.sendDeleteRequest(deleteSchemaUrl)).getMessage();
+    assertTrue(msg.contains("Cannot delete schema logical_table, as it is 
associated with logical table"), msg);
+
+    // Delete logical table
+    String logicalTableDeleteUrl = 
urlBuilder.forLogicalTableDelete(logicalTableName);
+    response = ControllerTest.sendDeleteRequest(logicalTableDeleteUrl);
+    assertEquals(response, "{\"status\":\"logical_table logical table 
successfully deleted.\"}");
+
+    // Delete schema should succeed now
+    response = ControllerTest.sendDeleteRequest(deleteSchemaUrl);
+    assertEquals(response, "{\"status\":\"Schema logical_table deleted\"}");
+  }
+
   @AfterClass
   public void tearDown() {
     TEST_INSTANCE.cleanup();
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotTableRestletResourceTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotTableRestletResourceTest.java
index fc21084328..056228c38a 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotTableRestletResourceTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotTableRestletResourceTest.java
@@ -51,13 +51,21 @@ import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.stream.StreamConfig;
 import org.apache.pinot.spi.utils.JsonUtils;
 import org.apache.pinot.spi.utils.StringUtil;
+import org.apache.pinot.spi.utils.builder.ControllerRequestURLBuilder;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import static org.testng.Assert.*;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.expectThrows;
+import static org.testng.Assert.fail;
 
 
 /**
@@ -79,15 +87,19 @@ public class PinotTableRestletResourceTest extends 
ControllerTest {
     
_offlineBuilder.setTableName(OFFLINE_TABLE_NAME).setTimeColumnName("timeColumn").setTimeType("DAYS")
         .setRetentionTimeUnit("DAYS").setRetentionTimeValue("5");
 
-    // add schema for realtime table
-    DEFAULT_INSTANCE.addDummySchema(REALTIME_TABLE_NAME);
-    DEFAULT_INSTANCE.addDummySchema(OFFLINE_TABLE_NAME);
     StreamConfig streamConfig = 
FakeStreamConfigUtils.getDefaultLowLevelStreamConfigs();
     
_realtimeBuilder.setTableName(REALTIME_TABLE_NAME).setTimeColumnName("timeColumn").setTimeType("DAYS")
         .setRetentionTimeUnit("DAYS").setRetentionTimeValue("5")
         .setStreamConfigs(streamConfig.getStreamConfigsMap());
   }
 
+  @BeforeMethod
+  public void beforeMethod()
+      throws Exception {
+    DEFAULT_INSTANCE.addDummySchema(REALTIME_TABLE_NAME);
+    DEFAULT_INSTANCE.addDummySchema(OFFLINE_TABLE_NAME);
+  }
+
   private void registerMinionTasks() {
     PinotTaskManager taskManager = 
DEFAULT_INSTANCE.getControllerStarter().getTaskManager();
     taskManager.registerTaskGenerator(new BaseTaskGenerator() {
@@ -634,6 +646,59 @@ public class PinotTableRestletResourceTest extends 
ControllerTest {
     assertEquals(deleteResponse, "{\"status\":\"Tables: [table3_OFFLINE] 
deleted\"}");
   }
 
+  @Test(dataProvider = "tableTypeProvider")
+  public void testDeleteTableWithLogicalTable(TableType tableType)
+      throws IOException {
+    ControllerRequestURLBuilder urlBuilder = 
DEFAULT_INSTANCE.getControllerRequestURLBuilder();
+    String logicalTable = "logicalTable";
+    String tableName = "physicalTable";
+    String tableNameWithType = 
TableNameBuilder.forType(tableType).tableNameWithType(tableName);
+    DEFAULT_INSTANCE.addDummySchema(tableName);
+    DEFAULT_INSTANCE.addDummySchema(logicalTable);
+    DEFAULT_INSTANCE.addTableConfig(createDummyTableConfig(tableName, 
tableType));
+
+    LogicalTableConfig logicalTableConfig =
+        ControllerTest.getDummyLogicalTableConfig(logicalTable, 
List.of(tableNameWithType), "DefaultTenant");
+    String logicalTableUrl = urlBuilder.forLogicalTableCreate();
+    String response = ControllerTest.sendPostRequest(logicalTableUrl, 
logicalTableConfig.toSingleLineJsonString());
+    assertEquals(response,
+        "{\"unrecognizedProperties\":{},\"status\":\"logicalTable logical 
table successfully added.\"}");
+
+    // table deletion should fail
+    String tableDeleteUrl = urlBuilder.forTableDelete(tableNameWithType);
+    String msg = expectThrows(IOException.class, () -> 
ControllerTest.sendDeleteRequest(tableDeleteUrl)).getMessage();
+    assertTrue(msg.contains("Cannot delete table config: " + tableNameWithType
+        + " because it is referenced in logical table: logicalTable"), msg);
+
+    // table delete with name and type should also fail
+    msg = expectThrows(IOException.class,
+        () -> ControllerTest.sendDeleteRequest(tableDeleteUrl + "?type=" + 
tableType)).getMessage();
+    assertTrue(msg.contains("Cannot delete table config: " + tableNameWithType
+        + " because it is referenced in logical table: logicalTable"), msg);
+
+    // table delete with raw table name also should fail
+    msg = expectThrows(IOException.class,
+        () -> 
ControllerTest.sendDeleteRequest(urlBuilder.forTableDelete(tableName))).getMessage();
+    assertTrue(msg.contains(
+        "Cannot delete table config: " + tableName + " because it is 
referenced in logical table: logicalTable"), msg);
+
+    // table delete with raw table name and type also should fail
+    msg = expectThrows(IOException.class,
+        () -> 
ControllerTest.sendDeleteRequest(urlBuilder.forTableDelete(tableName + "?type=" 
+ tableType)))
+            .getMessage();
+    assertTrue(msg.contains(
+        "Cannot delete table config: " + tableName + " because it is 
referenced in logical table: logicalTable"), msg);
+
+    // Delete logical table
+    String logicalTableDeleteUrl = 
urlBuilder.forLogicalTableDelete(logicalTable);
+    response = ControllerTest.sendDeleteRequest(logicalTableDeleteUrl);
+    assertEquals(response, "{\"status\":\"logicalTable logical table 
successfully deleted.\"}");
+
+    // Now table deletion should succeed
+    response = ControllerTest.sendDeleteRequest(tableDeleteUrl);
+    assertEquals(response, "{\"status\":\"Tables: [" + tableNameWithType + "] 
deleted\"}");
+  }
+
   @Test
   public void testCheckTableState()
       throws IOException {
@@ -998,6 +1063,13 @@ public class PinotTableRestletResourceTest extends 
ControllerTest {
         
InstanceAssignmentConfig.PartitionSelector.FD_AWARE_INSTANCE_PARTITION_SELECTOR.name(),
 false);
   }
 
+  @AfterMethod
+  public void cleanUp()
+      throws IOException {
+    // Delete all tables after each test
+    DEFAULT_INSTANCE.cleanup();
+  }
+
   @AfterClass
   public void tearDown() {
     DEFAULT_INSTANCE.stopSharedTestSetup();
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
index 54cc7b0ac0..7960f120dc 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/TableConfigsRestletResourceTest.java
@@ -35,11 +35,13 @@ import org.apache.pinot.spi.config.table.TableType;
 import org.apache.pinot.spi.config.table.TunerConfig;
 import org.apache.pinot.spi.data.DateTimeFieldSpec;
 import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.LogicalTableConfig;
 import org.apache.pinot.spi.data.MetricFieldSpec;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.stream.StreamConfig;
 import org.apache.pinot.spi.utils.CommonConstants;
 import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.builder.ControllerRequestURLBuilder;
 import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
 import org.apache.pinot.spi.utils.builder.TableNameBuilder;
 import org.testng.Assert;
@@ -612,6 +614,64 @@ public class TableConfigsRestletResourceTest extends 
ControllerTest {
     Assert.assertEquals(configs.size(), 0);
   }
 
+  @Test
+  public void testDeleteTableWithLogicalTable()
+      throws IOException {
+    String logicalTableName = "testDeleteLogicalTable";
+    String tableName = "physicalTable";
+    TableConfig offlineTableConfig = createOfflineTableConfig(tableName);
+    TableConfig realtimeTableConfig = createRealtimeTableConfig(tableName);
+    ControllerRequestURLBuilder urlBuilder = 
DEFAULT_INSTANCE.getControllerRequestURLBuilder();
+    DEFAULT_INSTANCE.addDummySchema(logicalTableName);
+    DEFAULT_INSTANCE.addDummySchema(tableName);
+    DEFAULT_INSTANCE.addTableConfig(offlineTableConfig);
+    DEFAULT_INSTANCE.addTableConfig(realtimeTableConfig);
+
+    // Create logical table
+    String createLogicalTableUrl = urlBuilder.forLogicalTableCreate();
+    LogicalTableConfig logicalTableConfig = 
getDummyLogicalTableConfig(logicalTableName,
+        List.of(offlineTableConfig.getTableName(), 
realtimeTableConfig.getTableName()), "DefaultTenant");
+    String response = sendPostRequest(createLogicalTableUrl, 
logicalTableConfig.toJsonString());
+    Assert.assertTrue(response.contains("testDeleteLogicalTable logical table 
successfully added"), response);
+
+    // Delete table should fail because it is referenced by a logical table
+    String msg = Assert.expectThrows(
+        IOException.class, () -> 
sendDeleteRequest(urlBuilder.forTableConfigsDelete(tableName))).getMessage();
+    Assert.assertTrue(msg.contains("Cannot delete table config: " + tableName
+        + " because it is referenced in logical table: " + logicalTableName), 
msg);
+
+    //  Delete logical table
+    String deleteLogicalTableUrl = 
urlBuilder.forLogicalTableDelete(logicalTableName);
+    response = sendDeleteRequest(deleteLogicalTableUrl);
+    Assert.assertTrue(response.contains("testDeleteLogicalTable logical table 
successfully deleted"), response);
+
+    // physical table should be deleted successfully
+    response = sendDeleteRequest(urlBuilder.forTableConfigsDelete(tableName));
+    Assert.assertTrue(response.contains("Deleted TableConfigs: 
physicalTable"), response);
+  }
+
+  @Test
+  public void testDeleteTableConfigWithTableTypeValidation()
+      throws IOException {
+    ControllerRequestURLBuilder urlBuilder = 
DEFAULT_INSTANCE.getControllerRequestURLBuilder();
+    String tableName = "testDeleteWithTypeValidation";
+    TableConfig offlineTableConfig = createOfflineTableConfig(tableName);
+    DEFAULT_INSTANCE.addDummySchema(tableName);
+    DEFAULT_INSTANCE.addTableConfig(offlineTableConfig);
+
+    // Delete table should fail because it is not a raw table name
+    String msg = Assert.expectThrows(
+            IOException.class,
+            () -> 
sendDeleteRequest(urlBuilder.forTableConfigsDelete(offlineTableConfig.getTableName())))
+        .getMessage();
+    Assert.assertTrue(msg.contains("Invalid table name: 
testDeleteWithTypeValidation_OFFLINE. Use raw table name."),
+        msg);
+
+    // Delete table with raw table name
+    String response = 
sendDeleteRequest(urlBuilder.forTableConfigsDelete(tableName));
+    Assert.assertTrue(response.contains("Deleted TableConfigs: 
testDeleteWithTypeValidation"), response);
+  }
+
   @Test
   public void testUnrecognizedProperties()
       throws IOException {
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotLogicalTableResourceTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotLogicalTableResourceTest.java
index 50a29e2e55..47eb75d62c 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotLogicalTableResourceTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotLogicalTableResourceTest.java
@@ -217,6 +217,60 @@ public class PinotLogicalTableResourceTest extends 
ControllerTest {
     assertTrue(aThrows.getMessage()
             .contains("Reason: 'refRealtimeTableName' should be one of the 
provided realtime tables"),
         aThrows.getMessage());
+
+    // Test ref offline table is not null but offline table does not exist
+    aThrows = expectThrows(
+        IOException.class, () -> {
+          LogicalTableConfig logicalTableConfig =
+              getDummyLogicalTableConfig(LOGICAL_TABLE_NAME, 
List.of("test_table_7_REALTIME"), BROKER_TENANT);
+          logicalTableConfig.setRefOfflineTableName("test_table_7_OFFLINE");
+          ControllerTest.sendPostRequest(_addLogicalTableUrl, 
logicalTableConfig.toSingleLineJsonString(),
+              getHeaders());
+        }
+    );
+    assertTrue(aThrows.getMessage()
+            .contains("Reason: 'refOfflineTableName' should be null or empty 
when offline tables do not exist"),
+        aThrows.getMessage());
+
+    // Test ref realtime table is not null but realtime table does not exist
+    aThrows = expectThrows(
+        IOException.class, () -> {
+          LogicalTableConfig logicalTableConfig =
+              getDummyLogicalTableConfig(LOGICAL_TABLE_NAME, 
List.of("test_table_7_OFFLINE"), BROKER_TENANT);
+          logicalTableConfig.setRefRealtimeTableName("test_table_7_REALTIME");
+          ControllerTest.sendPostRequest(_addLogicalTableUrl, 
logicalTableConfig.toSingleLineJsonString(),
+              getHeaders());
+        }
+    );
+    assertTrue(aThrows.getMessage()
+            .contains("Reason: 'refRealtimeTableName' should be null or empty 
when realtime tables do not exist"),
+        aThrows.getMessage());
+
+    // Test ref offline table name is realtime table name
+    aThrows = expectThrows(
+        IOException.class, () -> {
+          LogicalTableConfig logicalTableConfig =
+              getDummyLogicalTableConfig(LOGICAL_TABLE_NAME, 
physicalTableNamesWithType, BROKER_TENANT);
+          logicalTableConfig.setRefOfflineTableName("test_table_7_REALTIME");
+          ControllerTest.sendPostRequest(_addLogicalTableUrl, 
logicalTableConfig.toSingleLineJsonString(),
+              getHeaders());
+        }
+    );
+    assertTrue(aThrows.getMessage().contains("Reason: 'refOfflineTableName' 
should be an offline table type"),
+        aThrows.getMessage());
+
+    // Test ref realtime table name is offline table name
+    aThrows = expectThrows(
+        IOException.class, () -> {
+          LogicalTableConfig logicalTableConfig =
+              getDummyLogicalTableConfig(LOGICAL_TABLE_NAME, 
physicalTableNamesWithType, BROKER_TENANT);
+          logicalTableConfig.setRefRealtimeTableName("test_table_7_OFFLINE");
+          ControllerTest.sendPostRequest(_addLogicalTableUrl, 
logicalTableConfig.toSingleLineJsonString(),
+              getHeaders());
+        }
+    );
+    assertTrue(aThrows.getMessage().contains("Reason: 'refRealtimeTableName' 
should be a realtime table type"),
+        aThrows.getMessage());
   }
 
   @Test(expectedExceptions = IOException.class,
@@ -286,14 +340,6 @@ public class PinotLogicalTableResourceTest extends 
ControllerTest {
         throwable.getMessage());
   }
 
-  @DataProvider
-  public Object[][] tableTypeProvider() {
-    return new Object[][]{
-        {TableType.OFFLINE},
-        {TableType.REALTIME}
-    };
-  }
-
   @Test(dataProvider = "tableTypeProvider")
   public void testCreateLogicalTable(TableType tableType)
       throws IOException {
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/ControllerTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/ControllerTest.java
index cb04aed946..01092a3137 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/helix/ControllerTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/helix/ControllerTest.java
@@ -96,6 +96,7 @@ import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.testng.annotations.DataProvider;
 
 import static 
org.apache.pinot.spi.utils.CommonConstants.Helix.UNTAGGED_BROKER_INSTANCE;
 import static 
org.apache.pinot.spi.utils.CommonConstants.Helix.UNTAGGED_SERVER_INSTANCE;
@@ -1247,6 +1248,14 @@ public class ControllerTest {
     
assertTrue(CollectionUtils.isEmpty(getHelixResourceManager().getSchemaNames()));
   }
 
+  @DataProvider
+  public Object[][] tableTypeProvider() {
+    return new Object[][]{
+        {TableType.OFFLINE},
+        {TableType.REALTIME}
+    };
+  }
+
   /**
    * Clean shared state after a test case class has completed running. 
Additional cleanup may be needed depending upon
    * test functionality.


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to