Skip to content

HIVE-29578: Iceberg: add support for logical views#6449

Merged
difin merged 8 commits into
apache:masterfrom
difin:iceberg_native_views
Jun 10, 2026
Merged

HIVE-29578: Iceberg: add support for logical views#6449
difin merged 8 commits into
apache:masterfrom
difin:iceberg_native_views

Conversation

@difin

@difin difin commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Added support for Iceberg logical views in Hive for both HMS and REST catalogs.

Why are the changes needed?

To support Iceberg logical views. This can be especially useful for REST Catalog clients.

Does this PR introduce any user-facing change?

Yes, new HQL syntax:

  1. A regular Hive view syntax with tblproperties ('view-format'='iceberg'):
create [or replace] view [if not exists] <NAME> tblproperties ('view-format'='iceberg') 
as select <COLUMNS> from <SRC_TABLE> where <CONDITION>;
  1. Regular Hive syntax (without tblproperties ('view-format'='iceberg')) and Hive config hive.default.storage.handler.class pointing to a storage handler that supports external logical views:
set hive.default.storage.handler.class=org.apache.iceberg.mr.hive.HiveIcebergStorageHandler;

create [or replace] view [if not exists] <NAME> 
as select <COLUMNS> from <SRC_TABLE> where <CONDITION>;

How was this patch tested?

Created new and updated exiting unit and integration tests with Iceberg logical views test cases.

