Skip to content

Commit

Permalink
[Kernel] Unify table feature logic into structred APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
vkorukanti committed Feb 14, 2025
1 parent 5aee635 commit 963686e
Show file tree
Hide file tree
Showing 7 changed files with 1,065 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ public static KernelException invalidVersionRange(long startVersion, long endVer
}

/* ------------------------ PROTOCOL EXCEPTIONS ----------------------------- */
public static KernelException unsupportedTableFeature(String feature) {
String message =
String.format(
"Unsupported Delta table feature: table requires feature \"%s\" "
+ "which is unsupported by this version of Delta Kernel.",
feature);
return new KernelException(message);
}

public static KernelException unsupportedReaderProtocol(
String tablePath, int tableReaderVersion) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,77 @@ public class TableConfig<T> {
// TableConfigs //
//////////////////

/**
* Whether this Delta table is append-only. Files can't be deleted, or values can't be updated.
*/
public static final TableConfig<Boolean> APPEND_ONLY_ENABLED =
new TableConfig<>(
"delta.appendOnly",
"false",
Boolean::valueOf,
value -> true,
"needs to be a boolean.",
true);

/**
* Enable change data feed output. When enabled, DELETE, UPDATE, and MERGE INTO operations will
* need to do additional work to output their change data in an efficiently readable format.
*/
public static final TableConfig<Boolean> CHANGE_DATA_FEED_ENABLED =
new TableConfig<>(
"delta.enableChangeDataFeed",
"false",
Boolean::valueOf,
value -> true,
"needs to be a boolean.",
true);

public static final TableConfig<String> CHECKPOINT_POLICY =
new TableConfig<>(
"delta.checkpointPolicy",
"classic",
v -> v,
value -> value.equals("classic") || value.equals("v2"),
"needs to be a string and one of 'classic' or 'v2'.",
true);

/** Whether commands modifying this Delta table are allowed to create new deletion vectors. */
public static final TableConfig<Boolean> DELETION_VECTORS_CREATION_ENABLED =
new TableConfig<>(
"delta.enableDeletionVectors",
"false",
Boolean::valueOf,
value -> true,
"needs to be a boolean.",
true);

/**
* Whether widening the type of an existing column or field is allowed, either manually using
* ALTER TABLE CHANGE COLUMN or automatically if automatic schema evolution is enabled.
*/
public static final TableConfig<Boolean> TYPE_WIDENING_ENABLED =
new TableConfig<>(
"delta.enableTypeWidening",
"false",
Boolean::valueOf,
value -> true,
"needs to be a boolean.",
true);

/**
* Indicates whether Row Tracking is enabled on the table. When this flag is turned on, all rows
* are guaranteed to have Row IDs and Row Commit Versions assigned to them, and writers are
* expected to preserve them by materializing them to hidden columns in the data files.
*/
public static final TableConfig<Boolean> ROW_TRACKING_ENABLED =
new TableConfig<>(
"delta.enableRowTracking",
"false",
Boolean::valueOf,
value -> true,
"needs to be a boolean.",
true);

/**
* The shortest duration we have to keep logically deleted data files around before deleting them
* physically.
Expand Down Expand Up @@ -175,6 +246,17 @@ public class TableConfig<T> {
Collections.unmodifiableMap(
new HashMap<String, TableConfig<?>>() {
{
addConfig(this, APPEND_ONLY_ENABLED);
addConfig(this, CHANGE_DATA_FEED_ENABLED);
addConfig(this, CHECKPOINT_POLICY);
addConfig(this, DELETION_VECTORS_CREATION_ENABLED);
addConfig(this, TYPE_WIDENING_ENABLED);
addConfig(this, ROW_TRACKING_ENABLED);
addConfig(this, LOG_RETENTION);
addConfig(this, EXPIRED_LOG_CLEANUP_ENABLED);
addConfig(this, TOMBSTONE_RETENTION);
addConfig(this, CHECKPOINT_INTERVAL);
addConfig(this, IN_COMMIT_TIMESTAMPS_ENABLED);
addConfig(this, TOMBSTONE_RETENTION);
addConfig(this, CHECKPOINT_INTERVAL);
addConfig(this, IN_COMMIT_TIMESTAMPS_ENABLED);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (2024) The Delta Lake Project Authors.
*
* Licensed 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 io.delta.kernel.internal.tablefeatures;

import io.delta.kernel.internal.actions.Metadata;
import io.delta.kernel.internal.actions.Protocol;

/**
* Defines behavior for {@link TableFeature} that can be automatically enabled via a change in a
* table's metadata, e.g., through setting particular values of certain feature-specific table
* properties. When the requirements are satisfied, the feature is automatically enabled.
*/
public interface FeatureAutoEnabledByMetadata {
/**
* Determine whether the feature must be supported and enabled because its metadata requirements
* are satisfied.
*
* @param protocol the protocol of the table for features that are already enabled.
* @param metadata the metadata of the table for properties that can enable the feature.
*/
boolean metadataRequiresFeatureToBeEnabled(Protocol protocol, Metadata metadata);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*
* Copyright (2024) The Delta Lake Project Authors.
*
* Licensed 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 io.delta.kernel.internal.tablefeatures;

import static io.delta.kernel.internal.util.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

import io.delta.kernel.internal.actions.Metadata;
import java.util.Collections;
import java.util.Set;

/**
* Base class for table features.
*
* <p>A feature can be <b>explicitly supported</b> by a table's protocol when the protocol contains
* a feature's `name`. Writers (for writer-only features) or readers and writers (for reader-writer
* features) must recognize supported features and must handle them appropriately.
*
* <p>A table feature that released before Delta Table Features (reader version 3 and writer version
* 7) is considered as a <strong>legacy feature</strong>. Legacy features are <strong> implicitly
* supported</strong> when (a) the protocol does not support table features, i.e., has reader
* version less than 3 or writer version less than 7 and (b) the feature's minimum reader/writer
* version is less than or equal to the current protocol's reader/writer version.
*
* <p>Separately, a feature can be automatically supported by a table's metadata when certain
* feature-specific table properties are set. For example, `changeDataFeed` is automatically
* supported when there's a table property `delta.enableChangeDataFeed=true`. See {@link
* FeatureAutoEnabledByMetadata} for details on how to define such features. This is independent of
* the table's enabled features. When a feature is supported (explicitly or implicitly) by the table
* protocol but its metadata requirements are not satisfied, then clients still have to understand
* the feature (at least to the extent that they can read and preserve the existing data in the
* table that uses the feature).
*/
public abstract class TableFeature {

/////////////////////////////////////////////////////////////////////////////////
/// Instance variables. ///
/////////////////////////////////////////////////////////////////////////////////
private final String featureName;
private final int minReaderVersion;
private final int minWriterVersion;

/////////////////////////////////////////////////////////////////////////////////
/// Public methods. ///
/////////////////////////////////////////////////////////////////////////////////
/**
* Constructor. Does validations to make sure:
*
* <ul>
* <li>Feature name is not null or empty and has valid characters
* <li>minReaderVersion is always 0 for writer features
* <li>all legacy features can be auto applied based on the metadata and protocol
*
* @param featureName a globally-unique string indicator to represent the feature. All characters
* must be letters (a-z, A-Z), digits (0-9), '-', or '_'. Words must be in camelCase.
* @param minReaderVersion the minimum reader version this feature requires. For a feature that
* can only be explicitly supported, this is either `0` (i.e writerOnly feature) or `3` (the
* reader protocol version that supports table features), depending on the feature is
* writer-only or reader-writer. For a legacy feature that can be implicitly supported, this
* is the first protocol version which the feature is introduced.
* @param minWriterVersion the minimum writer version this feature requires. For a feature that
* can only be explicitly supported, this is the writer protocol `7` that supports table
* features. For a legacy feature that can be implicitly supported, this is the first protocol
* version which the feature is introduced.
*/
public TableFeature(String featureName, int minReaderVersion, int minWriterVersion) {
this.featureName = requireNonNull(featureName, "name is null");
checkArgument(!featureName.isEmpty(), "name is empty");
checkArgument(
featureName.chars().allMatch(c -> Character.isLetterOrDigit(c) || c == '-' || c == '_'),
"name contains invalid characters: " + featureName);
checkArgument(minReaderVersion >= 0, "minReaderVersion is negative");
checkArgument(minWriterVersion >= 1, "minWriterVersion is less than 1");
this.minReaderVersion = minReaderVersion;
this.minWriterVersion = minWriterVersion;

validate();
}

/** @return the name of the table feature. */
public String featureName() {
return featureName;
}

/**
* @return true if this feature is applicable to both reader and writer, false if it is
* writer-only.
*/
public boolean isReaderWriterFeature() {
return this instanceof ReaderWriterFeatureType;
}

/** @return the minimum reader version this feature requires */
public int minReaderVersion() {
return minReaderVersion;
}

/** @return the minimum writer version that this feature requires. */
public int minWriterVersion() {
return minWriterVersion;
}

/** @return if this feature is a legacy feature? */
public boolean isLegacyFeature() {
return this instanceof LegacyFeatureType;
}

/**
* Set of table features that this table feature depends on. I.e. the set of features that need to
* be enabled if this table feature is enabled.
*
* @return the set of table features that this table feature depends on.
*/
public Set<TableFeature> requiredFeatures() {
return Collections.emptySet();
}

/**
* Does Kernel has support to read a table containing this feature? Default implementation returns
* true. Features should override this method if they have special requirements or not supported
* by the Kernel yet.
*
* @return true if Kernel has support to read a table containing this feature.
*/
public boolean hasKernelReadSupport() {
checkArgument(isReaderWriterFeature(), "Should be called only for reader-writer features");
return true;
}

/**
* Does Kernel has support to write a table containing this feature? Default implementation
* returns true. Features should override this method if they have special requirements or not
* supported by the Kernel yet.
*
* @param metadata the metadata of the table. Sometimes checking the metadata is necessary to know
* the Kernel can write the table or not.
* @return true if Kernel has support to write a table containing this feature.
*/
public boolean hasKernelWriteSupport(Metadata metadata) {
return true;
}

/////////////////////////////////////////////////////////////////////////////////
/// Define the {@link TableFeature}s traits that define behavior/attributes. ///
/////////////////////////////////////////////////////////////////////////////////
/**
* An interface to indicate a feature is legacy, i.e., released before Table Features. All legacy
* features are auto enabled by metadata.
*/
public interface LegacyFeatureType extends FeatureAutoEnabledByMetadata {}

/** An interface to indicate a feature applies to readers and writers. */
public interface ReaderWriterFeatureType {}

/////////////////////////////////////////////////////////////////////////////////
/// Base classes for each of the feature category. ///
/////////////////////////////////////////////////////////////////////////////////
/** A base class for all table legacy writer-only features. */
public abstract static class LegacyWriterFeature extends TableFeature
implements LegacyFeatureType {
public LegacyWriterFeature(String featureName, int minWriterVersion) {
super(featureName, /* minReaderVersion = */ 0, minWriterVersion);
}

@Override
public boolean hasKernelReadSupport() {
return true;
}
}

/** A base class for all table legacy reader-writer features. */
public abstract static class LegacyReaderWriterFeature extends TableFeature
implements LegacyFeatureType, ReaderWriterFeatureType {
public LegacyReaderWriterFeature(
String featureName, int minReaderVersion, int minWriterVersion) {
super(featureName, minReaderVersion, minWriterVersion);
}
}

/** A base class for all non-legacy table writer features. */
public abstract static class WriterFeature extends TableFeature {
public WriterFeature(String featureName, int minWriterVersion) {
super(featureName, /* minReaderVersion = */ 0, minWriterVersion);
}

@Override
public boolean hasKernelReadSupport() {
return true;
}
}

/** A base class for all non-legacy table reader-writer features. */
public abstract static class ReaderWriterFeature extends TableFeature
implements ReaderWriterFeatureType {
public ReaderWriterFeature(String featureName, int minReaderVersion, int minWriterVersion) {
super(featureName, minReaderVersion, minWriterVersion);
}
}

/**
* Validate the table feature. This method should throw an exception if the table feature
* properties are invalid. Should be called after the object deriving the {@link TableFeature} is
* constructed.
*/
private void validate() {
if (!isReaderWriterFeature()) {
checkArgument(minReaderVersion() == 0, "Writer-only feature must have minReaderVersion=0");
}
}

// Important note: uses the default implementation of `equals` and `hashCode` methods.
// We expect that the feature instances are singletons, so we don't need to compare the fields.
}
Loading

0 comments on commit 963686e

Please sign in to comment.