Skip to content

Core,Api: Add overwrite option when register external table to catalog #12228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

dramaticlly
Copy link
Contributor

@dramaticlly dramaticlly commented Feb 11, 2025

This PR adds a new register-table with overwrite option on Catalog interface to allow overwrite table metadata of an existing Iceberg table. The overwrite is achieved via TableOperations.commit(base, new) for catalogs extends BaseMetastoreCatalog.

@dramaticlly
Copy link
Contributor Author

Java CI Failure is timing out on concurrent fast append and seems unrelated to the change.

@rdblue @RussellSpitzer @danielcweeks do you want to take a look?

Copy link
Collaborator

@gaborkaszab gaborkaszab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this, @dramaticlly !

@@ -2871,6 +2872,33 @@ public void testRegisterExistingTable() {
assertThat(catalog.dropTable(identifier)).isTrue();
}

@Test
public void testRegisterAndOverwriteExistingTable() {
C catalog = catalog();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we just adding a bucket to test the change? Why not just use the table UUID? I feel like we should be able to just

Make Table 1
Make Table 2
Register overwrite Table1 with Table2
Check that metadata table1 matches table 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I think register with overwrite helps revert an existing table to a new previous health state. If we want to support overwrite with another tables's metadata, It seems better suited with drop + register, to reflect the table UUID change.

From the table spec, it asks Implementations to throw an exception if a table's UUID does not match the expected UUID when refreshing metadata. What do you think?

Copy link
Contributor

@stevenzwu stevenzwu Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a similar question in the code where it check the UUID doesn't change for register with overwrite. Because API is called registerTable, I would think think UUID change should be allowed. Interesting to hear other people's takes on this one.

Initially I think register with overwrite helps revert an existing table to a new previous health state.

for this initial use case, agree that UUID shouldn't change. But if we want to only solve this specific/narrower problem, maybe a narrower API would make more sense. Enforcing UUID check for this narrow API is totally the right thing to do.

resetTable(TableIdentifier identifier, String metadataFileLocation)

Copy link
Contributor Author

@dramaticlly dramaticlly Feb 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sharing my thoughts here, so I believe table UUID is the ideal way for consumer to identify the uniqueness of a given table, instead of relying on the table identifier in the given catalog.

Today, the table operation on refresh will check if underlying the UUID has changed, also REST catalog will have requirements on table UUID unchanged for replace and update of the table. If we want to support register overwrite with foreign table, then it secretly break the assumption, implies catalog need to evict the cached table (even with same table identifier) and force to reload.

Personally I think it's probably better to only support same table UUID on register with overwrite (which provides atomicity), and support table UUID change with drop table first and then reregister.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some thoughts, I changed behavior from reuse existing commit logic which pass the ops.current, to drop the table first and then register with given metadata. This relax the constraints on table UUID check, also ensure that latest metadataFileLocation in TableMetadata after overwrite is the same as user provided.

I also added a comment in interface to highlight the potential table UUID change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me unresolved this comment. wondering if it is desirable to have two different tables share the UUID.

Make Table 1
Make Table 2
Register overwrite Table1 with Table2

E.g., the MV spec PR currently defines storage table refresh-state with only UUID as table/view identifier.

Interesting to hear more inputs.

ops.commit(null, metadata);

TableMetadata currentMetadata = tableExists(identifier) ? ops.current() : null;
ops.commit(currentMetadata, TableMetadataParser.read(ops.io(), metadataFile));
Copy link
Member

@RussellSpitzer RussellSpitzer Feb 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little worried about passing through current metadata here. Is this just a workaround to the normal commit logic?

If the metadata changes from "current" by the time an overwrite request goes through then we don't want a retry or failure, it should still pass?

Copy link
Contributor Author

@dramaticlly dramaticlly Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping to reuse the existing commit logic for atomicity support, and also better lineage to track previous/old metadata for hive table and JDBC tables.

As for potential conflict when base is out of date, I think that's a valid concern and we probably do not want this operation fail as user intent is replace with provided table metadata. I am thinking about add a retry block to help, please let me know if you feel otherwise

AtomicBoolean isRetry = new AtomicBoolean(false);
// commit with retry
Tasks.foreach(ops)
  .retry(COMMIT_NUM_RETRIES_DEFAULT)
  .exponentialBackoff(
      COMMIT_MIN_RETRY_WAIT_MS_DEFAULT,
      COMMIT_MAX_RETRY_WAIT_MS_DEFAULT,
      COMMIT_TOTAL_RETRY_TIME_MS_DEFAULT,
      2.0 /* exponential */)
  .onlyRetryOn(CommitFailedException.class)
  .run(
      taskOps -> {
        TableMetadata base = isRetry.get() ? taskOps.refresh() : taskOps.current();
        isRetry.set(true);
        taskOps.commit(base, newMetadata);
      });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ended up to drop the table first and then commit with current TableMetadata = null so that we do not need to pass current metadata here. This also avoid the needs to use retry here.

@@ -2871,6 +2872,33 @@ public void testRegisterExistingTable() {
assertThat(catalog.dropTable(identifier)).isTrue();
}

@Test
public void testRegisterAndOverwriteExistingTable() {
C catalog = catalog();
Copy link
Contributor

@stevenzwu stevenzwu Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a similar question in the code where it check the UUID doesn't change for register with overwrite. Because API is called registerTable, I would think think UUID change should be allowed. Interesting to hear other people's takes on this one.

Initially I think register with overwrite helps revert an existing table to a new previous health state.

for this initial use case, agree that UUID shouldn't change. But if we want to only solve this specific/narrower problem, maybe a narrower API would make more sense. Enforcing UUID check for this narrow API is totally the right thing to do.

resetTable(TableIdentifier identifier, String metadataFileLocation)

"The requested metadata matches the existing metadata. No changes will be committed.");
return new BaseTable(ops, fullTableName(name(), identifier), metricsReporter());
}
dropTable(identifier, false /* Keep all data and metadata files */);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop the table first and then register with given metadata

