This is an automated email from the ASF dual-hosted git repository.

Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new ac15da94a24 [fix](fe) Load Iceberg views without engine-name metadata 
(#69829)
ac15da94a24 is described below

commit ac15da94a247275b337f68a803a7354f0d7ce8f4
Author: daidai <[email protected]>
AuthorDate: Mon Nov 2 12:10:35 2026 +0800

    [fix](fe) Load Iceberg views without engine-name metadata (#69829)
    
    ### What problem does this PR solve?
    
    Allow missing/empty `engine-name` in Iceberg views. Preserve existing
    engine-based SQL selection and only use the Doris/SDK fallback when the
    field has no value.
    
    ### Release note
    
    Support Iceberg views without `engine-name` while retaining existing SQL
    selection when it is present.
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [x] Unit Test
        - [x] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason
    
    - Behavior changed:
        - [ ] No.
        - [x] Yes.
    
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../iceberg/IcebergConnectorMetadata.java          |  27 ++---
 .../iceberg/IcebergConnectorMetadataTest.java      | 128 ++++++++++++++++++---
 2 files changed, 121 insertions(+), 34 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
index b571662d2b0..006f3b6773a 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
@@ -384,15 +384,8 @@ public class IcebergConnectorMetadata implements 
ConnectorMetadata {
 
     @Override
     public ConnectorViewDefinition getViewDefinition(ConnectorSession session, 
String dbName, String viewName) {
-        // Mirror viewExists: wrap the remote load in the auth context and 
normalize EVERY failure into a
-        // RuntimeException (the seam's loadView already fails loud on a 
non-view catalog with a
-        // DorisConnectorException, which is a RuntimeException and surfaces 
wrapped here). ONE remote load
-        // yields both the sql/dialect (mirroring legacy 
IcebergExternalTable.getViewText + getSqlDialect: the
-        // dialect is the view-version summary's "engine-name", the SQL is 
that dialect's representation) AND
-        // the columns (parseSchema(view.schema()), mirroring legacy 
IcebergUtils.loadViewSchemaCacheValue — a
-        // view has NO partition columns and NO row-lineage). The 
sql/dialect/column extraction lives HERE,
-        // not in the SDK-only seam, because parseSchema reads the 
enable.mapping.* flags that only exist in
-        // this layer's properties (mirrors the table path: seam loadTable -> 
metadata buildTableSchema).
+        // Resolve SQL and columns in one authenticated view load. Column 
conversion belongs here because
+        // it uses this catalog's type-mapping flags. A view has no partition 
or row-lineage columns.
         try {
             return executeAuthenticated(() -> {
                 View icebergView = catalogOps.loadView(dbName, viewName);
@@ -405,17 +398,17 @@ public class IcebergConnectorMetadata implements 
ConnectorMetadata {
                 if (summary == null) {
                     throw new DorisConnectorException(String.format("Cannot 
get summary for view '%s'", icebergView));
                 }
-                // "engine-name" is the iceberg view-version summary key the 
writing engine (e.g. spark) records.
                 String engineName = summary.get("engine-name");
-                if (engineName == null || engineName.isEmpty()) {
-                    throw new DorisConnectorException(
-                            String.format("Cannot get engine-name for view 
'%s'", icebergView));
-                }
-                String dialect = engineName.toLowerCase(Locale.ROOT);
-                SQLViewRepresentation sqlViewRepresentation = 
icebergView.sqlFor(dialect);
+                // Preserve engine-based SQL selection for existing views. 
Only a missing/empty engine-name
+                // uses Doris as the preference; Iceberg falls back to the 
first SQL if it has no match.
+                String preferredDialect = engineName == null || 
engineName.isEmpty()
+                        ? "doris" : engineName.toLowerCase(Locale.ROOT);
+                SQLViewRepresentation sqlViewRepresentation = 
icebergView.sqlFor(preferredDialect);
                 if (sqlViewRepresentation == null) {
-                    throw new DorisConnectorException("Cannot get view text 
from iceberg view");
+                    throw new DorisConnectorException(String.format(
+                            "Cannot get SQL representation for Iceberg view 
'%s.%s'", dbName, viewName));
                 }
+                String dialect = 
sqlViewRepresentation.dialect().toLowerCase(Locale.ROOT);
                 List<ConnectorColumn> columns = 
parseSchema(icebergView.schema());
                 return new 
ConnectorViewDefinition(sqlViewRepresentation.sql(), dialect, columns);
             });
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
index 877ecfb65e1..ccce63017af 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataTest.java
@@ -41,8 +41,12 @@ import org.apache.iceberg.exceptions.NoSuchTableException;
 import org.apache.iceberg.exceptions.NotFoundException;
 import org.apache.iceberg.inmemory.InMemoryCatalog;
 import org.apache.iceberg.types.Types;
+import org.apache.iceberg.view.BaseView;
 import org.apache.iceberg.view.ImmutableSQLViewRepresentation;
 import org.apache.iceberg.view.ImmutableViewVersion;
+import org.apache.iceberg.view.View;
+import org.apache.iceberg.view.ViewMetadata;
+import org.apache.iceberg.view.ViewOperations;
 import org.apache.iceberg.view.ViewVersion;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -144,6 +148,29 @@ public class IcebergConnectorMetadataTest {
         return builder.build();
     }
 
+    private static View viewWithMetadata(ViewVersion version, Schema schema) {
+        ViewMetadata metadata = ViewMetadata.builder()
+                .setLocation("s3://warehouse/db1/v1")
+                .setCurrentVersion(version, schema)
+                .build();
+        return new BaseView(new ViewOperations() {
+            @Override
+            public ViewMetadata current() {
+                return metadata;
+            }
+
+            @Override
+            public ViewMetadata refresh() {
+                return metadata;
+            }
+
+            @Override
+            public void commit(ViewMetadata base, ViewMetadata updated) {
+                throw new UnsupportedOperationException();
+            }
+        }, "db1.v1");
+    }
+
     // ---------------------------------------------------------------------
     // write capabilities (row-level DML dispatch)
     // ---------------------------------------------------------------------
@@ -612,15 +639,14 @@ public class IcebergConnectorMetadataTest {
         Schema viewSchema = new Schema(
                 Types.NestedField.required(1, "id", Types.IntegerType.get()),
                 Types.NestedField.optional(2, "name", Types.StringType.get()));
-        ops.view = new FakeIcebergViewCatalog.StubView(
+        ops.view = viewWithMetadata(
                 viewVersionWith("Spark", "spark", "SELECT 1"), viewSchema);
 
         ConnectorViewDefinition def = 
metadataWith(ops).getViewDefinition(null, "db1", "v1");
 
         // WHY (H8): ONE loadView yields the sql/dialect (BindRelation / SHOW 
CREATE) AND the column schema
-        // (DESC / SHOW COLUMNS / information_schema.columns). The dialect is 
the summary engine-name LOWERCASED;
-        // the sql is that dialect's representation; the columns are 
parseSchema(view.schema()). MUTATION:
-        // dropping toLowerCase / wrong representation / not parsing the view 
schema (empty columns) -> red.
+        // (DESC / SHOW COLUMNS / information_schema.columns). SQL and dialect 
come from the selected
+        // representation, while columns come from parseSchema(view.schema()).
         Assertions.assertEquals("SELECT 1", def.getSql());
         Assertions.assertEquals("spark", def.getDialect());
         List<ConnectorColumn> cols = def.getColumns();
@@ -644,7 +670,7 @@ public class IcebergConnectorMetadataTest {
         Schema viewSchema = new Schema(
                 Types.NestedField.optional(1, "b", Types.BinaryType.get()),
                 Types.NestedField.optional(2, "ts_tz", 
Types.TimestampType.withZone()));
-        ops.view = new FakeIcebergViewCatalog.StubView(
+        ops.view = viewWithMetadata(
                 viewVersionWith("spark", "spark", "SELECT 1"), viewSchema);
         Map<String, String> props = new HashMap<>();
         props.put("enable.mapping.varbinary", "true");
@@ -690,26 +716,94 @@ public class IcebergConnectorMetadataTest {
     }
 
     @Test
-    public void getViewDefinitionThrowsWhenEngineNameMissing() {
-        // WHY (moved from the seam test post-H8): the dialect IS the summary 
engine-name; without it the SQL
-        // representation cannot be selected. MUTATION: dropping the 
empty-engine-name check -> wrong behavior.
+    public void getViewDefinitionDoesNotRequireEngineName() {
+        for (String engineName : Arrays.asList(null, "", "Polaris")) {
+            RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+            ops.view = viewWithMetadata(viewVersionWith(engineName, "SpArK", 
"SELECT 1"), idNameSchema());
+            ops.throwNoSuchTableOnLoadTable = true;
+
+            ConnectorViewDefinition definition = 
metadataWith(ops).getViewDefinition(null, "db1", "v1");
+
+            Assertions.assertEquals("SELECT 1", definition.getSql());
+            Assertions.assertEquals("spark", definition.getDialect());
+            Assertions.assertEquals(2, definition.getColumns().size());
+            
Assertions.assertEquals(Collections.singletonList("loadView:db1.v1"), ops.log);
+        }
+    }
+
+    @Test
+    public void getViewDefinitionPrefersDorisWhenEngineNameIsMissing() {
         RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
-        ops.view = new FakeIcebergViewCatalog.StubView(
-                viewVersionWith(null, "spark", "SELECT 1"), idNameSchema());
-        Assertions.assertThrows(RuntimeException.class,
-                () -> metadataWith(ops).getViewDefinition(null, "db1", "v1"));
+        ViewVersion version = ImmutableViewVersion.builder()
+                .from(viewVersionWith(null, "spark", "SELECT 1 AS 
spark_value"))
+                .addRepresentations(ImmutableSQLViewRepresentation.builder()
+                        .sql("SELECT 1 AS 
doris_value").dialect("DoRiS").build())
+                .build();
+        ops.view = viewWithMetadata(version, idNameSchema());
+
+        ConnectorViewDefinition definition = 
metadataWith(ops).getViewDefinition(null, "db1", "v1");
+
+        Assertions.assertEquals("SELECT 1 AS doris_value", 
definition.getSql());
+        Assertions.assertEquals("doris", definition.getDialect());
     }
 
     @Test
-    public void getViewDefinitionThrowsWhenNoSqlForDialect() {
-        // WHY (moved from the seam test post-H8): engine-name present but no 
SQL representation for that dialect
-        // -> sqlFor returns null -> must fail loud (mirrors legacy "Cannot 
get view text"). MUTATION: dropping
-        // the null-sql check -> NPE.
+    public void getViewDefinitionKeepsEngineDialectWhenDorisIsAbsent() {
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ViewVersion version = ImmutableViewVersion.builder()
+                .from(viewVersionWith("Trino", "spark", "SELECT 1 AS 
spark_value"))
+                .addRepresentations(ImmutableSQLViewRepresentation.builder()
+                        .sql("SELECT 1 AS 
trino_value").dialect("trino").build())
+                .build();
+        ops.view = viewWithMetadata(version, idNameSchema());
+
+        ConnectorViewDefinition definition = 
metadataWith(ops).getViewDefinition(null, "db1", "v1");
+
+        Assertions.assertEquals("SELECT 1 AS trino_value", 
definition.getSql());
+        Assertions.assertEquals("trino", definition.getDialect());
+    }
+
+    @Test
+    public void getViewDefinitionKeepsEngineDialectWhenDorisIsAvailable() {
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ViewVersion version = ImmutableViewVersion.builder()
+                .from(viewVersionWith("Spark", "spark", "SELECT 1 AS 
spark_value"))
+                .addRepresentations(ImmutableSQLViewRepresentation.builder()
+                        .sql("SELECT 1 AS 
doris_value").dialect("doris").build())
+                .build();
+        ops.view = viewWithMetadata(version, idNameSchema());
+
+        ConnectorViewDefinition definition = 
metadataWith(ops).getViewDefinition(null, "db1", "v1");
+
+        Assertions.assertEquals("SELECT 1 AS spark_value", 
definition.getSql());
+        Assertions.assertEquals("spark", definition.getDialect());
+    }
+
+    @Test
+    public void getViewDefinitionUsesFirstSqlWhenEngineNameAndDorisAreAbsent() 
{
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ViewVersion version = ImmutableViewVersion.builder()
+                .from(viewVersionWith(null, "spark", "SELECT 1 AS 
spark_value"))
+                .addRepresentations(ImmutableSQLViewRepresentation.builder()
+                        .sql("SELECT 1 AS 
trino_value").dialect("trino").build())
+                .build();
+        ops.view = viewWithMetadata(version, idNameSchema());
+
+        ConnectorViewDefinition definition = 
metadataWith(ops).getViewDefinition(null, "db1", "v1");
+
+        Assertions.assertEquals("SELECT 1 AS spark_value", 
definition.getSql());
+        Assertions.assertEquals("spark", definition.getDialect());
+    }
+
+    @Test
+    public void getViewDefinitionThrowsWhenNoSqlRepresentation() {
         RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
         ops.view = new FakeIcebergViewCatalog.StubView(
                 viewVersionWith("spark", null, null), idNameSchema());
-        Assertions.assertThrows(RuntimeException.class,
+        RuntimeException exception = 
Assertions.assertThrows(RuntimeException.class,
                 () -> metadataWith(ops).getViewDefinition(null, "db1", "v1"));
+        Assertions.assertTrue(exception.getMessage().contains(
+                "Cannot get SQL representation for Iceberg view 'db1.v1'"), 
exception.getMessage());
     }
 
     // ---------------------------------------------------------------------


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to