Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ public interface TablesApiHandler {
ApiResponse<GetTableResponseBody> getTable(
String databaseId, String tableId, String actingPrincipal);

/**
* Function to check whether a table exists for the given databaseId and tableId. Uses a
* lightweight HouseTable reference lookup (no metadata.json/HDFS read).
*
* @param databaseId
* @param tableId
* @return an empty-bodied response with HTTP 200 if the table exists, HTTP 404 otherwise.
*/
ApiResponse<Void> tableExists(String databaseId, String tableId);

/**
* Function to Get all Table Resources in a given databaseId by filters and return requested
* columns. If no columns are specified only identifier columns are returned.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public ApiResponse<GetTableResponseBody> getTable(
.build();
}

@Override
public ApiResponse<Void> tableExists(String databaseId, String tableId) {
tablesApiValidator.validateGetTable(databaseId, tableId);
return ApiResponse.<Void>builder()
.httpStatus(
tableService.tableExists(databaseId, tableId) ? HttpStatus.OK : HttpStatus.NOT_FOUND)
.build();
}

@Override
public ApiResponse<GetAllTablesResponseBody> searchTables(String databaseId) {
tablesApiValidator.validateSearchTables(databaseId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,38 @@ public ResponseEntity<GetTableResponseBody> getTable(
apiResponse.getResponseBody(), apiResponse.getHttpHeaders(), apiResponse.getHttpStatus());
}

@Operation(
summary = "Check if a Table exists in a Database",
description =
"Returns HTTP 200 if the Table identified by tableId exists in the database identified by "
+ "databaseId, and HTTP 404 otherwise. Uses a lightweight HouseTable lookup that does "
+ "not read metadata from storage, making it cheaper than GET Table.",
tags = {"Table"})
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "Table EXISTS: OK"),
@ApiResponse(responseCode = "401", description = "Table EXISTS: UNAUTHORIZED"),
@ApiResponse(responseCode = "403", description = "Table EXISTS: FORBIDDEN"),
@ApiResponse(responseCode = "404", description = "Table EXISTS: NOT_FOUND")
})
@GetMapping(
value = {
"/v0/databases/{databaseId}/tables/{tableId}/exists",
"/v1/databases/{databaseId}/tables/{tableId}/exists"
},
produces = {"application/json"})
@Secured(value = Privileges.Privilege.GET_TABLE_METADATA)
public ResponseEntity<Void> tableExists(
@Parameter(description = "Database ID", required = true) @PathVariable String databaseId,
@Parameter(description = "Table ID", required = true) @PathVariable String tableId) {

com.linkedin.openhouse.common.api.spec.ApiResponse<Void> apiResponse =
tablesApiHandler.tableExists(databaseId, tableId);

return new ResponseEntity<>(
apiResponse.getResponseBody(), apiResponse.getHttpHeaders(), apiResponse.getHttpStatus());
}

@Operation(
summary = "Search Tables in a Database",
description =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ public interface TablesService {
*/
TableDto getTable(String databaseId, String tableId, String actingPrincipal);

/**
* Check whether a table identified by databaseId and tableId exists, using a lightweight
* HouseTable reference lookup that does not parse metadata.json (i.e. no HDFS read).
*
* @param databaseId
* @param tableId
* @return true iff the table exists in the catalog.
*/
boolean tableExists(String databaseId, String tableId);

/**
* Given a databaseId, prepare list of {@link TableDto}s.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ public TableDto getTable(String databaseId, String tableId, String actingPrincip
return tableDto;
}

@Override
public boolean tableExists(String databaseId, String tableId) {
// Lightweight HouseTable reference lookup (no metadata.json parse / HDFS read) is enough to
// determine existence. Authorization is enforced at the controller via @Secured.
return openHouseInternalRepository
.findTableRefById(
TableDtoPrimaryKey.builder().databaseId(databaseId).tableId(tableId).build())
.isPresent();
}

@Override
public List<TableDto> searchTables(String databaseId) {
return openHouseInternalRepository.searchTables(databaseId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,42 @@ public void testTableDeleteSucceedsWhenMetadataJsonIsCorrupted() throws IOExcept
TABLE_DTO.getDatabaseId(), TABLE_DTO.getTableId(), TEST_USER));
}

@Test
public void testTableExists() {
Assertions.assertFalse(
tablesService.tableExists(TABLE_DTO.getDatabaseId(), TABLE_DTO.getTableId()),
"tableExists should be false before the table is created");

verifyPutTableRequest(TABLE_DTO, null, true);
Assertions.assertTrue(
tablesService.tableExists(TABLE_DTO.getDatabaseId(), TABLE_DTO.getTableId()),
"tableExists should be true after the table is created");

tablesService.deleteTable(TABLE_DTO.getDatabaseId(), TABLE_DTO.getTableId(), TEST_USER);
Assertions.assertFalse(
tablesService.tableExists(TABLE_DTO.getDatabaseId(), TABLE_DTO.getTableId()),
"tableExists should be false after the table is deleted");
}

/**
* tableExists relies on the HTS-only findTableRefById lookup, so it must report existence without
* parsing metadata.json — even when metadata.json is corrupted and loadTable would throw.
*/
@Test
public void testTableExistsWhenMetadataJsonIsCorrupted() throws IOException {
TableDto created = verifyPutTableRequest(TABLE_DTO, null, true);

Path metadataPath = Paths.get(URI.create(created.getTableLocation()));
Files.write(metadataPath, "{\"not\":\"valid iceberg metadata\"}".getBytes());

// getTable parses metadata.json and fails, but tableExists only consults HTS and succeeds.
Assertions.assertThrows(
Exception.class,
() -> tablesService.getTable(TABLE_DTO.getDatabaseId(), TABLE_DTO.getTableId(), TEST_USER));
Assertions.assertTrue(
tablesService.tableExists(TABLE_DTO.getDatabaseId(), TABLE_DTO.getTableId()));
}

@Test
public void testTimePartitioning() {
Schema schema =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ public ApiResponse<GetTableResponseBody> getTable(
}
}

@Override
public ApiResponse<Void> tableExists(String databaseId, String tableId) {
switch (databaseId) {
case "d200":
return ApiResponse.<Void>builder().httpStatus(HttpStatus.OK).build();
case "d404":
return ApiResponse.<Void>builder().httpStatus(HttpStatus.NOT_FOUND).build();
case "dnullpointer":
throw new NullPointerException(); // test for exception handler
default:
return null;
}
}

@Override
public ApiResponse<GetAllTablesResponseBody> searchTables(String databaseId) {
switch (databaseId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,37 @@ public void findTableById401() throws Exception {
.andExpect(status().isUnauthorized());
}

@Test
public void tableExists200() throws Exception {
mvc.perform(
MockMvcRequestBuilders.get(
CURRENT_MAJOR_VERSION_PREFIX + "/databases/d200/tables/t1/exists")
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + jwtAccessToken))
.andExpect(status().isOk())
.andExpect(content().string(""));
}

@Test
public void tableExists404() throws Exception {
mvc.perform(
MockMvcRequestBuilders.get(
CURRENT_MAJOR_VERSION_PREFIX + "/databases/d404/tables/t1/exists")
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + jwtAccessToken))
.andExpect(status().isNotFound())
.andExpect(content().string(""));
}

@Test
public void tableExists401() throws Exception {
mvc.perform(
MockMvcRequestBuilders.get(
CURRENT_MAJOR_VERSION_PREFIX + "/databases/d200/tables/t1/exists")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized());
}

@Test
@MockUnauthenticatedUser
public void findTableById403() throws Exception {
Expand Down
Loading