Skip to content
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

Nessie: Support views for NessieCatalog #8909

Merged
merged 6 commits into from Dec 12, 2023
Merged

Conversation

ajantha-bhat
Copy link
Member

@ajantha-bhat ajantha-bhat commented Oct 23, 2023

  • Extend NessieCatalog with BaseMetastoreViewCatalog.
  • Move some common code to NessieUtil for reusing between tables and views.
  • Generalize ViewCatalogTests to reuse for Nessie catalog.

Fixes: #8696

.isInstanceOf(NoSuchTableException.class)
.hasMessageStartingWith("Table does not exist: ns.view"),
throwable ->
assertThat(throwable)
Copy link
Member Author

Choose a reason for hiding this comment

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

Nessie throws AlreadyExistsException for a view in this case. Do we need to wrap it up with NoSuchTableException for a table in this case to generalize like other catalogs?

Copy link
Member Author

Choose a reason for hiding this comment

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

Even Hive view support has same problem
#8907

Copy link
Contributor

@dimas-b dimas-b Oct 31, 2023

Choose a reason for hiding this comment

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

Altering core test expectation on a PR that adds a specific view implementation does not look right to me.

If core test expectations are sub-optimal perhaps we can adjust them in a separate PR, then rebase the Nessie view PR.

That said, I think the exception thrown in this test case should be the same across all catalog implementations. WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

So far, REST and inmemory catalog follows one pattern (NoSuchTableException) and all other catalogs follows one pattern (AlreadyExistsException).

I tried making Nessie to follow as REST catalog, but it breaks other testcases. I will check more.

Copy link
Member Author

Choose a reason for hiding this comment

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

replaceTableViaTransactionThatAlreadyExistsAsView
NessieTableOperations.doRefresh() --> throws AlreadyExistsException. But expecting NoSuchViewException.

If I fix it, another test case (createOrReplaceTableViaTransactionThatAlreadyExistsAsView) fails.
Because from the same place doRefresh() we are expecting two different kind of exceptions. I think test case need to be modified instead of unifying code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we follow the in-memory catalog pattern in Nessie?

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually think that the way Nessie currently handles this error case is correct by saying that a view with the same name already exists when you try to create a table. To achieve the same for REST is actually slightly more difficult. There's also a TODO in the test a few lines above where I wanted to improve the error reporting for these 2 particular cases that were adjusted in this test. That being said, I'm +1 on these 2 changes in the test

Copy link
Contributor

Choose a reason for hiding this comment

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

this should be addressed by c63be65

Copy link
Member Author

Choose a reason for hiding this comment

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

@nastra: Awesome. Thanks for fixing it for REST and in-memory catalog. That should help in having a unified behaviour.

// View loaded from catalog will have all the representation as it parses the view metadata
// file.
SQLViewRepresentation sqlViewRepresentation =
(SQLViewRepresentation) metadata.currentVersion().representations().get(0);
Copy link
Member Author

Choose a reason for hiding this comment

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

Currently Nessie's IcebergView model stores only one dialect and representation. I didn't change Nessie side to hold multiple representation because it was needed for global state. Now that global state is removed in Nessie, only view metadata location is enough. It will have current version id and all the representation for that version.

Copy link
Contributor

Choose a reason for hiding this comment

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

it seems weird to only store one and not all of the representations

Copy link
Member Author

Choose a reason for hiding this comment

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

Either Nessie can store all the representations or None I guess. But storing representations is not really required as global state is removed in Nessie, only view metadata location is enough. Lets see what others think.

cc: @dimas-b, @snazy

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we even bother with storing representations in Nessie at this point? I'd say, until Nessie supports multiple representations, we can just keep metadata location and ignore all representations here. These fields are nullable in Nessie.

Copy link
Member Author

Choose a reason for hiding this comment

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

True. I mentioned this above.

Either Nessie can store all the representations or None I guess

I think we don't need to store any representation if we don't need those for metadata in Nessie project. So, I am waiting for @snazy's reply.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agree.

Copy link
Member

Choose a reason for hiding this comment

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

All metadata should really be taken directly from the view metadata - Nessie should really not store the dialect nor the SQL representation. I don't understand how this related to "global state" - those things are not related to each other at all.

Also the unconditional cast is error prone IMO - similar to assuming that there is only one representation - either of different kinds or multiple SQL representation.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't understand how this related to "global state" - those things are not related to each other at all.

IcebergTable model used to only store table metadata location. But then it started storing other info like schema id, snapshot id etc to support global state. The IcebergView model developed was also based on the design of IcebergTable (which was based on global state). Else just View metadata location was enough. Thats how I thought it is designed.

Nessie should really not store the dialect nor the SQL representation

Ok. I will update as per this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Aah, SqlText is not nullable. So, I cannot construct without it. I will raise a PR at Nessie side to make it Nullable.

