-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Flink: Support creating table and altering table in Flink SQL #1393
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
Changes from all commits
b003b29
87f8c79
5adac5c
2d1aed6
6eb6a5c
e1dcb7f
ac06e52
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.stream.Collectors; | ||
| import org.apache.flink.table.api.TableSchema; | ||
|
|
@@ -34,6 +35,7 @@ | |
| import org.apache.flink.table.catalog.CatalogFunction; | ||
| 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.exceptions.CatalogException; | ||
|
|
@@ -49,14 +51,22 @@ | |
| import org.apache.flink.util.StringUtils; | ||
| import org.apache.hadoop.conf.Configuration; | ||
| import org.apache.iceberg.CachingCatalog; | ||
| import org.apache.iceberg.PartitionField; | ||
| import org.apache.iceberg.PartitionSpec; | ||
| import org.apache.iceberg.Schema; | ||
| import org.apache.iceberg.Table; | ||
| import org.apache.iceberg.Transaction; | ||
| import org.apache.iceberg.UpdateProperties; | ||
| import org.apache.iceberg.catalog.Catalog; | ||
| import org.apache.iceberg.catalog.Namespace; | ||
| import org.apache.iceberg.catalog.SupportsNamespaces; | ||
| import org.apache.iceberg.catalog.TableIdentifier; | ||
| import org.apache.iceberg.exceptions.AlreadyExistsException; | ||
| import org.apache.iceberg.exceptions.NamespaceNotEmptyException; | ||
| import org.apache.iceberg.exceptions.NoSuchNamespaceException; | ||
| import org.apache.iceberg.relocated.com.google.common.base.Preconditions; | ||
| 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; | ||
|
|
||
|
|
@@ -277,15 +287,14 @@ public List<String> listTables(String databaseName) throws DatabaseNotExistExcep | |
| } | ||
|
|
||
| @Override | ||
| public CatalogBaseTable getTable(ObjectPath tablePath) throws TableNotExistException, CatalogException { | ||
| try { | ||
| Table table = icebergCatalog.loadTable(toIdentifier(tablePath)); | ||
| TableSchema tableSchema = FlinkSchemaUtil.toSchema(FlinkSchemaUtil.convert(table.schema())); | ||
| public CatalogTable getTable(ObjectPath tablePath) throws TableNotExistException, CatalogException { | ||
| Table table = loadIcebergTable(tablePath); | ||
| return toCatalogTable(table); | ||
| } | ||
|
|
||
| // NOTE: We can not create a IcebergCatalogTable, 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. | ||
| return new CatalogTableImpl(tableSchema, table.properties(), null); | ||
| private Table loadIcebergTable(ObjectPath tablePath) throws TableNotExistException { | ||
| try { | ||
| return icebergCatalog.loadTable(toIdentifier(tablePath)); | ||
| } catch (org.apache.iceberg.exceptions.NoSuchTableException e) { | ||
| throw new TableNotExistException(getName(), tablePath, e); | ||
| } | ||
|
|
@@ -320,19 +329,180 @@ public void renameTable(ObjectPath tablePath, String newTableName, boolean ignor | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * TODO Add partitioning to the Flink DDL parser. | ||
| */ | ||
| @Override | ||
| public void createTable(ObjectPath tablePath, CatalogBaseTable table, boolean ignoreIfExists) | ||
| throws CatalogException { | ||
| throw new UnsupportedOperationException("Not support createTable now."); | ||
| throws CatalogException, TableAlreadyExistException { | ||
| validateFlinkTable(table); | ||
|
|
||
| Schema icebergSchema = FlinkSchemaUtil.convert(table.getSchema()); | ||
| PartitionSpec spec = toPartitionSpec(((CatalogTable) table).getPartitionKeys(), icebergSchema); | ||
|
|
||
| ImmutableMap.Builder<String, String> properties = ImmutableMap.builder(); | ||
| String location = null; | ||
| for (Map.Entry<String, String> entry : table.getOptions().entrySet()) { | ||
| if ("location".equalsIgnoreCase(entry.getKey())) { | ||
| location = entry.getValue(); | ||
| } else { | ||
| properties.put(entry.getKey(), entry.getValue()); | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| icebergCatalog.createTable( | ||
| toIdentifier(tablePath), | ||
| icebergSchema, | ||
| spec, | ||
| location, | ||
| properties.build()); | ||
| } catch (AlreadyExistsException e) { | ||
| throw new TableAlreadyExistException(getName(), tablePath, e); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void alterTable(ObjectPath tablePath, CatalogBaseTable newTable, boolean ignoreIfNotExists) | ||
| throws CatalogException { | ||
| throw new UnsupportedOperationException("Not support alterTable now."); | ||
| throws CatalogException, TableNotExistException { | ||
| validateFlinkTable(newTable); | ||
| Table icebergTable = loadIcebergTable(tablePath); | ||
| CatalogTable table = toCatalogTable(icebergTable); | ||
|
|
||
| // Currently, Flink SQL only support altering table properties. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason that we could not support adding /removing/renaming column ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No Flink DLL to add/removing/renaming column...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should also note that support for adding/removing/renaming columns cannot be done by comparing The problem is clear when you consider a simple example:
There are two ways to get the Flink schema: rename a -> x and b -> y, or drop a, drop b, add x, add y. Guessing which one was intended by the user is not okay because it would corrupt data. If the values from a are read when projecting x after a was actually dropped, then this is a serious correctness bug. Also note that there are some transformations that can't be detected. For example, drop a then add a. The result should be that all values of column a are discarded. This happens when the wrong data was written to a column but the column is still needed for newer data.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. |
||
|
|
||
| // 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. | ||
| if (!table.getSchema().equals(newTable.getSchema())) { | ||
| throw new UnsupportedOperationException("Altering schema is not supported yet."); | ||
| } | ||
|
|
||
| if (!table.getPartitionKeys().equals(((CatalogTable) newTable).getPartitionKeys())) { | ||
| throw new UnsupportedOperationException("Altering partition keys is not supported yet."); | ||
| } | ||
|
|
||
| Map<String, String> oldOptions = table.getOptions(); | ||
| Map<String, String> setProperties = Maps.newHashMap(); | ||
|
|
||
| String setLocation = null; | ||
| String setSnapshotId = null; | ||
| String pickSnapshotId = null; | ||
|
|
||
| for (Map.Entry<String, String> entry : newTable.getOptions().entrySet()) { | ||
| String key = entry.getKey(); | ||
| String value = entry.getValue(); | ||
|
|
||
| if (Objects.equals(value, oldOptions.get(key))) { | ||
| continue; | ||
| } | ||
|
|
||
| if ("location".equalsIgnoreCase(key)) { | ||
| setLocation = value; | ||
| } else if ("current-snapshot-id".equalsIgnoreCase(key)) { | ||
| setSnapshotId = value; | ||
| } else if ("cherry-pick-snapshot-id".equalsIgnoreCase(key)) { | ||
| pickSnapshotId = value; | ||
| } else { | ||
| setProperties.put(key, value); | ||
| } | ||
| } | ||
|
|
||
| oldOptions.keySet().forEach(k -> { | ||
| if (!newTable.getOptions().containsKey(k)) { | ||
| setProperties.put(k, null); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q: Does this align with the flink sql semantics ? ALTER TABLE [catalog_name.][db_name.]table_name SET (key1=val1, key2=val2, ...)For the existing key-values (in old table ) which don't appear in the new table, should we remove them from old table ? ( The document did not describe this case clearly, just for confirmation).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can take a look to tests.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the new options are already merged with old options, so for the key in old options, shouldn't it be always in new options ? Seems there's no reason to add this sentence here ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add a test for unsetting PROPERTIES.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is okay to diff the property sets like this, but it seems like it would be easier not to. Right now, Flink has to apply the changes, then this code diffs the property sets, then Iceberg will re-apply the changes. In addition, this model doesn't work for schema updates, as I noted above.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But properties updates does not have column IDs. As long as the last is the same.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
* @throws NullPointerException if the specified key or value is null
* and this map does not permit null keys or values
* @throws IllegalArgumentException if some property of the specified key
* or value prevents it from being stored in this map
*/
V put(K key, V value);
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My point is that there isn't a correctness problem so this is okay. But, this causes Flink to do much more work because it has to apply changes from SQL, then recover those changes by comparing property maps, and pass the changes to Iceberg so that Iceberg can apply the changes. It is easier to pass the changes directly to Iceberg if the Flink API can be updated to support it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got it, I think we can have a try in Flink. |
||
| } | ||
| }); | ||
|
|
||
| commitChanges(icebergTable, setLocation, setSnapshotId, pickSnapshotId, setProperties); | ||
| } | ||
|
|
||
| private static void validateFlinkTable(CatalogBaseTable table) { | ||
| Preconditions.checkArgument(table instanceof CatalogTable, "The Table should be a CatalogTable."); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a case where
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
See the java docs on the current
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is |
||
|
|
||
| TableSchema schema = table.getSchema(); | ||
| schema.getTableColumns().forEach(column -> { | ||
| if (column.isGenerated()) { | ||
| 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."); | ||
| } | ||
|
|
||
| if (schema.getPrimaryKey().isPresent()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this something we should add to Iceberg for Flink use cases? What does Flink use the primary key for?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found this which might answer your question: https://cwiki.apache.org/confluence/display/FLINK/FLIP+87%3A+Primary+key+constraints+in+Table+API In particular, here are the proposed changes: So it sounds just like an RDBMS primary key. Note however, that even in the FLIP (which is just the proposal and not necessarily the finished product), it does state that there's no planned enforcement on the PK. It's up to the user to ensure that the PK is non-null and unique. So I agree here that throwing might be the most useful option and that there's likely nothing on the iceberg side to be added to enforce this as Flink doesn't enforce it either. In an entirely streaming setting, ensuring unique keys would be rather difficult and so to me it somewhat sounds like the PK is just more metadata that could very well be in TBLPROPERTIES. But a more experienced Flink SQL user than myself might have more to say on the matter. I've never attempted to enforce a PK when using Flink SQL. Sounds like the work to do so would involve custom operators etc. TLDR: The Primary Key is just a constraint, which is currently part of Flink's Table spec but goes unenforced and is up to the user. It does not appear as though the PK info is supported in any UpsertSinks etc, though that may be discussed / planned in the future. Support in the DDL for Primary Key constraints is relatively new (Flink 1.11 / current, with support in the API coming in at Flink 1.10).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @kbendick .
I think, If iceberg supports CDC native processing in the future, we may be able to use it. |
||
| throw new UnsupportedOperationException("Creating table with primary key is not supported yet."); | ||
| } | ||
| } | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to add a TODO indicating that we flink only support
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already have todo in |
||
| private static PartitionSpec toPartitionSpec(List<String> partitionKeys, Schema icebergSchema) { | ||
| PartitionSpec.Builder builder = PartitionSpec.builderFor(icebergSchema); | ||
| partitionKeys.forEach(builder::identity); | ||
| return builder.build(); | ||
| } | ||
|
|
||
| private static List<String> toPartitionKeys(PartitionSpec spec, Schema icebergSchema) { | ||
| List<String> partitionKeys = Lists.newArrayList(); | ||
| for (PartitionField field : spec.fields()) { | ||
| if (field.transform().isIdentity()) { | ||
| partitionKeys.add(icebergSchema.findColumnName(field.sourceId())); | ||
| } else { | ||
| // Not created by Flink SQL. | ||
| // For compatibility with iceberg tables, return empty. | ||
| // TODO modify this after Flink support partition transform. | ||
| return Collections.emptyList(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All tables with any partition transform other than identity appear to be unpartitioned? Why not return all of the identity fields at least?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To me, it seems like adding all of the identity fields (but not the transformed fields) would likely be incorrect. Although returning an empty list when the table is partitioned seems like a possible correctness bug to me too. Should we consider throwing an exception in this case instead until such a time that Flink supports partition transforms?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 to likely be incorrect. All partition operations are directly delegated to specific source / sink, so Flink does not need to see partition information.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking that since operations are delegated to Iceberg, correctness is not an issue. It would be nice to show users which columns are partition columns so they can see which ones are good candidates for query predicates. I don't think this is a blocker, though.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, maybe we can expose these information in properties. |
||
| } | ||
| } | ||
| return partitionKeys; | ||
| } | ||
|
|
||
| private static void commitChanges(Table table, String setLocation, String setSnapshotId, | ||
| String pickSnapshotId, Map<String, String> setProperties) { | ||
| // don't allow setting the snapshot and picking a commit at the same time because order is ambiguous and choosing | ||
| // one order leads to different results | ||
| Preconditions.checkArgument(setSnapshotId == null || pickSnapshotId == null, | ||
| "Cannot set the current snapshot ID and cherry-pick snapshot changes"); | ||
|
|
||
| if (setSnapshotId != null) { | ||
| long newSnapshotId = Long.parseLong(setSnapshotId); | ||
| table.manageSnapshots().setCurrentSnapshot(newSnapshotId).commit(); | ||
| } | ||
|
|
||
| // if updating the table snapshot, perform that update first in case it fails | ||
| if (pickSnapshotId != null) { | ||
| long newSnapshotId = Long.parseLong(pickSnapshotId); | ||
| table.manageSnapshots().cherrypick(newSnapshotId).commit(); | ||
| } | ||
|
|
||
| Transaction transaction = table.newTransaction(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we do the operations above this point in the transaction as well? That seems reasonable to me. I'm not sure why we don't in other places.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That explains it. Thanks! |
||
|
|
||
| if (setLocation != null) { | ||
| transaction.updateLocation() | ||
| .setLocation(setLocation) | ||
| .commit(); | ||
| } | ||
|
|
||
| if (!setProperties.isEmpty()) { | ||
| UpdateProperties updateProperties = transaction.updateProperties(); | ||
| setProperties.forEach((k, v) -> { | ||
| if (v == null) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| updateProperties.remove(k); | ||
| } else { | ||
| updateProperties.set(k, v); | ||
| } | ||
| }); | ||
| updateProperties.commit(); | ||
| } | ||
|
|
||
| transaction.commitTransaction(); | ||
| } | ||
|
|
||
| static CatalogTable toCatalogTable(Table table) { | ||
| TableSchema schema = FlinkSchemaUtil.toSchema(FlinkSchemaUtil.convert(table.schema())); | ||
| List<String> partitionKeys = toPartitionKeys(table.spec(), table.schema()); | ||
|
|
||
| // 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); | ||
| } | ||
|
|
||
| // ------------------------------ Unsupported methods --------------------------------------------- | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should
locationstill be placed in the table properties or will that cause some kind of conflict / error?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is no conflict/error, but I think it is good to reduce duplicate storage, cause iceberg has saved this information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer not to duplicate it in table properties. Then we would have to worry about keeping the two in sync.