Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@
import org.apache.flink.table.catalog.CatalogPartition;
import org.apache.flink.table.catalog.CatalogPartitionSpec;
import org.apache.flink.table.catalog.CatalogTable;
import org.apache.flink.table.catalog.CatalogTableImpl;
import org.apache.flink.table.catalog.ObjectPath;
import org.apache.flink.table.catalog.ResolvedCatalogTable;
import org.apache.flink.table.catalog.ResolvedSchema;
import org.apache.flink.table.catalog.exceptions.CatalogException;
import org.apache.flink.table.catalog.exceptions.DatabaseAlreadyExistException;
import org.apache.flink.table.catalog.exceptions.DatabaseNotEmptyException;
Expand Down Expand Up @@ -69,14 +70,15 @@
import org.apache.iceberg.exceptions.AlreadyExistsException;
import org.apache.iceberg.exceptions.NamespaceNotEmptyException;
import org.apache.iceberg.exceptions.NoSuchNamespaceException;
import org.apache.iceberg.flink.util.FlinkCompatibilityUtil;
import org.apache.iceberg.io.CloseableIterable;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;
import org.apache.iceberg.relocated.com.google.common.collect.Maps;
import org.apache.iceberg.relocated.com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* A Flink Catalog implementation that wraps an Iceberg {@link Catalog}.
Expand All @@ -90,6 +92,8 @@
*/
public class FlinkCatalog extends AbstractCatalog {

private static final Logger LOG = LoggerFactory.getLogger(FlinkCatalog.class);

private final CatalogLoader catalogLoader;
private final Catalog icebergCatalog;
private final Namespace baseNamespace;
Expand Down Expand Up @@ -374,7 +378,7 @@ void createIcebergTable(ObjectPath tablePath, CatalogBaseTable table, boolean ig
throws CatalogException, TableAlreadyExistException {
validateFlinkTable(table);

Schema icebergSchema = FlinkSchemaUtil.convert(table.getSchema());
Schema icebergSchema = FlinkSchemaUtil.convert(((ResolvedCatalogTable) table).getResolvedSchema());

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 always safe to type cast to ResolvedCatalogTable?

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.

yes, in this interface, we always get ResolvedCatalogTable

PartitionSpec spec = toPartitionSpec(((CatalogTable) table).getPartitionKeys(), icebergSchema);

ImmutableMap.Builder<String, String> properties = ImmutableMap.builder();
Expand All @@ -387,6 +391,11 @@ void createIcebergTable(ObjectPath tablePath, CatalogBaseTable table, boolean ig
}
}

// update properties from schema
Map<String, String> updatedProperties = FlinkSchemaUtil.generateTablePropertiesFromResolvedSchema(
((ResolvedCatalogTable) table).getResolvedSchema());
properties.putAll(updatedProperties);

try {
icebergCatalog.createTable(
toIdentifier(tablePath),
Expand All @@ -401,6 +410,19 @@ void createIcebergTable(ObjectPath tablePath, CatalogBaseTable table, boolean ig
}
}

private static void validateSchemaAndPartition(CatalogTable ct1, CatalogTable ct2) {
org.apache.flink.table.api.Schema ts1 = ct1.getUnresolvedSchema();
org.apache.flink.table.api.Schema ts2 = ct2.getUnresolvedSchema();

if (!Objects.equals(ts1, ts2)) {
throw new UnsupportedOperationException("Altering schema is not supported yet.");
}

if (!ct1.getPartitionKeys().equals(ct2.getPartitionKeys())) {
throw new UnsupportedOperationException("Altering partition keys is not supported yet.");
}
}

private static void validateTableSchemaAndPartition(CatalogTable ct1, CatalogTable ct2) {
TableSchema ts1 = ct1.getSchema();
TableSchema ts2 = ct2.getSchema();
Expand Down Expand Up @@ -447,7 +469,7 @@ public void alterTable(ObjectPath tablePath, CatalogBaseTable newTable, boolean

// For current Flink Catalog API, support for adding/removing/renaming columns cannot be done by comparing
// CatalogTable instances, unless the Flink schema contains Iceberg column IDs.
validateTableSchemaAndPartition(table, (CatalogTable) newTable);
validateSchemaAndPartition(table, (CatalogTable) newTable);

Map<String, String> oldProperties = table.getOptions();
Map<String, String> setProperties = Maps.newHashMap();
Expand Down Expand Up @@ -481,22 +503,23 @@ public void alterTable(ObjectPath tablePath, CatalogBaseTable newTable, boolean
}
});

Map<String, String> newTablePropertiesMap = Maps.newHashMap(oldProperties);
newTablePropertiesMap.putAll(setProperties);

CatalogTable catalogTable = toCatalogTable(icebergTable, newTablePropertiesMap);
ResolvedSchema resolvedSchema = FlinkSchemaUtil.convertToResolvedSchema(catalogTable);
Map<String, String> updatedProperties = FlinkSchemaUtil.generateTablePropertiesFromResolvedSchema(resolvedSchema);
setProperties.forEach((k, v) -> {
if (updatedProperties.containsKey(k)) {
setProperties.put(k, updatedProperties.get(k));
}
});

commitChanges(icebergTable, setLocation, setSnapshotId, pickSnapshotId, setProperties);
}

private static void validateFlinkTable(CatalogBaseTable table) {
Preconditions.checkArgument(table instanceof CatalogTable, "The Table should be a CatalogTable.");

TableSchema schema = table.getSchema();
schema.getTableColumns().forEach(column -> {
if (!FlinkCompatibilityUtil.isPhysicalColumn(column)) {
throw new UnsupportedOperationException("Creating table with computed columns is not supported yet.");
}
});

if (!schema.getWatermarkSpecs().isEmpty()) {
throw new UnsupportedOperationException("Creating table with watermark specs is not supported yet.");
}
}

private static PartitionSpec toPartitionSpec(List<String> partitionKeys, Schema icebergSchema) {
Expand Down Expand Up @@ -562,14 +585,33 @@ private static void commitChanges(Table table, String setLocation, String setSna
}

static CatalogTable toCatalogTable(Table table) {
TableSchema schema = FlinkSchemaUtil.toSchema(table.schema());

CatalogTable catalogTable = toCatalogTable(table, table.properties());
try {
FlinkSchemaUtil.convertToResolvedSchema(catalogTable);
} catch (RuntimeException e) {
LOG.warn("ignore watermark and computed columns!", e);
Map<String, String> properties = Maps.newHashMap();
table.properties().forEach((k, v) -> {
if (!k.startsWith(FlinkSchemaUtil.COMPUTED_COLUMNS_PREFIX) &&
!k.startsWith(FlinkSchemaUtil.WATERMARK_PREFIX)) {
properties.put(k, v);
}
});
catalogTable = toCatalogTable(table, properties);
}
return catalogTable;
}

static CatalogTable toCatalogTable(Table table, Map<String, String> properties) {
List<String> partitionKeys = toPartitionKeys(table.spec(), table.schema());
org.apache.flink.table.api.Schema schema = FlinkSchemaUtil.toSchema(table.schema(), properties);

// NOTE: We can not create a IcebergCatalogTable extends CatalogTable, because Flink optimizer may use
// CatalogTableImpl to copy a new catalog table.
// Let's re-loading table from Iceberg catalog when creating source/sink operators.
// Iceberg does not have Table comment, so pass a null (Default comment value in Flink).
return new CatalogTableImpl(schema, partitionKeys, table.properties(), null);
return CatalogTable.of(schema, null, partitionKeys, table.properties());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,25 @@
package org.apache.iceberg.flink;

import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.flink.api.java.ExecutionEnvironment;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.api.TableSchema;
import org.apache.flink.table.api.internal.TableEnvironmentImpl;
import org.apache.flink.table.catalog.CatalogManager;
import org.apache.flink.table.catalog.CatalogTable;
import org.apache.flink.table.catalog.Column;
import org.apache.flink.table.catalog.ResolvedSchema;
import org.apache.flink.table.catalog.SchemaResolver;
import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.table.types.logical.RowType;
import org.apache.flink.table.types.utils.TypeConversions;
import org.apache.iceberg.Schema;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;
import org.apache.iceberg.relocated.com.google.common.collect.Maps;
import org.apache.iceberg.relocated.com.google.common.collect.Sets;
import org.apache.iceberg.types.Type;
import org.apache.iceberg.types.TypeUtil;
Expand All @@ -52,35 +63,69 @@
*/
public class FlinkSchemaUtil {

public static final String FLINK_PREFIX = "flink.";

public static final String COMPUTED_COLUMNS = "computed-column.";
public static final String COMPUTED_COLUMNS_PREFIX = FLINK_PREFIX + COMPUTED_COLUMNS;

public static final String WATERMARK = "watermark.";
public static final String WATERMARK_PREFIX = FLINK_PREFIX + WATERMARK;

private FlinkSchemaUtil() {
}

/**
* Convert the flink table schema to apache iceberg schema.
*/
public static Schema convert(TableSchema schema) {
LogicalType schemaType = schema.toRowDataType().getLogicalType();
Schema iSchema = conv(schema.toPhysicalRowDataType().getLogicalType());
return freshIdentifierFieldIds(iSchema, schema);
}

/**
* Convert the flink table resolved schema to apache iceberg schema.
*/
public static Schema convert(ResolvedSchema schema) {
Schema iSchema = conv(schema.toPhysicalRowDataType().getLogicalType());
return freshIdentifierFieldIds(iSchema, schema);
}

private static Schema conv(LogicalType schemaType) {
Preconditions.checkArgument(schemaType instanceof RowType, "Schema logical type should be RowType.");

RowType root = (RowType) schemaType;
Type converted = root.accept(new FlinkTypeToType(root));

Schema iSchema = new Schema(converted.asStructType().fields());
return freshIdentifierFieldIds(iSchema, schema);
return new Schema(converted.asStructType().fields());
}

private static Schema freshIdentifierFieldIds(Schema iSchema, TableSchema schema) {
// Locate the identifier field id list.
Set<Integer> identifierFieldIds = Sets.newHashSet();
List<String> primaryKeys = Lists.newArrayList();
if (schema.getPrimaryKey().isPresent()) {
for (String column : schema.getPrimaryKey().get().getColumns()) {
primaryKeys.addAll(schema.getPrimaryKey().get().getColumns());
}
return freshIdentifier(iSchema, primaryKeys);
}

private static Schema freshIdentifierFieldIds(Schema iSchema, ResolvedSchema schema) {
List<String> primaryKeys = Lists.newArrayList();
if (schema.getPrimaryKey().isPresent()) {
primaryKeys.addAll(schema.getPrimaryKey().get().getColumns());
}
return freshIdentifier(iSchema, primaryKeys);
}

private static Schema freshIdentifier(Schema iSchema, List<String> keyColumns) {
Set<Integer> identifierFieldIds = Sets.newHashSet();
if (!keyColumns.isEmpty()) {
for (String column : keyColumns) {
Types.NestedField field = iSchema.findField(column);
Preconditions.checkNotNull(field,
"Cannot find field ID for the primary key column %s in schema %s", column, iSchema);
identifierFieldIds.add(field.fieldId());
}
}

return new Schema(iSchema.schemaId(), iSchema.asStruct().fields(), identifierFieldIds);
}

Expand Down Expand Up @@ -172,4 +217,110 @@ public static TableSchema toSchema(Schema schema) {

return builder.build();
}


/**
* Convert a {@link Schema} to a {@link Schema}.
*
* @param schema iceberg schema to convert.
* @return Flink Schema.
*/
public static org.apache.flink.table.api.Schema toSchema(Schema schema, Map<String, String> properties) {

org.apache.flink.table.api.Schema.Builder builder = org.apache.flink.table.api.Schema.newBuilder();

// get watermark and computed columns
Map<String, String> watermarkMap = Maps.newHashMap();
Map<String, String> computedColumnsMap = Maps.newHashMap();
properties.keySet().stream()
.filter(k -> k.startsWith(FLINK_PREFIX) && properties.get(k) != null)
.forEach(k -> {
final String name = k.substring(k.lastIndexOf('.') + 1);
String expr = properties.get(k);
if (k.startsWith(WATERMARK_PREFIX)) {
watermarkMap.put(name, expr);
} else if (k.startsWith(COMPUTED_COLUMNS_PREFIX)) {
computedColumnsMap.put(name, expr);
}
});

// add physical columns.
for (RowType.RowField field : convert(schema).getFields()) {
builder.column(field.getName(), TypeConversions.fromLogicalToDataType(field.getType()));
}

// add computed columns.
computedColumnsMap.forEach(builder::columnByExpression);

// add watermarks.
watermarkMap.forEach(builder::watermark);

// add primary key.
List<String> primaryKey = getPrimaryKeyFromSchema(schema);
if (!primaryKey.isEmpty()) {
builder.primaryKey(primaryKey.toArray(new String[0]));
}

return builder.build();
}

/**
* Convert a {@link CatalogTable} to a {@link ResolvedSchema}.
*
* @param table flink unresolved schema to convert.
* @return Flink ResolvedSchema.
*/
public static ResolvedSchema convertToResolvedSchema(CatalogTable table) {
Configuration configuration = ExecutionEnvironment.getExecutionEnvironment().getConfiguration();
TableEnvironment tableEnvironment = TableEnvironment.create(configuration);
CatalogManager catalogManager = ((TableEnvironmentImpl) tableEnvironment).getCatalogManager();
SchemaResolver schemaResolver = catalogManager.getSchemaResolver();
return table.getUnresolvedSchema().resolve(schemaResolver);
}

/**
* Generate table properties for watermark and computed columns from flink resolved schema.
*
* @param schema flink resolved schema.
* @return Table properties map.
*/
public static Map<String, String> generateTablePropertiesFromResolvedSchema(ResolvedSchema schema) {
Map<String, String> properties = Maps.newHashMap();

// save watermark
schema.getWatermarkSpecs().forEach(column -> {
String name = column.getRowtimeAttribute();
properties.put(
FlinkSchemaUtil.WATERMARK_PREFIX + name,
column.getWatermarkExpression().asSerializableString());
});

// save computed columns
schema.getColumns().stream()
.filter(column -> column instanceof Column.ComputedColumn)
.forEach(tableColumn -> {
Column.ComputedColumn column = (Column.ComputedColumn) tableColumn;
String name = column.getName();
properties.put(
FlinkSchemaUtil.COMPUTED_COLUMNS_PREFIX + name,
column.getExpression().asSerializableString());
});

return properties;
}

public static List<String> getPrimaryKeyFromSchema(Schema schema) {
Set<Integer> identifierFieldIds = schema.identifierFieldIds();
if (!identifierFieldIds.isEmpty()) {
List<String> columns = Lists.newArrayListWithExpectedSize(identifierFieldIds.size());
for (Integer identifierFieldId : identifierFieldIds) {
String columnName = schema.findColumnName(identifierFieldId);
Preconditions.checkNotNull(columnName,
"Cannot find field with id %s in schema %s", identifierFieldId, schema);
columns.add(columnName);
}
return columns;
}
return Lists.newArrayList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.apache.flink.table.api.EnvironmentSettings;
import org.apache.flink.table.api.TableEnvironment;
import org.apache.flink.table.api.TableResult;
import org.apache.flink.table.api.config.TableConfigOptions;
import org.apache.flink.test.util.MiniClusterWithClientResource;
import org.apache.flink.test.util.TestBaseUtils;
import org.apache.flink.types.Row;
Expand Down Expand Up @@ -78,7 +79,9 @@ protected TableEnvironment getTableEnv() {
.build();

TableEnvironment env = TableEnvironment.create(settings);
env.getConfig().getConfiguration().set(FlinkConfigOptions.TABLE_EXEC_ICEBERG_INFER_SOURCE_PARALLELISM, false);
env.getConfig().getConfiguration()
.set(FlinkConfigOptions.TABLE_EXEC_ICEBERG_INFER_SOURCE_PARALLELISM, false)
.set(TableConfigOptions.LOCAL_TIME_ZONE, "UTC");

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.

good

tEnv = env;
}
}
Expand Down
Loading