https://github.com/projectnessie/nessie/blob/fff9154da6a19cf289c09f2996b75f3cd15e0ca0/api/model/src/main/java/org/projectnessie/model/IcebergView.java#L56

Copy link
Member Author

Choose a reason for hiding this comment

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

added a dummy sqlText and dummy dialect as we have concluded that we don't have to modify at Nessie side.

import org.projectnessie.model.Branch;
import org.projectnessie.model.Reference;

public class TestBranchVisibilityForView extends BaseTestIceberg {
Copy link
Member Author

Choose a reason for hiding this comment

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

Similar to Table's TestBranchVisibility

Copy link
Member

Choose a reason for hiding this comment

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

This test class duplicates the test cases - most are not specific to views.
It seems the view-specific stuff can be merged into TestBranchVisibility.

Copy link
Member Author

Choose a reason for hiding this comment

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

This test class duplicates the test cases - most are not specific to views.

I will remove TestBranchVisibilityForView

@ajantha-bhat ajantha-bhat marked this pull request as ready for review October 27, 2023 06:14
@ajantha-bhat
Copy link
Member Author

PR is ready for review.
cc: @nastra, @dimas-b, @snazy, @adutra

maybeThrowSpecializedException((NessieReferenceConflictException) ex);
} catch (NessieConflictException | NessieNotFoundException | HttpClientException ex) {
NessieUtil.handleExceptionsForCommits(ex, client.refName(), failure);
} catch (NessieBadRequestException ex) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Here too, I think this catch block is a code smell, but this one is trickier to solve.

The issue is that in the test called createTableViaTransactionThatAlreadyExistsAsView, we create a transaction based on current hash (let's call it c0), then we commit something outside the transaction (let's call it c1), then we attempt to commit the transaction.

As a result, the transaction/client is trying to commit using the expected hash of c1, but it should really use c0 as the expected commit hash.

There is a property to convey the commit ID: NESSIE_COMMIT_ID_PROPERTY, but it doesn't work when creating things, only when updating.

I think we could solve the problem by storing the current expected hash in a field inside NessieTableOperations, then passing that to the client:

public class NessieTableOperations extends BaseMetastoreTableOperations {
...
  private Reference reference;
...
  NessieTableOperations(...) {
    ...
    reference = client.getRef().getReference();
  }
...
  protected void doRefresh() {
    ...
    String metadataLocation = null;
    this.reference = client.getRef().getReference();
    ...
  }
...
  protected void doCommit(TableMetadata base, TableMetadata metadata) {
      ...
      client.commitTable(base, metadata, newMetadataLocation, contentId, key, reference);
      ...
  }
}

I did a quick experiment with the above suggestion and the test threw NessieConflictException as expected, instead of NessieBadRequestException.

@dimas-b what would you suggest in this situation?

Copy link
Member Author

Choose a reason for hiding this comment

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

In my understanding, commits will be retried on failure for NessieConflictException and it will call doRefresh() for retry which will again use the new reference?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the proper way to fix it is to check if any different kind of content exist for the key while committing this key in doCommit() of NessieTableOperation and NessieViewOperation. But that is an extra round trip with server and might degrade commit performance slightly?

Copy link
Contributor

Choose a reason for hiding this comment

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

  1. Conflicts will not be retried for NessieReferenceConflictException since we throw specialized errors, see here:
    if (exception instanceof NessieReferenceConflictException) {
    // Throws a specialized exception, if possible
    NessieUtil.maybeThrowSpecializedException(
    (NessieReferenceConflictException) exception, false);
    }
  2. My main point was: we should get rid of this catch (NessieBadRequestException ex) block as, in my opinion, it denotes a code smell.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the proper way to fix it is to check if any different kind of content exist for the key while committing this key in doCommit() of NessieTableOperation and NessieViewOperation. But that is an extra round trip with server and might degrade commit performance slightly?

This solution is viable but would imho be less efficient since, as you mentioned, it involves an extra roundtrip.

I personally think that if NessieTableOperation could "memorize" the commit hash of that last time it did a refresh, then reuse it to commit the table changes, that would solve the problem in a better way since it does not involve fetching contents.

Copy link
Member Author

Choose a reason for hiding this comment

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

That particular testcase is tricky because createTransaction() will call refresh() which already has code to check if any other content exist with the same name. But during commitTransaction() it will not call refresh() so that code is never hit and we need to check again if any new content created with same key concurrently.

So, I have added that logic now instead of catching NessieBadRequestException later on.

I am not sure about memorize commit hash solution. Last time (some other PR) we concluded that we need to be as stateless as possible. Lets see what others think.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's a good idea to record the current commit hash for "new" table/view commits at the time NessieTableOperations is created. At the same time, if the commit is an update, I think we should use the hash from the metadata (basically track it for views too).