@difin difin force-pushed the iceberg_native_views branch from 4fdad42 to 252c608 Compare April 24, 2026 23:06
@difin difin force-pushed the iceberg_native_views branch from 252c608 to e10eba5 Compare April 24, 2026 23:31
@difin difin marked this pull request as ready for review April 24, 2026 23:31
@difin difin changed the title HIVE-29578: Iceberg: support for Iceberg native views HIVE-29578: Iceberg: support native views Apr 24, 2026
@difin difin changed the title HIVE-29578: Iceberg: support native views HIVE-29578: Iceberg: add support for native views Apr 24, 2026
@difin difin force-pushed the iceberg_native_views branch from e10eba5 to 96fa476 Compare April 25, 2026 20:46
@difin difin requested review from deniskuzZ and kasakrisz April 25, 2026 20:46
@difin difin force-pushed the iceberg_native_views branch from 96fa476 to 114412a Compare April 26, 2026 15:12
Comment thread iceberg/iceberg-handler/src/test/queries/positive/iceberg_native_view.q Outdated
Comment thread ql/src/java/org/apache/hadoop/hive/ql/ddl/view/create/CreateViewAnalyzer.java Outdated
Comment thread iceberg/iceberg-handler/src/test/queries/positive/iceberg_view.q
Comment thread ql/src/java/org/apache/hadoop/hive/ql/ddl/view/create/CreateViewAnalyzer.java Outdated
Comment thread ql/src/java/org/apache/hadoop/hive/ql/ddl/view/create/CreateViewDesc.java Outdated
Comment thread ql/src/java/org/apache/hadoop/hive/ql/ddl/view/create/CreateViewDesc.java Outdated
Comment thread ql/src/java/org/apache/hadoop/hive/ql/ddl/view/create/CreateViewDesc.java Outdated
public void createTable(CreateTableRequest request) throws TException {
Table table = request.getTable();
if (hasIcebergNativeViewTableType(table) && restCatalog instanceof ViewCatalog) {
createOrReplaceLogicalView(table, table.getDbName(), table.getTableName());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it leads to inconsistent behavior: if we call the method on a table, it creates it if it is a new one and throws an AlreadyExistsException if the table already exists and also if a view with the same name is already exists.
But if we call this method on a view, it will create it even if a table exists with the same name and replaces the existing one if the view is already exists.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This method cannot be called on a view if it already exists - see #6449 (comment)


String comment = tblProps.get("comment");
IcebergNativeLogicalViewSupport.createOrReplaceNativeView(
conf, dbName, tableName, cols, table.getViewExpandedText(), tblProps, comment);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The view text is pretty interesting difference between Hive and Iceberg:
Hive stores it in two versions: original and expanded text.

Iceberg has only one field, sql.

I think if we want to be compatible with both Hive and Iceberg, I would rather choose the original text here and add the expanded text as a tableproperty.

But also, there is a catch: the sql is stored in ViewVersion so it can differ for each version. But in Hive, it is a single property of the table metadata.

Honestly, I'm not sure what is the correct solution to do that but if we store only one of them, I fear we can loose functionality on Hive side.

@difin difin Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe passing table.getViewExpandedText() is more correct because SemanticAnalyzer uses expanded text when resolving views.

On the write view path, view text is stored in HMS mainly for passing it to the IcebergNativeLogicalViewSupport.createOrReplaceNativeView, but on the read path view text is always retrieved from Iceberg API both on HMS and REST paths in BaseHiveIcebergMetaHook:

  @Override
  public void postGetTable(org.apache.hadoop.hive.metastore.api.Table hmsTable) {
    if (hmsTable != null) {
      try {
        if (isNativeIcebergLogicalView(hmsTable)) {
          IcebergNativeLogicalViewSupport.enrichHmsTableFromIcebergView(hmsTable, conf);
          return;
        }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree on storing the expanded query text if only one field is available. The expanded query text is used by the Hive compiler to resolve view definitions. IIUC the original query text is more user friendly when printing the view definition via describe.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would add the original text as a query property to be able to support the same kind of output. But I can live with this solution as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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


try {
if (catalog instanceof Closeable closeable) {
try (Closeable ignored = closeable) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure I'm missing something: what is the reason of checking if the catalog is Closeable and ignoring it?

@difin difin Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

buildIcebergCatalog gives us a one-off catalog for this call. The Catalog interface isn’t Closeable, but some implementations are (RestCatalog is Closable) and they hold clients/IO that need to be closed when we’re done. HiveCatalog isn’t, so we only wrap when instanceof Closeable.

We are not ignoring it. The try (Closeable ignored = closeable) bit is just try-with-resources, we need a variable so close() runs at the end of the block. We don’t reference it in the body because we already use catalog for loadView. Naming it as ignored is only to signal “this exists for cleanup, not for logic.” Without it you can leak connections on every postGetTable enrich against REST.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You're right and I'm stupid :D

Still, I think this way of writing hurts the readability: CatalogUtil.buildIcebergCatalog.
What's about calling loadAndApplyView and if the catalog is closeable, close it? It would flatten the code.

try {
  if (catalog instanceof Closeable closeable) {
    try (Closeable ignored = closeable) {
      loadAndApplyView(hmsTable, conf, catalog, catalogName, identifier);
    }
  } else {
    loadAndApplyView(hmsTable, conf, catalog, catalogName, identifier);
  }
} catch (IOException e) {
  throw new UncheckedIOException("Failed to close Iceberg catalog", e);
}
...
try {
  loadAndApplyView(hmsTable, conf, catalog, catalogName, identifier);
}
catch (IOException e) {
      throw new UncheckedIOException("Failed to close Iceberg catalog", e);
}
finally {
  if (catalog instanceof Closeable closeable) {
    catalog.close();
  }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This doesn't compile because catalog.close() method does not exist.
Also, loadAndApplyView does not throw IOException, only close() does, and that runs in finally after the catch. So that catch never handles close errors.

I agree that the original way of writing hurts readability.
Fixed by using a helper method.

PropertyUtil.propertyAsString(
metadata.properties(), HiveCatalog.HMS_TABLE_OWNER, System.getProperty("user.name"));
result.setOwner(owner);
result.setCreateTime(nowSec);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In Iceberg view, timestamp-ms in the view version stores the create time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed to get it from the Iceberg metadata instead.


private void preCreateNativeIcebergLogicalView(CreateTableRequest request) {

org.apache.hadoop.hive.metastore.api.Table hmsTable = request.getTable();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For tables, we check if the table is already existing. Why don't we need a similar check in case of views?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as #6449 (comment)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure if this code is defensive enough: even if the code currently can only run on a view it is not exists, those checks is pretty far from the execution, not even in the same module.
A future modification can easily break this check and there is no way the author of that future change will know about this execution path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree that writing defensive code is good, but there is a trade off with performance.
I didn't add a check here that the view exists because it is checked before in the execution path, although in a different module.

@kasakrisz, what is your opinion on this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The view's existence is checked here:

Table oldview = context.getDb().getTable(desc.getViewName(), false);
if (oldview != null) {

  • Both CreateViewOperation.execute and the methods in BaseHiveIcebergMetaHook run on the client side. If we want to add extra checks, I think they should be added on the server side.
  • To add extra checks to the server side, we will need to either extend the CreateTableRequest API with fields like replace and ifNotExists, or create a brand new API for creating views. This introduces an API change either way, so all types of views should be addressed. Since this isn't currently supported for native Hive views, we can handle it in a follow-up patch.
  • Performance for a CREATE VIEW statement is not a high priority because it is not a frequently executed operation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because of the hook based architecture of the Hive metadata handling, I interpret BaseHiveIcebergMetaHook as one of the entry points for the Iceberg code. To me, it is a clear module boundary.
Even if I disagree checking the existence only on a lower level of the code execution I can live with it.

Comment thread iceberg/iceberg-handler/src/test/queries/positive/iceberg_view.q
# Detailed Table Information
Database: ice_rest
#### A masked pattern was here ####
Retention: 2147483647

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it a timestamp or some remaining time? I wonder if it would worth masking out those values or not to avoid test flakyness.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

2147483647 is Integer.MAX_VALUE - the HMS Table.retention field is set in MetastoreUtil.applyIcebergViewToHmsTable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I removed setting retention to match HMS behavior which sets 0 by default.

Comment thread ql/src/java/org/apache/hadoop/hive/ql/exec/repl/ReplLoadTask.java
@InvisibleProgrammer

Copy link
Copy Markdown
Contributor

On conceptional level, I have two major questions:
Should the Rest client handle views as tables? Or views should be views? At the current implementation it mixes them and basic CRUD operations doesn't separate them. For example, listing tables can receive both tables and views. But we don't know if a given entry is a table or a view.

The other important question is the boundary between what we store on HMS and what we store on Iceberg side?
What happens if we create a native Iceberg view? Do we store the metadata only on Iceberg side? Or duplicate it between Hive and Iceberg? What if we duplicate them? How we can make sure the duplicated data is in sync? And what is the plan if it is not in sync? For example, I'm creating a native Iceberg view in Beeline. Connect to the Rest client. Modify the view. Go back to Hive and query this view. What will happen?

@difin

difin commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

@difin When compiling a query referencing an Iceberg logical views the view definition query should come via Iceberg api from the Iceberg metadata files not from the HMS backend db.

In HiveRESTCatalogClient.java I found the implementation of this logic

if (restCatalog instanceof ViewCatalog viewCatalog) {
        try {
          View icebergView = viewCatalog.loadView(id);
          return MetastoreUtil.toHiveView(icebergView, conf);
        } catch (NoSuchViewException viewMissing) {
          throw new NoSuchObjectException();
        }

Maybe I'm missing something but haven't found the implementation of this in case of HMS catalog via thrift. Could you please point it out?

Done - previously Hive was getting the view query definition that was stored in HMS, now I made it retrieve view definition from Iceberg files (via Iceberg API) in BaseHiveIcebergMetaHook.postGetTable:

  public void postGetTable(org.apache.hadoop.hive.metastore.api.Table hmsTable) {
    if (hmsTable != null) {
      try {
        if (isNativeIcebergLogicalView(hmsTable)) {
          if (!usesRestMetastoreClient(conf)) {
            IcebergNativeLogicalViewSupport.enrichHmsTableFromIcebergView(hmsTable, conf);
          }
          return;
        }

@difin

difin commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

On conceptional level, I have two major questions: Should the Rest client handle views as tables? Or views should be views? At the current implementation it mixes them and basic CRUD operations doesn't separate them. For example, listing tables can receive both tables and views. But we don't know if a given entry is a table or a view.

Same as #6449 (comment), answered there.

The other important question is the boundary between what we store on HMS and what we store on Iceberg side? What happens if we create a native Iceberg view? Do we store the metadata only on Iceberg side? Or duplicate it between Hive and Iceberg? What if we duplicate them? How we can make sure the duplicated data is in sync? And what is the plan if it is not in sync? For example, I'm creating a native Iceberg view in Beeline. Connect to the Rest client. Modify the view. Go back to Hive and query this view. What will happen?

HMS stores only a view identity; at the read time, we construct HMS in-memory view object that gets enriched from the Iceberg metadata (including View SQL text) on both HMS and REST client paths in BaseHiveIcebergMetaHook:

  @Override
  public void postGetTable(org.apache.hadoop.hive.metastore.api.Table hmsTable) {
    if (hmsTable != null) {
      try {
        if (isNativeIcebergLogicalView(hmsTable)) {
          IcebergLogicalViewSupport.enrichHmsTableFromIcebergView(hmsTable, conf);
          return;
        }

@sonarqubecloud

sonarqubecloud Bot commented Jun 9, 2026

Copy link
Copy Markdown

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.

4 participants