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 a1e9f0a0554 Database name validation for logical tables (#15994) a1e9f0a0554 is described below commit a1e9f0a05545d768bbe8d6aac63dec4b0d4b89ea Author: Abhishek Bafna <aba...@startree.ai> AuthorDate: Tue Jun 17 09:16:54 2025 +0530 Database name validation for logical tables (#15994) --- .../common/utils/LogicalTableConfigUtils.java | 10 +++ .../api/resources/PinotLogicalTableResource.java | 25 +++++- .../resources/PinotLogicalTableResourceTest.java | 100 ++++++++++++++++----- 3 files changed, 110 insertions(+), 25 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 1f9d948728b..e1ac8e352e1 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 @@ -139,6 +139,7 @@ public class LogicalTableConfigUtils { "Invalid logical table. Reason: 'physicalTableConfigMap' should not be null or empty"); } + String databaseName = DatabaseUtils.extractDatabaseFromFullyQualifiedTableName(logicalTableConfig.getTableName()); Set<String> offlineTableNames = new HashSet<>(); Set<String> realtimeTableNames = new HashSet<>(); @@ -146,6 +147,15 @@ public class LogicalTableConfigUtils { String physicalTableName = entry.getKey(); PhysicalTableConfig physicalTableConfig = entry.getValue(); + // validate database name matches + String physicalTableDatabaseName = DatabaseUtils.extractDatabaseFromFullyQualifiedTableName(physicalTableName); + if (!StringUtils.equalsIgnoreCase(databaseName, physicalTableDatabaseName)) { + throw new IllegalArgumentException( + "Invalid logical table. Reason: '" + physicalTableName + + "' should have the same database name as logical table: " + databaseName + " != " + + physicalTableDatabaseName); + } + // validate physical table exists if (!physicalTableExistsPredicate.test(physicalTableName)) { throw new IllegalArgumentException( diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotLogicalTableResource.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotLogicalTableResource.java index 56a9f02edd9..1192c8444fe 100644 --- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotLogicalTableResource.java +++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotLogicalTableResource.java @@ -29,6 +29,7 @@ import io.swagger.annotations.SecurityDefinition; import io.swagger.annotations.SwaggerDefinition; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -57,6 +58,7 @@ import org.apache.pinot.core.auth.Authorize; import org.apache.pinot.core.auth.ManualAuthorization; import org.apache.pinot.core.auth.TargetType; import org.apache.pinot.spi.data.LogicalTableConfig; +import org.apache.pinot.spi.data.PhysicalTableConfig; import org.apache.pinot.spi.utils.JsonUtils; import org.glassfish.grizzly.http.server.Request; import org.slf4j.Logger; @@ -141,10 +143,31 @@ public class PinotLogicalTableResource { ResourceUtils.checkPermissionAndAccess(tableName, request, httpHeaders, AccessType.CREATE, Actions.Table.CREATE_TABLE, _accessControlFactory, LOGGER); + translatePhysicalTableNamesWithDB(logicalTableConfig, httpHeaders); SuccessResponse successResponse = addLogicalTable(logicalTableConfig); return new ConfigSuccessResponse(successResponse.getStatus(), logicalTableConfigAndUnrecognizedProps.getRight()); } + private void translatePhysicalTableNamesWithDB(LogicalTableConfig logicalTableConfig, HttpHeaders headers) { + // Translate physical table names to include the database name + Map<String, PhysicalTableConfig> physicalTableConfigMap = logicalTableConfig.getPhysicalTableConfigMap().entrySet() + .stream() + .map(entry -> Map.entry(DatabaseUtils.translateTableName(entry.getKey(), headers), entry.getValue())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + logicalTableConfig.setPhysicalTableConfigMap(physicalTableConfigMap); + + // Translate refOfflineTableName and refRealtimeTableName to include the database name + String refOfflineTableName = logicalTableConfig.getRefOfflineTableName(); + if (refOfflineTableName != null) { + logicalTableConfig.setRefOfflineTableName(DatabaseUtils.translateTableName(refOfflineTableName, headers)); + } + String refRealtimeTableName = logicalTableConfig.getRefRealtimeTableName(); + if (refRealtimeTableName != null) { + logicalTableConfig.setRefRealtimeTableName(DatabaseUtils.translateTableName(refRealtimeTableName, headers)); + } + } + @PUT @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @@ -170,7 +193,7 @@ public class PinotLogicalTableResource { tableName = DatabaseUtils.translateTableName(tableName, headers); logicalTableConfig.setTableName(tableName); - + translatePhysicalTableNamesWithDB(logicalTableConfig, headers); SuccessResponse successResponse = updateLogicalTable(logicalTableConfig); return new ConfigSuccessResponse(successResponse.getStatus(), logicalTableConfigAndUnrecognizedProps.getRight()); } 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 47eb75d62ca..f65c4a98b34 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 @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import org.apache.helix.model.IdealState; +import org.apache.pinot.common.utils.DatabaseUtils; import org.apache.pinot.common.utils.helix.HelixHelper; import org.apache.pinot.controller.helix.ControllerTest; import org.apache.pinot.controller.helix.core.PinotHelixResourceManager; @@ -85,23 +86,23 @@ public class PinotLogicalTableResourceTest extends ControllerTest { @DataProvider public Object[][] tableNamesProvider() { return new Object[][]{ - {"test_logical_table", List.of("test_table_1", "test_table_2"), List.of("test_table_3")}, - {"test_logical_table", List.of("test_table_1", "db.test_table_2"), List.of("test_table_3")}, - {"test_logical_table", List.of("test_table_1", "test_table_2"), List.of("db.test_table_3")}, - {"test_logical_table", List.of("db.test_table_1", "db.test_table_2"), List.of("db.test_table_3")}, - {"test_table", List.of("db1.test_table", "db2.test_table"), List.of("db3.test_table")}, - {"db0.test_table", List.of("db1.test_table", "db2.test_table"), List.of("db3.test_table")}, - {"db.test_logical_table", List.of("test_table_1", "test_table_2"), List.of("test_table_3")}, - {"db.test_logical_table", List.of("test_table_1", "db.test_table_2"), List.of("test_table_3")}, - {"db.test_logical_table", List.of("test_table_1", "test_table_2"), List.of("db.test_table_3")}, - {"db.test_logical_table", List.of("db.test_table_1", "db.test_table_2"), List.of("db.test_table_3")}, + {"test_logical_table", List.of("test_table_1", "test_table_2"), List.of("test_table_3"), Map.of()}, + {"db.test_logical_table", List.of("db.test_table_1", "db.test_table_2"), List.of("db.test_table_3"), Map.of()}, + { + "test_logical_table", List.of("db1.test_table_1", "db1.test_table_2"), List.of("db1.test_table_3"), Map.of( + CommonConstants.DATABASE, "db1") + }, }; } @Test(dataProvider = "tableNamesProvider") public void testCreateUpdateDeleteLogicalTables(String logicalTableName, List<String> physicalTableNames, - List<String> physicalTablesToUpdate) + List<String> physicalTablesToUpdate, Map<String, String> dbHeaders) throws IOException { + Map<String, String> headers = new HashMap<>(getHeaders()); + headers.putAll(dbHeaders); + logicalTableName = DatabaseUtils.translateTableName(logicalTableName, headers.get(CommonConstants.DATABASE)); + addDummySchema(logicalTableName); // verify logical table does not exist String getLogicalTableUrl = _controllerRequestURLBuilder.forLogicalTableGet(logicalTableName); @@ -118,7 +119,7 @@ public class PinotLogicalTableResourceTest extends ControllerTest { // create logical table String resp = - ControllerTest.sendPostRequest(_addLogicalTableUrl, logicalTableConfig.toSingleLineJsonString(), getHeaders()); + ControllerTest.sendPostRequest(_addLogicalTableUrl, logicalTableConfig.toSingleLineJsonString(), headers); assertEquals(resp, "{\"unrecognizedProperties\":{},\"status\":\"" + logicalTableName + " logical table successfully added.\"}"); @@ -131,7 +132,7 @@ public class PinotLogicalTableResourceTest extends ControllerTest { logicalTableConfig = getDummyLogicalTableConfig(logicalTableName, tableNameToUpdateWithType, BROKER_TENANT); String response = - ControllerTest.sendPutRequest(updateLogicalTableUrl, logicalTableConfig.toSingleLineJsonString(), getHeaders()); + ControllerTest.sendPutRequest(updateLogicalTableUrl, logicalTableConfig.toSingleLineJsonString(), headers); assertEquals(response, "{\"unrecognizedProperties\":{},\"status\":\"" + logicalTableName + " logical table successfully updated.\"}"); @@ -139,7 +140,7 @@ public class PinotLogicalTableResourceTest extends ControllerTest { verifyLogicalTableExists(getLogicalTableUrl, logicalTableConfig); // delete logical table - String deleteResponse = ControllerTest.sendDeleteRequest(deleteLogicalTableUrl, getHeaders()); + String deleteResponse = ControllerTest.sendDeleteRequest(deleteLogicalTableUrl, headers); assertEquals(deleteResponse, "{\"status\":\"" + logicalTableName + " logical table successfully deleted.\"}"); // verify logical table is deleted @@ -271,6 +272,55 @@ public class PinotLogicalTableResourceTest extends ControllerTest { ); assertTrue(aThrows.getMessage().contains("Reason: 'refRealtimeTableName' should be a realtime table type"), aThrows.getMessage()); + + // Test ref offline table is specified with a database prefix + aThrows = expectThrows( + IOException.class, () -> { + LogicalTableConfig logicalTableConfig = + getDummyLogicalTableConfig(LOGICAL_TABLE_NAME, physicalTableNamesWithType, BROKER_TENANT); + logicalTableConfig.setRefOfflineTableName("db.test_table_7_OFFLINE"); + ControllerTest.sendPostRequest(_addLogicalTableUrl, logicalTableConfig.toSingleLineJsonString(), + getHeaders()); + } + ); + assertTrue( + aThrows.getMessage().contains("Reason: 'refOfflineTableName' should be one of the provided offline tables"), + aThrows.getMessage()); + + // Test ref realtime table is specified with a database prefix + aThrows = expectThrows( + IOException.class, () -> { + LogicalTableConfig logicalTableConfig = + getDummyLogicalTableConfig(LOGICAL_TABLE_NAME, physicalTableNamesWithType, BROKER_TENANT); + logicalTableConfig.setRefRealtimeTableName("db.test_table_7_REALTIME"); + ControllerTest.sendPostRequest(_addLogicalTableUrl, logicalTableConfig.toSingleLineJsonString(), + getHeaders()); + } + ); + assertTrue( + aThrows.getMessage().contains("Reason: 'refRealtimeTableName' should be one of the provided realtime tables"), + aThrows.getMessage()); + } + + @Test + public void testLogicalTableDatabaseValidation() { + String logicalTableName = "db1.test_logical_table"; + LogicalTableConfig logicalTableConfig = + getDummyLogicalTableConfig(logicalTableName, List.of("test_table_1_OFFLINE", "test_table_2_REALTIME"), + BROKER_TENANT); + // Test add logical table with different database prefix + String msg = expectThrows(IOException.class, () -> { + addLogicalTableConfig(logicalTableConfig); + }).getMessage(); + assertTrue(msg.contains( + "Reason: 'test_table_1_OFFLINE' should have the same database name as logical table: db1 != default"), msg); + + // Test update logical table with different database prefix + msg = expectThrows(IOException.class, () -> updateLogicalTableConfig(logicalTableConfig)).getMessage(); + assertTrue( + msg.contains( + "Reason: 'test_table_1_OFFLINE' should have the same database name as logical table: db1 != default"), + msg); } @Test(expectedExceptions = IOException.class, @@ -374,7 +424,8 @@ public class PinotLogicalTableResourceTest extends ControllerTest { throwable = expectThrows(IOException.class, () -> { addDummySchema(LOGICAL_TABLE_NAME); LogicalTableConfig logicalTableConfig = - getDummyLogicalTableConfig("db." + LOGICAL_TABLE_NAME, physicalTableNamesWithType, BROKER_TENANT); + getDummyLogicalTableConfig("db." + LOGICAL_TABLE_NAME, createHybridTables(List.of("db.test_table_6")), + BROKER_TENANT); ControllerTest.sendPostRequest(_addLogicalTableUrl, logicalTableConfig.toSingleLineJsonString(), getHeaders()); }); assertTrue(throwable.getMessage() @@ -463,8 +514,8 @@ public class PinotLogicalTableResourceTest extends ControllerTest { return new Object[][]{ {LOGICAL_TABLE_NAME, List.of("test_table_1"), "unknown_table_OFFLINE"}, {LOGICAL_TABLE_NAME, List.of("test_table_2"), "unknown_table_REALTIME"}, - {LOGICAL_TABLE_NAME, List.of("test_table_1"), "db.test_table_1_OFFLINE"}, - {LOGICAL_TABLE_NAME, List.of("test_table_2"), "db.test_table_2_REALTIME"}, + {"db." + LOGICAL_TABLE_NAME, List.of("db.test_table_1"), "db.unknown_table_OFFLINE"}, + {"db." + LOGICAL_TABLE_NAME, List.of("db.test_table_2"), "db.unknown_table_REALTIME"}, }; } @@ -472,6 +523,7 @@ public class PinotLogicalTableResourceTest extends ControllerTest { public void testPhysicalTableShouldExist(String logicalTableName, List<String> physicalTableNames, String unknownTableName) throws IOException { + addDummySchema(logicalTableName); // setup physical tables List<String> physicalTableNamesWithType = createHybridTables(physicalTableNames); physicalTableNamesWithType.add(unknownTableName); @@ -499,13 +551,14 @@ public class PinotLogicalTableResourceTest extends ControllerTest { // setup physical tables and logical tables List<String> logicalTableNames = List.of("db.test_logical_table_1", "default.test_logical_table_2", "test_logical_table_3"); - List<String> physicalTableNames = List.of("test_table_1", "test_table_2", "db.test_table_3"); - List<String> physicalTableNamesWithType = createHybridTables(physicalTableNames); for (int i = 0; i < logicalTableNames.size(); i++) { - addDummySchema(logicalTableNames.get(i)); - LogicalTableConfig logicalTableConfig = getDummyLogicalTableConfig(logicalTableNames.get(i), List.of( - physicalTableNamesWithType.get(2 * i), physicalTableNamesWithType.get(2 * i + 1)), BROKER_TENANT); + String logicalTableName = logicalTableNames.get(i); + String databaseName = DatabaseUtils.extractDatabaseFromFullyQualifiedTableName(logicalTableName); + List<String> physicalTableNamesWithType = createHybridTables(List.of(databaseName + ".test_table_" + i)); + addDummySchema(logicalTableName); + LogicalTableConfig logicalTableConfig = + getDummyLogicalTableConfig(logicalTableName, physicalTableNamesWithType, BROKER_TENANT); ControllerTest.sendPostRequest(_addLogicalTableUrl, logicalTableConfig.toSingleLineJsonString(), getHeaders()); } @@ -524,8 +577,7 @@ public class PinotLogicalTableResourceTest extends ControllerTest { } @Test - public void testLogicalTableDatabaseHeaderMismatchValidation() - throws IOException { + public void testLogicalTableDatabaseHeaderMismatchValidation() { Map<String, String> headers = new HashMap<>(getHeaders()); headers.put(CommonConstants.DATABASE, "db1"); String logicalTableName = "db2.test_logical_table"; --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@pinot.apache.org For additional commands, e-mail: commits-h...@pinot.apache.org