what if the job failed btw these two steps? we can end up with table deleted (but new metadata not registered), which is also not ideal.

Thinking about it again. Enforcing UUID match for overwrite seems reasonable.

Copy link
Contributor Author

@dramaticlly dramaticlly Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional notes of switching from TableOperations.commit(ops.current, newTableMetadata) to drop and then re-register:

  1. This ensure the same input metadata.json is used to commit and result as latest TableMetadata.current.metadataLocation like in Core: Avoid creating new metadata file when registerTable API is used #6591. Existing doCommit logic
    protected String writeNewMetadataIfRequired(boolean newTable, TableMetadata metadata) {
    return newTable && metadata.metadataFileLocation() != null
    ? metadata.metadataFileLocation()
    : writeNewMetadata(metadata, currentVersion() + 1);
    }
    will only reuse the metadata.json instead of writing a new one if the commit is for creating a table. It would be difficult to differentiate the register-with-force and normal commit in doCommit method without change its interface for all TableOperations
  2. This also relax the constraint on matching the table UUID between existing table and new metadata to be overwritten
  3. Register-with-force in generally shall be user facing retrievable as end state is having the input metadata as the latest state of the table. Where ops.current() are subject to change if running with a race condition and require retry within method

Copy link
Contributor

@stevenzwu stevenzwu Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensure the same input metadata.json is used to commit
Register-with-force in generally shall be retrievable as end state is having the input metadata as the latest state of the table. Where ops.current() are subject to change if running with a race condition

I understand this may look intuitive for registerTable API. But what overwrite is essentially update the state of an existing table. Hence, the current behavior of writeNewMetadataIfRequired is fine to me. It would create and commit a new metadata file with the same content as the input file.

Let's assume current metadata file is meta-009.json. It is reasonable to me that registerTable("meta-005.json", true) would commit a new file meta-010.json with the same content as meta-005.json.

The real problem might be piggyback overwrite with registerTable. If it is a separate overwriteTable/resetTable API, then it won't be confusing that the commit metadata file is a new file with the same content as the input metadata file.

Copy link
Contributor

@stevenzwu stevenzwu Feb 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also relax the constraint on matching the table UUID between existing table and new metadata to be overwritten

This is where I have a second thought from my earlier stance. overwrite should enforce UUID match check. See my other comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have a disagreement here. I think the behavior should be identical to the registerTable without overwrite. The file that is being passed is the file that should be used for registration. For example, if i'm updating an table to match an existing one and I rely on the metadata.json path to tell what files are copied (like in dr) having a different file name could be really make things difficult.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you @guykhazma , I don't think we have any clear semantics expectation for register-table with overwrite in REST API to complete atomically. Table specification states that

Table state is maintained in metadata files. All changes to table state create a new metadata file and replace the old metadata with an atomic swap.

IMO, overwrite of table metadata is not a valid state change but rather a state overwrite, where state can even come from another table with a different table UUID. I had some offline discussion with @RussellSpitzer on this where we do agree on this can be catalog implementation specific and open the room for atomic swap if catalog can support this.

As for your proposed alternative approach, i think we can write multiple metadata.json on file system first and rely on catalog for atomic swap, but we might hit the same limitation in TableOperations API, where new metadata.json will be rewritten with a different file name as input and difficult to verify.

Copy link
Contributor

@guykhazma guykhazma Apr 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you @dramaticlly, Could you elaborate on why you see this as a state overwrite?

I can imagine a scenario where some state or partial state is transferred from another table (with a different UUID), which might be interpreted as a state change. However, I’m not sure the register API is the appropriate mechanism for that.

From my understanding, the purpose of the register API is to create a named reference to a metadata JSON. It doesn’t inherently imply any change to the actual state of a table. Even if you register it against an existing table and the resulting metadata reflects a different state, it doesn’t mean that the underlying storage state has changed.

