This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 9adba48761 [#13696] fix(lance): validate dataset location before
registering tables (#13722)
9adba48761 is described below
commit 9adba4876193b0ffc846461e47e04bda63876b93
Author: Arvin <[email protected]>
AuthorDate: Thu Oct 29 21:03:56 2026 +0800
[#13696] fix(lance): validate dataset location before registering tables
(#13722)
### What changes were proposed in this pull request?
Validate the provider-resolved location as a readable Lance dataset
before registering metadata, using the effective catalog/table storage
options and closing the dataset afterward.
- In `CREATE` mode, check metadata existence before accessing the
dataset; retain the final strict insert to detect concurrent
registrations.
- In `OVERWRITE` mode, validate before removing anything. Replace
external-table metadata without deleting its dataset; reject
registration overwrite of managed tables.
- Preserve the SDK distinction between invalid input and storage
failures. Wrap storage and cleanup failures with context and the
original cause.
### Why are the changes needed?
Registration previously accepted plain directories and nonexistent
locations. Validation must also precede overwrite removal so an invalid
replacement cannot delete the existing metadata or managed dataset.
Refs #13696 (partial fix). This validates dataset existence/readability
at registration time. Operator-defined storage scope, purge
authorization for external datasets, and validation of other
existing-table I/O paths remain tracked by #13696. Opening a dataset is
not an authorization check.
### Does this PR introduce _any_ user-facing change?
Yes. Missing/non-Lance locations are rejected as invalid input (HTTP 400
in Lance REST). Storage and dataset-cleanup failures remain server
errors (HTTP 500). Registering an existing name in `CREATE` mode returns
a conflict before dataset validation (HTTP 409). Invalid overwrite
registrations preserve the original metadata and dataset. Registration
overwrite rejects managed tables; explicit managed-table drop/purge
remains available and deletes managed data. No API or configuration keys
change.
### How was this patch tested?
- Latest head: 183 tests passed across `TestLanceTableOperations` (79),
`TestGenericCatalogOperations` (38), `CatalogGenericCatalogLanceIT`
(17), `TestGravitinoLanceTableOperations` (12),
`TestLanceNamespaceOperations` (16), `TestLanceExceptionMapper` (6), and
`LanceRESTServiceIT` (15). These include real SDK tests and embedded
HTTP integration tests.
- Regressions cover missing/plain-directory rejection without writes,
valid registration and closure, overwrite preservation, managed-table
rejection, storage I/O/runtime failures in both modes, cleanup failure
classification, existence-read failure, concurrent registration, and
HTTP conflict precedence for an existing name with a missing dataset.
- Removing the CREATE precheck and restoring storage-failure-to-400
conversion makes all five targeted regression cases fail. Restoring the
fix passes all 79 operation tests again.
- Both changed modules pass Spotless checks. Docker and remote
object-store suites were not run.
---------
Co-authored-by: yuqi <[email protected]>
---
.../lakehouse/lance/LanceTableOperations.java | 76 +++-
.../generic/TestGenericCatalogOperations.java | 17 +-
.../lakehouse/lance/TestLanceTableOperations.java | 388 +++++++++++++++++++++
.../test/CatalogGenericCatalogLanceIT.java | 41 ++-
docs/lakehouse-generic-lance-table.md | 8 +-
docs/lance-rest-service.md | 6 +
.../integration/test/LanceDatasetTestUtils.java | 42 +++
.../integration/test/LanceFormatBoundaryIT.java | 4 +-
.../lance/integration/test/LanceRESTServiceIT.java | 36 +-
.../test/LanceTableAuthorizationIT.java | 21 +-
10 files changed, 592 insertions(+), 47 deletions(-)
diff --git
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
index 0e43d84284..15f7da1dd2 100644
---
a/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
+++
b/catalogs/catalog-lakehouse-generic/src/main/java/org/apache/gravitino/catalog/lakehouse/lance/LanceTableOperations.java
@@ -231,10 +231,26 @@ public class LanceTableOperations extends
ManagedTableOperations {
}
}
+ if (register) {
+ if (mode == CreationMode.CREATE) {
+ // Check metadata before accessing storage so an existing name
consistently reports a
+ // conflict. The final strict insert still detects a concurrent
registration.
+ try {
+ if (store().exists(ident, Entity.EntityType.TABLE)) {
+ throw new TableAlreadyExistsException("Table %s already exists",
ident);
+ }
+ } catch (IOException e) {
+ throw ExceptionMessages.wrap("Failed to check existence of table " +
ident, e);
+ }
+ }
+ // Validate before OVERWRITE can remove existing metadata or managed
data.
+ validateExistingDatasetLocation(ident, location, properties);
+ }
+
// Handle OVERWRITE mode - drop existing table if present
if (mode == CreationMode.OVERWRITE) {
if (register) {
- dropTable(ident);
+ dropExternalTableForRegistration(ident);
} else {
purgeTable(ident);
}
@@ -973,4 +989,62 @@ public class LanceTableOperations extends
ManagedTableOperations {
throw new IllegalArgumentException("Unsupported index type: " +
indexType);
}
}
+
+ private void dropExternalTableForRegistration(NameIdentifier ident) {
+ try {
+ Table existing = super.loadTable(ident);
+ validateLanceTable(ident, existing);
+ Preconditions.checkArgument(
+ LancePropertiesUtils.isExternalTable(existing.properties()),
+ "Cannot overwrite managed Lance table %s with a registration; "
+ + "explicitly drop or purge the managed table before registering
a replacement",
+ ident);
+ // Registration replaces external metadata only. Calling dropTable could
delete a managed
+ // dataset, including the same dataset that the incoming registration
just validated.
+ super.dropTable(ident);
+ } catch (NoSuchTableException e) {
+ // No existing registration needs replacing.
+ }
+ }
+
+ /**
+ * Validates that a caller-supplied location resolves to an existing, valid
Lance dataset before
+ * metadata changes adopt it. An explicit purge can later delete the
registered location, so a
+ * plain directory must not be adopted as if it were a Lance dataset (issue
#13696).
+ *
+ * <p>This checks dataset existence, not operator-defined storage scope or
purge authorization.
+ *
+ * @param ident the table being registered, used only for the error message
+ * @param location the caller-supplied dataset location
+ * @param properties the table properties, used to resolve storage options
+ * @throws IllegalArgumentException if the SDK rejects the dataset location
as invalid input
+ * @throws RuntimeException if storage access or closing the validation
dataset fails
+ */
+ private void validateExistingDatasetLocation(
+ NameIdentifier ident, String location, Map<String, String> properties) {
+ Map<String, String> storageOptions =
+ LancePropertiesUtils.resolveLanceStorageOptions(catalogProperties,
properties);
+ String context =
+ String.format(
+ "Could not validate location %s for Lance table %s as a readable
Lance dataset",
+ location, ident);
+ Dataset dataset;
+ try {
+ dataset = openDataset(location, storageOptions);
+ } catch (IllegalArgumentException e) {
+ // Lance maps DatasetNotFound and InvalidInput to
IllegalArgumentException, while storage
+ // I/O errors use IOException. Preserve that distinction for the REST
error mapper.
+ throw ExceptionMessages.illegalArgument(context + "; check dataset
existence and format", e);
+ } catch (Exception e) {
+ throw ExceptionMessages.wrap(
+ context + "; check access permissions and storage connectivity", e);
+ }
+
+ try (Dataset ignored = dataset) {
+ LOG.debug("Validated Lance dataset at location {} for table {}",
location, ident);
+ } catch (Exception e) {
+ // Cleanup failures are server errors, even if close throws
IllegalArgumentException.
+ throw ExceptionMessages.wrap("Failed to close validated dataset for
table " + ident, e);
+ }
+ }
}
diff --git
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestGenericCatalogOperations.java
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestGenericCatalogOperations.java
index 7daaed9a8d..4278939881 100644
---
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestGenericCatalogOperations.java
+++
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestGenericCatalogOperations.java
@@ -208,10 +208,23 @@ public class TestGenericCatalogOperations {
@CsvSource(
value = {"NULL", "false", "true"},
nullValues = "NULL")
- void testRegisterPropertiesAreExternalBeforeProvisioning(String external) {
+ void testRegisterPropertiesAreExternalBeforeProvisioning(String external)
throws Exception {
NameIdentifier schemaIdent = createSchema();
NameIdentifier ident =
NameIdentifier.of(METALAKE_NAME, CATALOG_NAME, schemaIdent.name(),
"registered");
+ String location = lanceDataDir.resolve("existing").toString();
+ // Arrow Schema and Gravitino Schema share a simple name in this test
class.
+ try (Dataset ignored =
+ Dataset.write()
+ .schema(
+ new org.apache.arrow.vector.types.pojo.Schema(
+ List.of(Field.nullable("id", new ArrowType.Int(32,
true)))))
+ .uri(location)
+ .mode(WriteParams.WriteMode.CREATE)
+ .execute()) {
+ // Registration now requires an existing dataset at the
provider-resolved location.
+ }
+ FakeTableLocationProvider.provisionLocation(location);
Map<String, String> properties =
Maps.newHashMap(
Map.of(
@@ -220,7 +233,7 @@ public class TestGenericCatalogOperations {
"lance.register",
"true",
Table.PROPERTY_LOCATION,
- lanceDataDir.resolve("existing").toString()));
+ location));
properties.putAll(
StringIdentifier.newPropertiesWithId(StringIdentifier.fromId(idGenerator.nextId()),
null));
if (external != null) {
diff --git
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
index 2196782da0..bbefb7d002 100644
---
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
+++
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/TestLanceTableOperations.java
@@ -27,6 +27,7 @@ import static
org.apache.gravitino.lance.common.utils.LanceConstants.LANCE_TABLE
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
@@ -39,6 +40,7 @@ import static org.mockito.Mockito.when;
import com.google.common.collect.Maps;
import java.io.IOException;
import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.time.Instant;
@@ -50,6 +52,7 @@ import org.apache.arrow.vector.types.pojo.ArrowType;
import org.apache.arrow.vector.types.pojo.Field;
import org.apache.arrow.vector.types.pojo.Schema;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityAlreadyExistsException;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.StringIdentifier;
@@ -325,6 +328,7 @@ public class TestLanceTableOperations {
properties.put(LANCE_CREATION_MODE, "OVERWRITE");
if (register) {
properties.put(LANCE_TABLE_REGISTER, "true");
+
Mockito.doReturn(mock(Dataset.class)).when(lanceTableOps).openDataset(eq(location),
anyMap());
}
IllegalArgumentException exception =
@@ -1686,6 +1690,390 @@ public class TestLanceTableOperations {
verify(dataset).getSchema();
}
+ //
---------------------------------------------------------------------------
+ // Location validation before storage I/O (issue #13696)
+ //
---------------------------------------------------------------------------
+
+ @ParameterizedTest
+ @ValueSource(strings = {"Dataset not found", "Invalid dataset location"})
+ public void testRegisterRejectsLocationWithoutValidLanceDataset(String
reason) throws Exception {
+ // Registering a table adopts an existing dataset at the caller-supplied
location. A location
+ // that does not resolve to a valid Lance dataset must be rejected before
any metadata is
+ // written, otherwise a later explicit purge could delete the unrelated
directory.
+ NameIdentifier ident = NameIdentifier.of("schema", "table");
+ String location = tempDir.resolve("not-a-dataset").toString();
+ Map<String, String> properties = Maps.newHashMap();
+ properties.put(Table.PROPERTY_LOCATION, location);
+ properties.put(LANCE_TABLE_REGISTER, "true");
+
+ IllegalArgumentException storageFailure = new
IllegalArgumentException(reason);
+
Mockito.doThrow(storageFailure).when(lanceTableOps).openDataset(eq(location),
any());
+
+ IllegalArgumentException failure =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ lanceTableOps.createTable(
+ ident,
+ new Column[0],
+ null,
+ properties,
+ new Transform[0],
+ null,
+ new SortOrder[0],
+ new Index[0]));
+ Assertions.assertTrue(
+ failure.getMessage().contains("Could not validate location"),
+ "Error must describe a validation failure, got: " +
failure.getMessage());
+ Assertions.assertTrue(failure.getMessage().contains(location));
+ Assertions.assertTrue(failure.getMessage().contains(ident.toString()));
+ Assertions.assertTrue(failure.getMessage().contains("dataset existence and
format"));
+ Assertions.assertSame(storageFailure, failure.getCause());
+ verify(store, never()).put(any(), Mockito.anyBoolean());
+ }
+
+ @Test
+ public void testRegisterRejectsBlankLocation() throws Exception {
+ NameIdentifier ident = NameIdentifier.of("schema", "table");
+ Map<String, String> properties = Maps.newHashMap();
+ properties.put(LANCE_TABLE_REGISTER, "true");
+
+ IllegalArgumentException failure =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ lanceTableOps.createTable(
+ ident,
+ new Column[0],
+ null,
+ properties,
+ new Transform[0],
+ null,
+ new SortOrder[0],
+ new Index[0]));
+ Assertions.assertTrue(
+ failure.getMessage().contains("Table location must be specified"),
+ "Blank location must be rejected, got: " + failure.getMessage());
+ verify(store, never()).put(any(), Mockito.anyBoolean());
+ }
+
+ @Test
+ public void testRegisterAcceptsLocationWithValidLanceDataset() throws
Exception {
+ // A location that opens as a valid Lance dataset passes validation and
the table metadata is
+ // then written to the entity store.
+ NameIdentifier ident = NameIdentifier.of("schema", "table");
+ String location = tempDir.resolve("valid-dataset").toString();
+ Map<String, String> properties = Maps.newHashMap();
+ properties.put(Table.PROPERTY_LOCATION, location);
+ properties.put(Table.PROPERTY_TABLE_FORMAT, "lance");
+ properties.put(LANCE_TABLE_REGISTER, "true");
+ properties.put(StringIdentifier.ID_KEY,
StringIdentifier.fromId(1L).toString());
+
+ Dataset dataset = mock(Dataset.class);
+ Mockito.doReturn(dataset).when(lanceTableOps).openDataset(eq(location),
anyMap());
+
+ Table registered =
+ PrincipalUtils.doAs(
+ new UserPrincipal("tester"),
+ () ->
+ lanceTableOps.createTable(
+ ident,
+ new Column[0],
+ null,
+ properties,
+ new Transform[0],
+ null,
+ new SortOrder[0],
+ new Index[0]));
+
+ Assertions.assertEquals("table", registered.name());
+ verify(lanceTableOps).openDataset(eq(location), anyMap());
+ verify(dataset).close();
+ verify(store).put(any(), Mockito.anyBoolean());
+ }
+
+ /** Invalid replacement registration must preserve both metadata and managed
data. */
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void
testRegisterOverwriteRejectsInvalidLocationBeforeDroppingExistingTable(boolean
external)
+ throws Exception {
+ NameIdentifier ident = NameIdentifier.of("schema", "original");
+ String oldLocation = tempDir.resolve("original-dataset").toString();
+ Column[] columns = {Column.of("id", Types.IntegerType.get())};
+ try (Dataset ignored = lanceTableOps.createDataset(oldLocation, columns,
Map.of())) {
+ // The existing dataset must survive rejection of its proposed
replacement.
+ }
+ TableEntity existing =
+ tableEntity(
+ ident,
+ List.of(),
+ Map.of(
+ Table.PROPERTY_TABLE_FORMAT,
+ "lance",
+ Table.PROPERTY_LOCATION,
+ oldLocation,
+ Table.PROPERTY_EXTERNAL,
+ Boolean.toString(external)));
+ when(store.get(eq(ident), eq(Entity.EntityType.TABLE),
eq(TableEntity.class)))
+ .thenReturn(existing);
+ when(store.delete(ident, Entity.EntityType.TABLE)).thenReturn(true);
+ String invalidLocation = tempDir.resolve("invalid-replacement").toString();
+ Map<String, String> properties =
+ Maps.newHashMap(
+ Map.of(
+ Table.PROPERTY_LOCATION,
+ invalidLocation,
+ Table.PROPERTY_TABLE_FORMAT,
+ "lance",
+ LANCE_TABLE_REGISTER,
+ "true",
+ LANCE_CREATION_MODE,
+ "OVERWRITE"));
+ IllegalArgumentException invalidDataset = new
IllegalArgumentException("not a Lance dataset");
+
Mockito.doThrow(invalidDataset).when(lanceTableOps).openDataset(eq(invalidLocation),
anyMap());
+
+ IllegalArgumentException failure =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ lanceTableOps.createTable(
+ ident,
+ columns,
+ null,
+ properties,
+ new Transform[0],
+ null,
+ new SortOrder[0],
+ new Index[0]));
+
+ Assertions.assertSame(invalidDataset, failure.getCause());
+ verify(store, never()).delete(any(), any());
+ verify(store, never()).put(any(), anyBoolean());
+ try (Dataset preserved = Dataset.open().uri(oldLocation).build()) {
+ Assertions.assertEquals("id",
preserved.getSchema().getFields().get(0).getName());
+ }
+ }
+
+ /** Register-overwrite must not destroy a managed dataset, even when reusing
its location. */
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testRegisterOverwriteRejectsManagedTableWithoutDeletingData(boolean
reuseLocation)
+ throws Exception {
+ NameIdentifier ident = NameIdentifier.of("schema", "managed");
+ String originalLocation = tempDir.resolve("managed-original").toString();
+ String replacementLocation =
+ reuseLocation ? originalLocation :
tempDir.resolve("existing-replacement").toString();
+ Column[] columns = {Column.of("id", Types.IntegerType.get())};
+ try (Dataset ignored = lanceTableOps.createDataset(originalLocation,
columns, Map.of())) {
+ // Existing managed data belongs to the old table, not to the
registration operation.
+ }
+ if (!reuseLocation) {
+ try (Dataset ignored = lanceTableOps.createDataset(replacementLocation,
columns, Map.of())) {
+ // Both replacement variants are valid datasets.
+ }
+ }
+ TableEntity existing =
+ tableEntity(
+ ident,
+ List.of(),
+ Map.of(
+ Table.PROPERTY_TABLE_FORMAT,
+ "lance",
+ Table.PROPERTY_LOCATION,
+ originalLocation,
+ Table.PROPERTY_EXTERNAL,
+ "false"));
+ when(store.get(eq(ident), eq(Entity.EntityType.TABLE),
eq(TableEntity.class)))
+ .thenReturn(existing);
+ when(store.delete(ident, Entity.EntityType.TABLE)).thenReturn(true);
+ Map<String, String> properties =
+ Maps.newHashMap(
+ Map.of(
+ Table.PROPERTY_LOCATION,
+ replacementLocation,
+ Table.PROPERTY_TABLE_FORMAT,
+ "lance",
+ LANCE_TABLE_REGISTER,
+ "true",
+ LANCE_CREATION_MODE,
+ "OVERWRITE"));
+
+ properties.put(StringIdentifier.ID_KEY,
StringIdentifier.fromId(2L).toString());
+ IllegalArgumentException failure =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PrincipalUtils.doAs(
+ new UserPrincipal("tester"),
+ () ->
+ lanceTableOps.createTable(
+ ident,
+ columns,
+ null,
+ properties,
+ new Transform[0],
+ null,
+ new SortOrder[0],
+ new Index[0])));
+
+ Assertions.assertTrue(failure.getMessage().contains("Cannot overwrite
managed Lance table"));
+ verify(store, never()).delete(any(), any());
+ verify(store, never()).put(any(), anyBoolean());
+ try (Dataset original = Dataset.open().uri(originalLocation).build();
+ Dataset replacement = Dataset.open().uri(replacementLocation).build())
{
+ Assertions.assertEquals("id",
original.getSchema().getFields().get(0).getName());
+ Assertions.assertEquals("id",
replacement.getSchema().getFields().get(0).getName());
+ }
+ }
+
+ /** Exercise the real SDK against a plain directory, not just a stubbed open
failure. */
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void
testRegisterRejectsMissingOrPlainDirectoryWithoutChangingItsContents(boolean
missing)
+ throws Exception {
+ NameIdentifier ident = NameIdentifier.of("schema", "not_a_dataset");
+ Path directory =
Files.createDirectory(tempDir.resolve("unrelated-directory"));
+ Path marker = Files.writeString(directory.resolve("keep.txt"), "unrelated
data");
+ Map<String, String> properties =
+ Maps.newHashMap(
+ Map.of(
+ Table.PROPERTY_LOCATION,
+ (missing ? directory.resolve("missing") :
directory).toString(),
+ Table.PROPERTY_TABLE_FORMAT,
+ "lance",
+ LANCE_TABLE_REGISTER,
+ "true"));
+
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ lanceTableOps.createTable(
+ ident,
+ new Column[0],
+ null,
+ properties,
+ new Transform[0],
+ null,
+ new SortOrder[0],
+ new Index[0]));
+
+ verify(store, never()).put(any(), anyBoolean());
+ verify(store, never()).delete(any(), any());
+ Assertions.assertTrue(Files.isDirectory(directory));
+ Assertions.assertEquals("unrelated data", Files.readString(marker));
+ }
+
+ @ParameterizedTest
+ @CsvSource({"CREATE,true", "CREATE,false", "OVERWRITE,true",
"OVERWRITE,false"})
+ void testRegisterStorageFailureIsNotBadInput(String mode, boolean io) throws
Exception {
+ NameIdentifier ident = NameIdentifier.of("schema", "table");
+ Exception storageFailure =
+ io ? new IOException("storage timed out") : new
RuntimeException("storage unavailable");
+ Mockito.doAnswer(
+ invocation -> {
+ throw storageFailure;
+ })
+ .when(lanceTableOps)
+ .openDataset(anyString(), anyMap());
+
+ RuntimeException failure =
+ Assertions.assertThrows(RuntimeException.class, () -> register(ident,
mode));
+
+ Assertions.assertFalse(failure instanceof IllegalArgumentException);
+ Assertions.assertSame(storageFailure, failure.getCause());
+
Assertions.assertTrue(failure.getMessage().contains(storageFailure.getMessage()));
+ Assertions.assertTrue(failure.getMessage().contains(ident.toString()));
+ verify(store, never()).put(any(), anyBoolean());
+ verify(store, never()).delete(any(), any());
+ }
+
+ @Test
+ void testRegisterCloseFailureIsNotBadInput() throws Exception {
+ NameIdentifier ident = NameIdentifier.of("schema", "table");
+ Dataset dataset = mock(Dataset.class);
+ IllegalArgumentException closeFailure =
+ new IllegalArgumentException("allocator cleanup failed");
+ Mockito.doReturn(dataset).when(lanceTableOps).openDataset(anyString(),
anyMap());
+ doThrow(closeFailure).when(dataset).close();
+
+ RuntimeException failure =
+ Assertions.assertThrows(RuntimeException.class, () -> register(ident,
"OVERWRITE"));
+
+ Assertions.assertFalse(failure instanceof IllegalArgumentException);
+ Assertions.assertSame(closeFailure, failure.getCause());
+
Assertions.assertTrue(failure.getMessage().contains(closeFailure.getMessage()));
+ verify(store, never()).put(any(), anyBoolean());
+ verify(store, never()).delete(any(), any());
+ }
+
+ @Test
+ void testRegisterCreateChecksExistingNameBeforeAccessingDataset() throws
Exception {
+ NameIdentifier ident = NameIdentifier.of("schema", "table");
+ when(store.exists(ident, Entity.EntityType.TABLE)).thenReturn(true);
+
+ TableAlreadyExistsException failure =
+ Assertions.assertThrows(TableAlreadyExistsException.class, () ->
register(ident, "CREATE"));
+
+ Assertions.assertTrue(failure.getMessage().contains(ident.toString()));
+ verify(lanceTableOps, never()).openDataset(anyString(), anyMap());
+ verify(store, never()).put(any(), anyBoolean());
+ verify(store, never()).delete(any(), any());
+ }
+
+ @Test
+ void testRegisterCreateRetainsStrictInsertForConcurrentRegistration() throws
Exception {
+ NameIdentifier ident = NameIdentifier.of("schema", "table");
+ when(store.exists(ident, Entity.EntityType.TABLE)).thenReturn(false);
+ Dataset dataset = mock(Dataset.class);
+ Mockito.doReturn(dataset).when(lanceTableOps).openDataset(anyString(),
anyMap());
+ doThrow(new EntityAlreadyExistsException("concurrent registration"))
+ .when(store)
+ .put(any(), eq(false));
+
+ Assertions.assertThrows(TableAlreadyExistsException.class, () ->
register(ident, "CREATE"));
+
+ verify(store).put(any(), eq(false));
+ verify(dataset).close();
+ verify(store, never()).delete(any(), any());
+ }
+
+ @Test
+ void testRegisterCreateMetadataReadFailureStopsBeforeDatasetAccess() throws
Exception {
+ NameIdentifier ident = NameIdentifier.of("schema", "table");
+ IOException readFailure = new IOException("metadata unavailable");
+ when(store.exists(ident, Entity.EntityType.TABLE)).thenThrow(readFailure);
+
+ RuntimeException failure =
+ Assertions.assertThrows(RuntimeException.class, () -> register(ident,
"CREATE"));
+
+ Assertions.assertFalse(failure instanceof IllegalArgumentException);
+ Assertions.assertSame(readFailure, failure.getCause());
+ Assertions.assertTrue(failure.getMessage().contains(ident.toString()));
+ verify(lanceTableOps, never()).openDataset(anyString(), anyMap());
+ verify(store, never()).put(any(), anyBoolean());
+ }
+
+ private Table register(NameIdentifier ident, String mode) throws Exception {
+ Map<String, String> properties = Maps.newHashMap();
+ properties.put(Table.PROPERTY_LOCATION,
tempDir.resolve("registration").toString());
+ properties.put(Table.PROPERTY_TABLE_FORMAT, "lance");
+ properties.put(LANCE_TABLE_REGISTER, "true");
+ properties.put(LANCE_CREATION_MODE, mode);
+ properties.put(StringIdentifier.ID_KEY,
StringIdentifier.fromId(1L).toString());
+ return PrincipalUtils.doAs(
+ new UserPrincipal("tester"),
+ () ->
+ lanceTableOps.createTable(
+ ident,
+ new Column[0],
+ null,
+ properties,
+ new Transform[0],
+ null,
+ new SortOrder[0],
+ new Index[0]));
+ }
+
private static TableEntity tableEntity(
NameIdentifier ident, List<ColumnEntity> columns, Map<String, String>
properties) {
return TableEntity.builder()
diff --git
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/integration/test/CatalogGenericCatalogLanceIT.java
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/integration/test/CatalogGenericCatalogLanceIT.java
index 64ccdbd123..72ceedf795 100644
---
a/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/integration/test/CatalogGenericCatalogLanceIT.java
+++
b/catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/lance/integration/test/CatalogGenericCatalogLanceIT.java
@@ -54,6 +54,7 @@ import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Schema;
import org.apache.gravitino.client.GravitinoMetalake;
import org.apache.gravitino.exceptions.NoSuchCatalogException;
+import org.apache.gravitino.exceptions.NoSuchTableException;
import org.apache.gravitino.integration.test.util.BaseIT;
import org.apache.gravitino.integration.test.util.GravitinoITUtils;
import org.apache.gravitino.rel.Column;
@@ -1207,7 +1208,7 @@ public class CatalogGenericCatalogLanceIT extends BaseIT {
@Test
void testRegisterWithNonExistLocation() {
- // Now try to register a table with non-existing location with CREATE mode
- should succeed
+ // Registration must reject a missing dataset without creating catalog
metadata or a directory.
String newTableName = GravitinoITUtils.genRandomName(TABLE_PREFIX);
NameIdentifier newTableIdentifier = NameIdentifier.of(schemaName,
newTableName);
Map<String, String> newCreateProperties = createProperties();
@@ -1220,26 +1221,24 @@ public class CatalogGenericCatalogLanceIT extends
BaseIT {
newCreateProperties.put(Table.PROPERTY_EXTERNAL, "true");
newCreateProperties.put(LANCE_TABLE_REGISTER, "true");
- Table nonExistingTable =
- catalog
- .asTableCatalog()
- .createTable(
- newTableIdentifier,
- new Column[0],
- "Updated comment",
- newCreateProperties,
- Transforms.EMPTY_TRANSFORM,
- Distributions.NONE,
- new SortOrder[0]);
-
- Assertions.assertNotNull(nonExistingTable);
- Assertions.assertEquals(
- newLocation,
nonExistingTable.properties().get(Table.PROPERTY_LOCATION));
- Assertions.assertEquals(dirExists, new File(newLocation).exists());
-
- // Now try to drop table, there should no problem here
- boolean dropSuccess =
catalog.asTableCatalog().dropTable(newTableIdentifier);
- Assertions.assertTrue(dropSuccess);
+ IllegalArgumentException failure =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ catalog
+ .asTableCatalog()
+ .createTable(
+ newTableIdentifier,
+ new Column[0],
+ "Updated comment",
+ newCreateProperties,
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ new SortOrder[0]));
+ Assertions.assertTrue(failure.getMessage().contains("Could not validate
location"));
+ Assertions.assertFalse(new File(newLocation).exists());
+ Assertions.assertThrows(
+ NoSuchTableException.class, () ->
catalog.asTableCatalog().loadTable(newTableIdentifier));
}
@Test
diff --git a/docs/lakehouse-generic-lance-table.md
b/docs/lakehouse-generic-lance-table.md
index 92afc8acb7..c6b5e55c35 100644
--- a/docs/lakehouse-generic-lance-table.md
+++ b/docs/lakehouse-generic-lance-table.md
@@ -150,12 +150,14 @@ Required and optional properties for tables in a Generic
Lakehouse Catalog:
| `location` | Storage path for table metadata and data, Lance
supports: S3, GCS, OSS, AZ, File, Memory and file-object-store.
| (none) | Conditional* |
| `external` | Whether the data directory is an external location.
If it's `true`, dropping a table will only remove metadata in Gravitino and
will not delete the data directory, and purge table will delete both. For a
non-external table, dropping will drop both.
| false | No |
| `lance.creation-mode` | Create mode: for create table, it can be `CREATE`,
`EXIST_OK` or `OVERWRITE`. and it should be `CREATE` or `OVERWRITE` for
registering tables
| `CREATE` | No |
-| `lance.register` | Whether it is a register table operation. If it's
`true`, This API will not create data directory actually and it's the user's
responsibility to create and manage the data directory. `false` it will
actually create a table.
| false | No |
+| `lance.register` | Whether it is a register table operation. If `true`,
register an existing, readable Lance dataset without creating data. A missing
dataset or plain directory is rejected. Registered tables are external. If
`false`, create a dataset.
| false | No |
| `lance.storage.xxxx` | Any additional storage-specific properties required
by Lance format (e.g., S3 credentials, HDFS configs). Replace `xxxx` with
actual property names. For example, we can use
`lance.storage.aws_access_key_id` to set S3 aws_access_key_id when using a S3
location, for detail, refer to https://lancedb.com/docs/storage/integrations/ |
(none) | No |
- `CREATE`: Create a new table, fail if the table already exists.
- `EXIST_OK`: Create a new table if it does not exist, otherwise do nothing.
-- `OVERWRITE`: Create a new table, overwrite if the table already exists, it
will delete the existing data directory first if the table is not a registered
table and then create a new one.
+- `OVERWRITE`: For creation (`lance.register=false`), purge the existing table
and its dataset before creating a replacement. For registration
(`lance.register=true`), validate the replacement dataset first and replace
only external-table metadata, retaining the old dataset. Registration overwrite
of a managed table is rejected; explicitly dropping or purging a managed table
deletes its data.
+
+Registration validation checks dataset existence and readability, not an
operator-defined storage scope or authorization to purge external data.
### Format boundary
@@ -350,7 +352,7 @@ tableCatalog.createTable(
:::tip Registration vs Creation
- **Registration** (`lance.register: true`):
- - Links to existing Lance dataset or a path placeholder
+ - Requires an existing, readable Lance dataset; missing datasets and plain
directories are rejected
- Schema automatically detected from Lance metadata
- Useful for importing existing datasets
diff --git a/docs/lance-rest-service.md b/docs/lance-rest-service.md
index 1064d77f0f..e0f9c9ec78 100644
--- a/docs/lance-rest-service.md
+++ b/docs/lance-rest-service.md
@@ -110,6 +110,12 @@ canonical form.
**RegisterTable vs CreateTable**:
- **RegisterTable**: Links existing Lance datasets into Gravitino catalog
without data movement
- **CreateTable**: Creates an empty Lance dataset using the schema from the
Arrow IPC stream
+
+Registration requires an existing, readable Lance dataset. A plain directory
or missing dataset
+is rejected before any existing table is removed. Registration with
`overwrite` replaces only an
+external table's metadata and preserves its dataset; it rejects existing
managed tables. Replacing
+a managed table requires an explicit drop first, which deletes the managed
dataset.
+
:::note
The current `CreateTable` implementation accepts schema-only Arrow streams,
including zero-row
batches. A stream containing rows returns HTTP `406` before any metadata or
dataset changes,
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceDatasetTestUtils.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceDatasetTestUtils.java
new file mode 100644
index 0000000000..09c274d6ee
--- /dev/null
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceDatasetTestUtils.java
@@ -0,0 +1,42 @@
+/*
+ * 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.gravitino.lance.integration.test;
+
+import java.io.IOException;
+import java.util.List;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.lance.Dataset;
+import org.lance.WriteParams;
+
+/** Creates existing native datasets for registration tests. */
+final class LanceDatasetTestUtils {
+ private LanceDatasetTestUtils() {}
+
+ static void createDataset(String location) throws IOException {
+ try (Dataset ignored =
+ Dataset.write()
+ .schema(new Schema(List.of(Field.nullable("id", new
ArrowType.Int(32, true)))))
+ .uri(location)
+ .mode(WriteParams.WriteMode.CREATE)
+ .execute()) {
+ // Close the native dataset before exercising registration through the
REST service.
+ }
+ }
+}
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceFormatBoundaryIT.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceFormatBoundaryIT.java
index c23e63dc7a..675af39340 100644
---
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceFormatBoundaryIT.java
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceFormatBoundaryIT.java
@@ -231,7 +231,9 @@ public class LanceFormatBoundaryIT extends BaseIT {
RegisterTableRequest registerRequest = new RegisterTableRequest();
registerRequest.setId(deltaIds);
-
registerRequest.setLocation(tempDir.resolve("rest_register_overwrite").toString());
+ String registerLocation =
tempDir.resolve("rest_register_overwrite").toString();
+ LanceDatasetTestUtils.createDataset(registerLocation);
+ registerRequest.setLocation(registerLocation);
registerRequest.setMode("overwrite");
LanceNamespaceException registerException =
Assertions.assertThrows(
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
index 41ac0e6eee..68420eba26 100644
---
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceRESTServiceIT.java
@@ -774,11 +774,12 @@ public class LanceRESTServiceIT extends BaseIT {
}
@Test
- void testRegisterTable() {
+ void testRegisterTable() throws IOException {
catalog = createCatalog(CATALOG_NAME);
createSchema();
String location = tempDir + "/" + "register/";
+ LanceDatasetTestUtils.createDataset(location);
List<String> ids = List.of(CATALOG_NAME, SCHEMA_NAME, "table_register");
RegisterTableRequest registerTableRequest = new RegisterTableRequest();
registerTableRequest.setLocation(location);
@@ -788,9 +789,9 @@ public class LanceRESTServiceIT extends BaseIT {
RegisterTableResponse response = ns.registerTable(registerTableRequest);
Assertions.assertNotNull(response);
- // The location should not exist yet as we do not create it in advanced.
+ // Registration retains the existing dataset.
Assertions.assertEquals(location, response.getLocation());
- Assertions.assertFalse(new File(location).exists());
+ Assertions.assertTrue(new File(location).exists());
DescribeTableRequest describeTableRequest = new DescribeTableRequest();
describeTableRequest.setId(ids);
@@ -799,19 +800,30 @@ public class LanceRESTServiceIT extends BaseIT {
Assertions.assertEquals(location, loadTable.getLocation());
Assertions.assertTrue(loadTable.getMetadata().containsKey("key1"));
+ // An existing name must report a conflict without trying to open the new
location.
+ String missingLocation = tempDir + "/register-missing/";
+ registerTableRequest.setLocation(missingLocation);
+ RuntimeException conflict =
+ Assertions.assertThrows(
+ RuntimeException.class, () ->
ns.registerTable(registerTableRequest));
+ assertLanceErrorCode(conflict, ErrorCode.TABLE_ALREADY_EXISTS);
+ Assertions.assertFalse(new File(missingLocation).exists());
+ Assertions.assertEquals(location,
ns.describeTable(describeTableRequest).getLocation());
+
// Test register again with OVERWRITE mode
String newLocation = tempDir + "/" + "register_new/";
+ LanceDatasetTestUtils.createDataset(newLocation);
registerTableRequest.setMode("overwrite");
registerTableRequest.setLocation(newLocation);
response = Assertions.assertDoesNotThrow(() ->
ns.registerTable(registerTableRequest));
Assertions.assertNotNull(response);
Assertions.assertEquals(newLocation, response.getLocation());
- // The location should not exist yet as we do not create it in advanced.
- Assertions.assertFalse(new File(newLocation).exists());
+ // Registration retains the existing dataset.
+ Assertions.assertTrue(new File(newLocation).exists());
// Register a new table with location exists
String existingLocation = tempDir + "/" + "existing_location/";
- new File(existingLocation).mkdirs();
+ LanceDatasetTestUtils.createDataset(existingLocation);
Assertions.assertTrue(new File(existingLocation).exists());
registerTableRequest.setMode("create");
registerTableRequest.setLocation(existingLocation);
@@ -845,17 +857,19 @@ public class LanceRESTServiceIT extends BaseIT {
// Test Overwrite again after deregister
String nonExistingLocation = tempDir + "/" + "non_existing_location/";
+ LanceDatasetTestUtils.createDataset(nonExistingLocation);
registerTableRequest.setMode("overwrite");
registerTableRequest.setId(ids);
registerTableRequest.setLocation(nonExistingLocation);
response = Assertions.assertDoesNotThrow(() ->
ns.registerTable(registerTableRequest));
Assertions.assertNotNull(response);
Assertions.assertEquals(nonExistingLocation, response.getLocation());
- Assertions.assertFalse(new File(nonExistingLocation).exists());
+ Assertions.assertTrue(new File(nonExistingLocation).exists());
// Test registerTable with null mode after deregister should succeed
registerTableRequest.setMode(null);
String verifyNullModeLocation = tempDir + "/" +
"verify_null_mode_location/";
+ LanceDatasetTestUtils.createDataset(verifyNullModeLocation);
registerTableRequest.setLocation(verifyNullModeLocation);
registerTableRequest.setId(List.of(CATALOG_NAME, SCHEMA_NAME,
"verify_null_mode_location"));
response = Assertions.assertDoesNotThrow(() ->
ns.registerTable(registerTableRequest));
@@ -1121,13 +1135,14 @@ public class LanceRESTServiceIT extends BaseIT {
}
@Test
- void testDropTableWhenLocationMissingOrNotCreated() {
+ void testDropTableWhenLocationMissingOrNotCreated() throws IOException {
catalog = createCatalog(CATALOG_NAME);
createSchema();
- // Create table via register with non-existent location
+ // Register a valid dataset, then remove its location before dropping the
table
List<String> registerIds = List.of(CATALOG_NAME, SCHEMA_NAME,
"register_missing_location");
String missingLocation = tempDir + "/" + "register_missing_location/";
+ LanceDatasetTestUtils.createDataset(missingLocation);
RegisterTableRequest registerTableRequest = new RegisterTableRequest();
registerTableRequest.setId(registerIds);
registerTableRequest.setLocation(missingLocation);
@@ -1135,6 +1150,7 @@ public class LanceRESTServiceIT extends BaseIT {
RegisterTableResponse registerTableResponse =
Assertions.assertDoesNotThrow(() ->
ns.registerTable(registerTableRequest));
Assertions.assertEquals(missingLocation,
registerTableResponse.getLocation());
+ FileUtils.deleteQuietly(new File(missingLocation));
Assertions.assertFalse(new File(missingLocation).exists());
DropTableRequest dropRegisteredTable = new DropTableRequest();
@@ -1148,7 +1164,7 @@ public class LanceRESTServiceIT extends BaseIT {
// Register table with an existing location, then delete the location
before drop
List<String> existingIds = List.of(CATALOG_NAME, SCHEMA_NAME,
"existing_then_deleted");
String existingLocation = tempDir + "/" + "existing_then_deleted/";
- new File(existingLocation).mkdirs();
+ LanceDatasetTestUtils.createDataset(existingLocation);
Assertions.assertTrue(new File(existingLocation).exists());
RegisterTableRequest existingRegisterRequest = new RegisterTableRequest();
diff --git
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceTableAuthorizationIT.java
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceTableAuthorizationIT.java
index 36f41c8b3e..400eb40add 100644
---
a/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceTableAuthorizationIT.java
+++
b/lance/lance-rest-server/src/test/java/org/apache/gravitino/lance/integration/test/LanceTableAuthorizationIT.java
@@ -139,10 +139,8 @@ public class LanceTableAuthorizationIT extends BaseIT {
Table.PROPERTY_EXTERNAL,
"true"));
// Registered by the admin, so neither owner below owns the table itself.
- assertStatus(
- 200, register(ADMIN, OWNED_SCHEMA, OWNED_SCHEMA_TABLE, null,
location(OWNED_SCHEMA_TABLE)));
- assertStatus(
- 200, register(ADMIN, LONE_SCHEMA, LONE_SCHEMA_TABLE, null,
location(LONE_SCHEMA_TABLE)));
+ assertStatus(200, registerExistingTable(ADMIN, OWNED_SCHEMA,
OWNED_SCHEMA_TABLE));
+ assertStatus(200, registerExistingTable(ADMIN, LONE_SCHEMA,
LONE_SCHEMA_TABLE));
setSchemaOwner(metalake, OWNED_SCHEMA, SCHEMA_OWNER);
setSchemaOwner(metalake, LONE_SCHEMA, LONE_OWNER);
@@ -292,7 +290,7 @@ public class LanceTableAuthorizationIT extends BaseIT {
assertStatus(403, register(READER, SCHEMA, "reader_table", null,
location("reader_table")));
assertStatus(403, declare(READER, SCHEMA, "reader_table"));
- assertStatus(200, register(PROBER, WRITE_SCHEMA, PROBER_TABLE, null,
location(PROBER_TABLE)));
+ assertStatus(200, registerExistingTable(PROBER, WRITE_SCHEMA,
PROBER_TABLE));
// Creating assigns ownership, so the creator can read back what it
created even though it
// holds no SELECT_TABLE privilege.
assertStatus(200, table(PROBER, WRITE_SCHEMA, PROBER_TABLE, "describe"));
@@ -337,9 +335,8 @@ public class LanceTableAuthorizationIT extends BaseIT {
assertStatus(200, table(ADMIN, VISIBLE_TABLE, "describe"));
// Creating assigns ownership, so the creator may use either removal
operation.
- assertStatus(
- 200, register(PROBER, WRITE_SCHEMA, DEREGISTER_TABLE, null,
location(DEREGISTER_TABLE)));
- assertStatus(200, register(PROBER, WRITE_SCHEMA, DROP_TABLE, null,
location(DROP_TABLE)));
+ assertStatus(200, registerExistingTable(PROBER, WRITE_SCHEMA,
DEREGISTER_TABLE));
+ assertStatus(200, registerExistingTable(PROBER, WRITE_SCHEMA, DROP_TABLE));
assertStatus(200, table(PROBER, WRITE_SCHEMA, DEREGISTER_TABLE,
"deregister"));
assertStatus(200, table(PROBER, WRITE_SCHEMA, DROP_TABLE, "drop"));
assertStatus(404, table(ADMIN, WRITE_SCHEMA, DEREGISTER_TABLE,
"describe"));
@@ -444,7 +441,7 @@ public class LanceTableAuthorizationIT extends BaseIT {
}
private void registerTable(String tableName) throws Exception {
- assertStatus(200, register(ADMIN, SCHEMA, tableName, null,
location(tableName)));
+ assertStatus(200, registerExistingTable(ADMIN, SCHEMA, tableName));
}
private void createTable(String schemaName, String tableName) throws
Exception {
@@ -462,6 +459,12 @@ public class LanceTableAuthorizationIT extends BaseIT {
assertStatus(200, httpClient.send(request,
HttpResponse.BodyHandlers.ofString()));
}
+ private HttpResponse<String> registerExistingTable(String user, String
schema, String tableName)
+ throws Exception {
+ LanceDatasetTestUtils.createDataset(location(tableName));
+ return register(user, schema, tableName, null, location(tableName));
+ }
+
private HttpResponse<String> register(
String user, String schema, String tableName, String mode, String
location) throws Exception {
RegisterTableRequest body = new RegisterTableRequest();