Copy link
Contributor

Choose a reason for hiding this comment

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

Re: NessieBadRequestException, I agree that we should not try to "handle" it. Instead the catalog should be implemented such that NessieBadRequestException does not happen.

@ajantha-bhat
Copy link
Member Author

@nastra and @adutra: Can you please take another look? I have addressed the comments.

Content content = null;
try {
content =
client.getApi().getContent().key(key).reference(client.getReference()).get().get(key);
Copy link
Contributor

Choose a reason for hiding this comment

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

Checking for the content here imho defeats the purpose of calling doRefresh only when necessary. I think I still prefer to store the hash of the last refresh, then use it for commit. @dimas-b wdyt?

// Need a fix from Nessie (https://github.com/projectnessie/nessie/issues/7645)
}

// Overriding the below rename view testcases to exclude checking same view metadata after rename.
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems problematic to me. It would be better to engage with core devs and check whether the metadata is allowed to have extra properties. @nastra wdyt?

At first glance, the assertion seems indeed a bit restrictive:

    assertThat(((BaseView) renamed).operations().current())
        .usingRecursiveComparison()
        .ignoringFieldsOfTypes(Schema.class)
        .isEqualTo(original);

Maybe change the semantics to "contains all the original fields but may also contain some extra ones?"

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that would be ok to change the semantics to contains all the original fields but may also contain some extra ones (in case a catalog impl decides to add new properties)

Copy link
Member Author

Choose a reason for hiding this comment

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

Not just that. The original property might modified during rename for Nessie.
Like "nessie.commit.id" is different for original and renamed one.

Shall I ignore the properties aomparision as a whole?

Copy link
Contributor

Choose a reason for hiding this comment

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

@ajantha-bhat can you please follow-up on this in a separate PR?

// Need a fix from Nessie (https://github.com/projectnessie/nessie/issues/7645)
}

// Overriding the below rename view testcases to exclude checking same view metadata after rename.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think that would be ok to change the semantics to contains all the original fields but may also contain some extra ones (in case a catalog impl decides to add new properties)

@ajantha-bhat ajantha-bhat force-pushed the nessie_view branch 2 times, most recently from 217aee0 to c727200 Compare December 8, 2023 10:00
}