For instance, it's possible to register the same table multiple times under different names using distinct metadata files—effectively simulating branching using different entries in the catalog.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delayed response. Let's say original table with identifier mytable and state A is represented by A.metadata.json and now we are registering with B.metadata.json. There's no guarantee that A and B has anything in common.

Probably because we are looking from different angles, While the underlying storage state may remain unchanged during a register-table operation, the perception of the table can shift significantly. From a data consumer’s standpoint, if the identifier mytable now references a different collection of data due to registration with the overwrite flag, its internal state—including metadata, schema, partitioning, and data—may have changed entirely.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dramaticlly I see your point. It seems to me the core question here is whether the responsibility for maintaining the lineage of a table identifier lies with the catalog or the table itself. From my perspective, it makes more sense for the catalog to handle this, especially since the overwrite operation doesn't alter the physical state of the table. Ideally, this reference change should be atomic, but the implementation details can be left to individual catalogs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it makes sense to move this into an explicit Table operation api. We essentially want something that's like
ops.setMetadata(newMetadata)

Which ignores validations and transactionally swaps. May be cleaner than doing the drop/create we are currently doing. This is essentially what any rest catalog could do and would fix @stevenzwu 's issues with atomicity by letting each catalog implementation decide whether to make it atomic or not.

@@ -2871,6 +2872,33 @@ public void testRegisterExistingTable() {
assertThat(catalog.dropTable(identifier)).isTrue();
}

@Test
public void testRegisterAndOverwriteExistingTable() {
C catalog = catalog();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me unresolved this comment. wondering if it is desirable to have two different tables share the UUID.

Make Table 1
Make Table 2
Register overwrite Table1 with Table2

E.g., the MV spec PR currently defines storage table refresh-state with only UUID as table/view identifier.

Interesting to hear more inputs.

… catalog

Update REST RegisterTableRequest model and parser to support overwrite
Enforce table UUID requirement
Add commit conflict and retry test in TestHiveCommits
Signed-off-by: Hongyue Zhang <[email protected]>
…e constraint on table UUID check between existing and new TableMetadata

Signed-off-by: Hongyue Zhang <[email protected]>
Signed-off-by: Hongyue Zhang <[email protected]>
* false.
*/
default Table registerTable(
TableIdentifier identifier, String metadataFileLocation, boolean overwrite) {
throw new UnsupportedOperationException("Registering tables is not supported");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is worth while, but you could decide only to fail if "overwrite" is true"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @RussellSpitzer , in this PR I introduced a new register-overwrite API on catalog interface, and changed as new base for register-table (where it can call the new API with overwrite=false).

Before:

default register-table API throw UnsupportedOperationException

After:

default register-table API -> register-overwrite(overwrite=true)
default register-overwrite API throw UnsupportedOperationException


The benefits are all concrete catalog implementations can just implement the new API and interface is only used for delegation between the 2 APIs. This is an easier to reason (as all register logic sits in one place) and follows the convention like drop-table and drop-table-purge.

The potential downside is that some custom catalog implementations outside iceberg repo who implements the register-table API, might need to update their code when upgrades iceberg dependency with the interface change. I feel like it's generally justified for customized catalog to keep up with iceberg interface change. Please let me know if you think otherwise.

// register table t1 with metadata from table t2
Table registered = catalog.registerTable(identT1, opsT2.current().metadataFileLocation(), true);

assertThat(registered.uuid())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just check that it matches t2 uuid rather than checking it doesn't match t1.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact why aren't we checking if the T2 object matches the "registered" table? Shouldn't they just be completely identical?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think the assertions below on line 3210 basically is checking for complete identification.

.isTrue();
assertThat(operation(registered).refresh())
.usingRecursiveComparison()
// Nessie catalog holds different Nessie commit-ID from which the metadata has been loaded.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should have nessie specific issues in the core module

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I agree, but since CatalogTests is abstract and existing TestNessieCatalog implements this require exclusion on some of the table properties. Do you think I shall add a assumeTrue to exclude Nessies from this test instead?

}

@Test
public void testRegisterAndOverwriteExistingTable() throws IOException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably shouldn't be supported at all for Hadoop Catalog? Won't this only work if you are replacing the Highest-metadata.json with a Higher-metadata.json?

Copy link
Contributor Author

@dramaticlly dramaticlly May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I think this is a good point as Hadoop table operations always look for highest version file in metadata root.

@dramaticlly dramaticlly reopened this May 12, 2025
Copy link

This pull request has been marked as stale due to 30 days of inactivity. It will be closed in 1 week if no further activity occurs. If you think that’s incorrect or this pull request requires a review, please simply write any comment. If closed, you can revive the PR at any time and @mention a reviewer or discuss it on the [email protected] list. Thank you for your contributions.

@github-actions github-actions bot added the stale label Jun 12, 2025
@dramaticlly
Copy link
Contributor Author

Not stale, going to send a discussion email on dev-list to understand the feasibility of #13057

@github-actions github-actions bot removed the stale label Jun 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants