Guosmilesmile commented on code in PR #17668:
URL: https://github.com/apache/iceberg/pull/17668#discussion_r3794016610


##########
flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/source/IcebergTableSource.java:
##########
@@ -204,15 +205,19 @@ public ChangelogMode getChangelogMode() {
 
   @Override
   public ScanRuntimeProvider getScanRuntimeProvider(ScanContext 
runtimeProviderContext) {
+    // The planner reads a FLIP-314 lineage vertex from a SourceProvider but 
not from a
+    // DataStreamScanProvider (see CommonExecTableSourceScan), so expose 
IcebergSource
+    // declaratively. The legacy FlinkSource has no lineage to report and 
stays on the old path.
+    if 
(readableConfig.get(FlinkConfigOptions.TABLE_EXEC_ICEBERG_USE_FLIP27_SOURCE)) {
+      IcebergSource<RowData> source = buildFLIP27Source();
+      return SourceProvider.of(source, scanParallelism(source));
+    }

Review Comment:
   Would switching here from directly providing a datastream to providing a 
source to the plan cause any changes to the UID, operator names, or other 
factors that could potentially lead to state loss? 
   Do we need to add a separate toggle to ensure that existing jobs are not 
affected?
   



##########
flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/IcebergTableSink.java:
##########
@@ -124,49 +130,139 @@ public IcebergTableSink(
     this.useDynamicSink = true;
   }
 
-  @SuppressWarnings("deprecation")
   @Override
   public SinkRuntimeProvider getSinkRuntimeProvider(Context context) {
     Preconditions.checkState(
         !overwrite || context.isBounded(),
         "Unbounded data stream doesn't support overwrite operation.");
 
+    if (canProvideSinkV2()) {
+      IcebergSink sink = buildIcebergSink();
+      Integer parallelism = sink.writeParallelism();
+      return parallelism != null ? SinkV2Provider.of(sink, parallelism) : 
SinkV2Provider.of(sink);
+    }
+
     return (DataStreamSinkProvider)
         (providerContext, dataStream) -> {
           if (useDynamicSink) {
             return createDynamicIcebergSink(dataStream);
           }
 
-          ResolvedSchema physicalColumnsOnlySchema = null;
-          List<String> equalityColumns;
-          if (resolvedSchema != null) {
-            physicalColumnsOnlySchema =
-                ResolvedSchema.of(
-                    resolvedSchema.getColumns().stream()
-                        .filter(Column::isPhysical)
-                        .collect(Collectors.toList()));
-
-            equalityColumns =
-                physicalColumnsOnlySchema
-                    .getPrimaryKey()
-                    .map(UniqueConstraint::getColumns)
-                    .orElseGet(ImmutableList::of);
-          } else {
-            equalityColumns =
-                tableSchema
-                    .getPrimaryKey()
-                    
.map(org.apache.flink.table.legacy.api.constraints.UniqueConstraint::getColumns)
-                    .orElseGet(ImmutableList::of);
-          }
-
+          ResolvedSchema physicalColumnsOnlySchema = 
physicalColumnsOnlySchema();
+          List<String> equalityColumns = 
equalityColumns(physicalColumnsOnlySchema);
           if 
(readableConfig.get(FlinkConfigOptions.TABLE_EXEC_ICEBERG_USE_V2_SINK)) {
             return createIcebergSink(dataStream, equalityColumns, 
physicalColumnsOnlySchema);
-          } else {
-            return createLegacySink(dataStream, equalityColumns, 
physicalColumnsOnlySchema);
           }
+
+          return createLegacySink(dataStream, equalityColumns, 
physicalColumnsOnlySchema);
         };
   }
 
+  /**
+   * Whether the sink can be exposed as a {@link SinkV2Provider}, which is 
what it takes to report
+   * sink lineage: the planner reads a FLIP-314 vertex off the {@code Sink} 
object (see {@code
+   * CommonExecSink}), whereas a {@code DataStreamSinkProvider} only hands it 
a built
+   * transformation. Requires {@link IcebergSink}, the only sink that reports 
lineage.
+   *
+   * <p>Also requires {@code TABLE_EXEC_UID_GENERATION=ALWAYS}. {@link 
IcebergSink}'s custom commit
+   * topology puts explicit uids on its operators, so Flink demands one on the 
sink transformation
+   * too ({@code SinkTransformationTranslator.SinkExpander}). The planner only 
sets it under {@code
+   * ALWAYS} — under the default {@code PLAN_ONLY} only for a compiled plan, 
which a connector
+   * cannot detect — so taking this path otherwise would fail job submission 
outright.
+   */
+  private boolean canProvideSinkV2() {
+    if (useDynamicSink || 
!readableConfig.get(FlinkConfigOptions.TABLE_EXEC_ICEBERG_USE_V2_SINK)) {
+      return false;
+    }
+
+    ExecutionConfigOptions.UidGeneration uidGeneration =
+        readableConfig.get(ExecutionConfigOptions.TABLE_EXEC_UID_GENERATION);
+    if (uidGeneration != ExecutionConfigOptions.UidGeneration.ALWAYS) {

Review Comment:
   On the source side, can we hahve a separate toggle (e.g., 
`table.exec.iceberg.emit-lineage`, defaulting to `false`)?  Only when this 
toggle is enabled should we validate `UID_GENERATION` and provide a clear error 
message, rather than the other way around.
   



##########
flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -749,13 +782,16 @@ public Builder toBranch(String branch) {
       return this;
     }
 
-    IcebergSink build() {
-
-      Preconditions.checkArgument(
-          inputCreator != null,
-          "Please use forRowData() or forMapperOutputType() to initialize the 
input DataStream.");
+    /**
+     * Builds the sink without wiring it into a {@link DataStream}. Use this 
when Flink calls {@code
+     * DataStream#sinkTo} itself, as it does for a {@code SinkV2Provider}; use 
{@link #append()} to
+     * attach the sink to an input stream directly.
+     */
+    public IcebergSink build() {

Review Comment:
   Do we really need to make this public?
   



##########
flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/IcebergLineageUtil.java:
##########
@@ -0,0 +1,334 @@
+/*
+ * 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.iceberg.flink;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.flink.streaming.api.lineage.DatasetConfigFacet;
+import org.apache.flink.streaming.api.lineage.LineageDataset;
+import org.apache.flink.streaming.api.lineage.LineageDatasetFacet;
+import org.apache.iceberg.CatalogProperties;
+import org.apache.iceberg.CatalogUtil;
+import org.apache.iceberg.catalog.Catalog;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.flink.TableLoader.CatalogTableLoader;
+import org.apache.iceberg.relocated.com.google.common.base.Strings;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.rest.RESTCatalog;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Builds the FLIP-314 {@link LineageDataset} that the Iceberg source and sink 
publish, so a job's
+ * source→sink table lineage reaches a Flink {@code JobStatusChangedListener}.
+ *
+ * <p>The dataset carries Iceberg's own vocabulary only: catalog, namespace, 
table, and the
+ * catalog's {@code uri}/{@code warehouse}. Composing a vendor fully-qualified 
name — BigLake's
+ * four-part {@code $project.$catalog.$database.$table}, say — is the 
listener's job, since only it
+ * has the deployment context. Namespaces are reported verbatim for the same 
reason: Iceberg allows
+ * any depth, and flattening one to fit a vendor scheme is not this class's 
decision.
+ *
+ * <p>Coordinates live in a {@link DatasetConfigFacet} rather than in {@link 
LineageDataset#name()}
+ * because on the SQL path the Table planner wraps the dataset in {@code 
TableLineageDatasetImpl},
+ * which overwrites {@code name()} with the Flink object identifier. {@code 
namespace()} and {@code
+ * facets()} survive.
+ *
+ * <p>Lineage is best-effort observability: every path here yields no dataset 
rather than throwing,
+ * so a table whose coordinates cannot be resolved never fails the job.
+ */
+public class IcebergLineageUtil {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(IcebergLineageUtil.class);
+
+  /** Facet key under which the table's coordinates are published. */
+  static final String FACET_NAME = "iceberg";
+
+  /** Dataset namespace when the catalog declares neither a {@code uri} nor a 
{@code warehouse}. */
+  static final String DEFAULT_NAMESPACE = "iceberg";
+
+  // Facet keys. CONFIG_CATALOG is the Flink CREATE CATALOG alias, which is 
arbitrary and local to
+  // the job; CONFIG_CATALOG_PREFIX is the identity the REST server itself 
assigned, which is not.
+  static final String CONFIG_CATALOG = "catalog";
+  static final String CONFIG_CATALOG_PREFIX = "catalog.prefix";
+  static final String CONFIG_CATALOG_URI = "catalog.uri";
+  static final String CONFIG_CATALOG_WAREHOUSE = "catalog.warehouse";
+  static final String CONFIG_NAMESPACE = "namespace";
+  static final String CONFIG_TABLE = "table";
+
+  /**
+   * REST config key holding the catalog handle the server resolved for this 
client. Mirrors the
+   * private {@code org.apache.iceberg.rest.ResourcePaths#PREFIX}; a client 
never sends it, so its
+   * presence means the value came from the server's {@code GET /v1/config} 
response.
+   */
+  private static final String REST_PREFIX = "prefix";
+
+  /**
+   * What {@link #restPrefixOf} returns when a live catalog was consulted and 
has no prefix to give,
+   * as distinct from null, which means no catalog could be consulted at all. 
Only the second is
+   * worth a retry: an answer of "there is no prefix" is still an answer, and 
asking again would
+   * cost a catalog initialization on every submission for the life of the 
deployment.
+   */
+  public static final String NO_REST_PREFIX = "";
+
+  private IcebergLineageUtil() {}
+
+  /**
+   * The lineage datasets for the Iceberg table addressed by {@code 
tableLoader}, shaped for {@code
+   * LineageVertex#datasets()}: one dataset, or none when the table cannot be 
described — for
+   * example a path-based {@code HadoopTableLoader}, which has no catalog.
+   *
+   * @param tableLoader the loader the source or sink was built with
+   * @param fullTableName {@code Table.name()}, i.e. {@code 
catalog.namespace.table}
+   */
+  public static List<LineageDataset> datasetsOf(TableLoader tableLoader, 
String fullTableName) {
+    return datasetsOf(tableLoader, fullTableName, null);
+  }
+
+  /**
+   * As {@link #datasetsOf(TableLoader, String)}, but using a {@code 
restPrefix} the caller already
+   * captured with {@link #restPrefixOf} from a catalog it had open for its 
own reasons.
+   *
+   * <p>This is the overload sources and sinks should use. The prefix is the 
one coordinate that
+   * only a live catalog knows, so resolving it here would cost a catalog 
initialization — on the
+   * job-submission path, per table, and more than once per table, since Flink 
asks a connector for
+   * its lineage vertex both when it extracts the dataset and when it 
constructs the transformation.
+   * Passing a captured prefix makes reporting lineage free.
+   *
+   * @param restPrefix a prefix captured from a live catalog, {@link 
#NO_REST_PREFIX} if that
+   *     catalog had none, or null to resolve it by opening a catalog
+   */
+  public static List<LineageDataset> datasetsOf(
+      TableLoader tableLoader, String fullTableName, String restPrefix) {
+    try {
+      LineageDataset dataset = describe(tableLoader, fullTableName, 
restPrefix);
+      return dataset == null ? ImmutableList.of() : ImmutableList.of(dataset);
+    } catch (Exception e) {
+      LOG.warn("Could not resolve Iceberg lineage for {}; continuing without 
it", fullTableName, e);
+      return ImmutableList.of();
+    }
+  }
+
+  /**
+   * The REST {@code prefix} carried by {@code tableLoader}'s open catalog; 
{@link #NO_REST_PREFIX}
+   * if that catalog answered but has no prefix to give; null if no catalog 
could be consulted,
+   * because the loader is not catalog-backed or is not open.
+   *
+   * <p>Costs nothing — it reads a property off a live catalog rather than 
opening one. Call it
+   * while a loader opened for some other purpose is still open, and hand the 
result to {@link
+   * #datasetsOf(TableLoader, String, String)}.
+   */
+  public static String restPrefixOf(TableLoader tableLoader) {
+    try {
+      if (!(tableLoader instanceof CatalogTableLoader) || 
!tableLoader.isOpen()) {
+        return null;
+      }
+
+      Catalog catalog = ((CatalogTableLoader) tableLoader).catalog();
+      if (!(catalog instanceof RESTCatalog)) {
+        // A live catalog that is not REST has no prefix, and no second look 
will produce one.
+        return NO_REST_PREFIX;
+      }
+
+      String prefix = ((RESTCatalog) catalog).properties().get(REST_PREFIX);
+      return Strings.isNullOrEmpty(prefix) ? NO_REST_PREFIX : prefix;
+    } catch (Exception e) {
+      LOG.debug("Could not read the REST catalog prefix from the open 
catalog", e);
+      return null;
+    }
+  }
+
+  /** The dataset describing {@code tableLoader}'s table, or null if it cannot 
be described. */
+  private static LineageDataset describe(
+      TableLoader tableLoader, String fullTableName, String restPrefix) {
+    if (!(tableLoader instanceof CatalogTableLoader)) {
+      LOG.debug("Skipping lineage for {}: not a catalog-backed table", 
fullTableName);
+      return null;
+    }
+
+    if (Strings.isNullOrEmpty(fullTableName)) {
+      LOG.debug("Skipping lineage: no table name available");
+      return null;
+    }
+
+    CatalogTableLoader loader = (CatalogTableLoader) tableLoader;
+    CatalogLoader catalogLoader = loader.catalogLoader();
+    TableIdentifier identifier = loader.tableIdentifier();
+    Map<String, String> catalogProperties = catalogLoader.properties();
+
+    // Only these keys are copied: catalog properties routinely carry 
credentials, and the facet is
+    // forwarded off-cluster.
+    ImmutableMap.Builder<String, String> config = ImmutableMap.builder();
+    putIfPresent(config, CONFIG_CATALOG, catalogAlias(fullTableName, 
identifier));
+    putIfPresent(
+        config,
+        CONFIG_CATALOG_PREFIX,
+        restPrefix != null ? restPrefix : loadRestPrefix(catalogLoader, 
catalogProperties));
+    putIfPresent(config, CONFIG_CATALOG_URI, 
catalogProperties.get(CatalogProperties.URI));

Review Comment:
   If the issue is that the URI itself can potentially carry credentials or 
secrets:
   
   * A JdbcCatalog URI is a JDBC URL, where 
jdbc:postgresql://h/db?user=u&password=p is a standard format.
   * REST/Nessie URIs may also contain query parameters such as ?token=.
   * There is also the scheme://user:pass@host format, where credentials are 
included in the userinfo.
   
   Could this pose a security risk or lead to information leakage? Are these 
URIs or their contents going to be sent to external systems?



##########
flink/v2.1/flink/src/main/java/org/apache/iceberg/flink/sink/IcebergSink.java:
##########
@@ -1026,10 +1082,15 @@ private DataStream<RowData> 
distributeDataStreamByHashDistributionMode(
     }
   }
 
+  /** The configured {@code write-parallelism}, or null to follow the input's 
parallelism. */
+  public Integer writeParallelism() {
+    return writeParallelism;
+  }

Review Comment:
   Could you help me understand why we need to expose a new setting for the 
write parallelism?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to