/** Lists Iceberg table or view from the given namespace */
protected List<TableIdentifier> listContents(Namespace namespace, Content.Type type) {
Copy link
Contributor

Choose a reason for hiding this comment

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

is there any value in keeping this (and others) protected? I would have thought it should be ok to make this private, since all the other places would then use the table/view-specific methods

Copy link
Member Author

Choose a reason for hiding this comment

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

In the NessieCatalog itself I have some common code for content which calls these protected classes.

If I doesn't have common code in NessieCatalog, it will be code duplication.
For example see, NessieCatalog.renameContent()

Copy link
Member Author

Choose a reason for hiding this comment

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

NessieCatalog.table --> NessieCatalog.content --> NessieClient.content this way it is less code duplication.

NessieCatalog.table --> NessieClient.table --> NessieClient.content will cause duplicate lines of code in NessieCatalog

Copy link
Contributor

Choose a reason for hiding this comment

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

why can't those places just call renameTable() / renameView() directly?

@ajantha-bhat
Copy link
Member Author

@nastra: I have addressed the comments. Thanks for the review.

@ajantha-bhat
Copy link
Member Author

@nastra: Looks like #9012 is causing test failure for this PR after rebase. I tried fixing at Nessie side. But it opens up another issues. Please take a look.

@@ -239,33 +240,24 @@ public boolean dropTable(TableIdentifier identifier, boolean purge) {
TableReference tableReference = parseTableReference(identifier);
return client
.withReference(tableReference.getReference(), tableReference.getHash())
.dropTable(identifierWithoutTableReference(identifier, tableReference), purge);
.dropTable(identifierWithoutTableReference(identifier, tableReference), false);
Copy link
Contributor

Choose a reason for hiding this comment

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

@ajantha-bhat shouldn't this keep the purge flag?

fromReference.equalsIgnoreCase(toReference),
"Cannot rename %s '%s' on reference '%s' to '%s' on reference '%s':"
+ " source and target references must be the same.",
NessieUtil.contentTypeString(type).toLowerCase(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the expected text is in English we should use .toLowerCase(Locale.ENGLISH)). Using the default locale can have surprising results.... may be not in this particular case, but we ought to ensure correctness in general, I think.

Try this: assertThat("VIEW".toLowerCase(new Locale("TR"))).isEqualTo("view");

Copy link
Member Author

Choose a reason for hiding this comment

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

While adding it, I did check the Iceberg code.
.toLowerCase() is used in other places also. So, I followed it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed for new code added by this PR.

We can have GH issue for existing code to track it.

Copy link
Contributor

@dimas-b dimas-b Dec 11, 2023

Choose a reason for hiding this comment

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

I did a quick scan of the Iceberg codebase for this, and I do a few places where .toLowerCase() may be a concern:

  1. TableIdentifier.toLoweCase() is used only in CachingCatalog internally, which is probably ok, since the lower case strings are not exposed to the outside.

  2. VectorizedSupport seems like it may be a problem, but I do not really know whether the lower case data is exposed and how.

  3. IcebergRecordObjectInspector converts field names to lower case, and that seems to be affected by the locale problem as various case names are accepted as input parameters to its methods.

  4. jQuery code uses a lot of toLowerCase(), but I do not really know how it is supposed to be used.

@nastra : Do you think this is worth opening an issue? To the best of my knowledge this kind of case conversion can be problematic only in German and Turkish locales. The German locale affects only proper German language words (so it is less of a problem), but the Turkish locale can cause English words to be converted in unexpected ways. Does Iceberg support using its libraries in user-defined locales?

Copy link
Contributor

Choose a reason for hiding this comment

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

it's probably a good idea to open an issue and raise awareness about this

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

@dimas-b dimas-b left a comment

Choose a reason for hiding this comment

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

LGTM overall. Sorry for a few late comments.

}

// We try to drop the table. Simple retry after ref update.
// We try to drop the content. Simple retry after ref update.
try {
commitRetry(
String.format("Iceberg delete table %s", identifier),
Copy link
Contributor

Choose a reason for hiding this comment

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

This may be a view now, not just "table" (cf. line 554).

Copy link
Member Author

Choose a reason for hiding this comment

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

good catch. Induced during rebase.

maybeThrowSpecializedException((NessieReferenceConflictException) ex);
} catch (NessieConflictException | NessieNotFoundException | HttpClientException ex) {
if (ex instanceof NessieConflictException || ex instanceof NessieNotFoundException) {
failure = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it might be preferable to delegate determining the failure flag to NessieUtil.handleExceptionsForCommits. This logic should be the same between tables and views.

Perhaps handleExceptionsForCommits could return an object with both the flag and the exception to be re-thrown. WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

That will be more complicated I guess.

I have simplified a bit now. Please take a look

Comment on lines +125 to +128
NessieUtil.handleExceptionsForCommits(ex, client.refName(), Content.Type.ICEBERG_TABLE)
.ifPresent(
exception -> {
throw exception;
Copy link
Contributor

Choose a reason for hiding this comment

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

If I follow the logic correctly, handleExceptionsForCommits will always return something for the exception types caught in this case, so ifPresent() is really confusing. Could the code be refactored to avoid unnecessary conditional execution?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 on improving exception handling in the nessie code as I've raised this already before, because it's difficult to read & understand when certain things are thrown.
However, I would suggest to do this as an immediate follow-up after this PR is merged, as otherwise this makes it more difficult to review the changes being introduced for Views

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm fine with a follow-up.

Copy link
Member Author

Choose a reason for hiding this comment

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

Slightly improved now ;)

throw NessieUtil.handleBadRequestForCommit(client, key, Content.Type.ICEBERG_VIEW).orElse(ex);
} finally {
if (failure) {
io().deleteFile(newMetadataLocation);
Copy link
Contributor

Choose a reason for hiding this comment

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

This code appears to be shared with NessieTableOperations. Could we refactor it to have the exception/failure/cleanup logic in one place?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried before.
But one works on client.commitView and one is client.commitTable. So, it cannot be extracted to common code.
I did also try accepting a generic Runnable to execute. But it cannot throw the required exception. Hence, it has to be duplicated.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could define our own functional interface with declared exceptions.


private void resetData() throws NessieConflictException, NessieNotFoundException {
Branch defaultBranch = api.getDefaultBranch();
for (Reference r : api.getAllReferences().get().getReferences()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

tangential: This code is repeated in many downstream tests... Perhaps we could make a helper utility in Nessie OSS 🤔

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see the advantage of adding at Nessie side. But it can be added as Util for the test classes. But maybe as a follow up PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

fair enough.

Comment on lines +125 to +128
NessieUtil.handleExceptionsForCommits(ex, client.refName(), Content.Type.ICEBERG_TABLE)
.ifPresent(
exception -> {
throw exception;
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm fine with a follow-up.

@nastra nastra merged commit b309d9b into apache:main Dec 12, 2023
45 checks passed
@ajantha-bhat ajantha-bhat added this to the Iceberg 1.5.0 milestone Jan 3, 2024
lisirrx pushed a commit to lisirrx/iceberg that referenced this pull request Jan 4, 2024
geruh pushed a commit to geruh/iceberg that referenced this pull request Jan 26, 2024
devangjhabakh pushed a commit to cdouglas/iceberg that referenced this pull request Apr 22, 2024
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.

Add view support for Nessie catalog
5 participants