Skip to content

Rename SUPPORTED_CATALOG_CONNECTION_TYPES #1959

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

Closed
Closed
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti
### Changes

### Deprecations
- Deprecates the `SUPPORTED_CATALOG_CONNECTION_TYPES` configuration property and replaces it with `SUPPORTED_EXTERNAL_CATALOG_CONNECTION_TYPES`.

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.apache.polaris.core.config;

import java.util.Optional;
import java.util.Set;

/**
* Internal configuration flags for non-feature behavior changes in Polaris. These flags control
Expand All @@ -34,11 +35,12 @@ public class BehaviorChangeConfiguration<T> extends PolarisConfiguration<T> {

protected BehaviorChangeConfiguration(
String key,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can you have multiple legacyKeys but not multiple keys? It seems like maybe both should be Sets that support multiple calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is my understanding: A PolarisConfiguration or a configuration for any quarkus app has a name (key in PoalrisConfiguration) that is used to identify it. The key/name is the identity of the configuration. The point of adding support for legacyKeys was to enable identity changes wherein we can for certain time support deprecated keys. I don't see why we should have multiple identifying keys mapping to a single entity. If you ever need multiple identities, then the legacy keys do support the need but eventually each entity must converge to a single identity.

Please let me know if I am missing something here.

Copy link
Contributor

@eric-maynard eric-maynard Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just seems odd that we would support multiple invalid keys but not multiple valid keys. In my original comment here I wrote:

Introduce a framework to support multiple config names, with some of them marked for deprecation, similar to catalogConfigUnsafe

rather than ... with all but one marked for deprecation ... because I had this in mind.

I do agree that the situation where you'd have multiple valid configs seems contrived, but so does the situation where you'd have multiple invalid configs or really even one invalid config. So if we're overhauling the whole key system, I think it makes sense to go all the way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we support multiple invalid keys. We track multiple legacy keys.

From a semantic perspective, a PrimaryKey in a RDBMS, a keyin a key-value store or a Java Map (or any other programming language) maps to a single row, value or entity. In all these examples, two keys never map to the same entity.

What we have with the legacyKeys is a way to track historic versions of the key-value pair. In any DBMS or KV store that supports multi-versioning, it's pretty normal to have multiple entries (multiple versions) for a single key. When the primaryKey/key changes, the DBMS/KV store adds a tombstone for the old record and creates a new record with the new key. While changing keys in PolarisConfig, we are essentially creating a new key-value pair in itself but its nice to track the older key, hence the support for legacy keys. Essentially one could rewrite the entire PolarisConfig class with a Map where the key is the active key and the value is everything else. In which case, we'd track the legacyKeys as a part of the value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key is not a primary key though. There's no RDBMS or KV store storing these; they describe entries in a config file. It seems reasonable to expect that multiple different entries could be used to configure the same feature.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider, too, that the current code needs to deal with a situation where the key has value true but the legacyKey has value false. Or two legacy keys have different values. Clearly, the key(s) is not unique and the code needs to handle multiple keys for a given feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with this:

It seems reasonable to expect that multiple different entries could be used to configure the same feature.

You can have two configurations that store the same logical value and then let the specific module decide upon how to reconcile any conflicting values. But the config service should not register two configurations for the same feature.

Regarding the case where the key is true and legacyKey is false. I don't see how it is any different from a default value being defined say FeatureConfiguration.java, a value being defined in application.properties and then a JVM system property. In this case the system property takes precedence. Similarly the key takes precedence over legacyKey.

Copy link
Contributor

@eric-maynard eric-maynard Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with this:

It seems reasonable to expect that multiple different entries could be used to configure the same feature.

Your current PR has this functionality... except only partially:

Keys Legacy Keys Multiple Entries OK?
0 0 No Yes
0 1 No Yes
0 2+ Yes Yes
1 0 No Yes
1 1 Yes Yes
1 2+ Yes Yes
2 0 Yes No
2 1 Yes No
2 2+ Yes No

What is the rationale behind this asymmetry? If, as stated before, it is because the key is thought of as a primary key then this is just not correct. You can write a valid config that violates the "primary key constraint" by duplicating an entry.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case with 0 keys is not possible. We can't define a PolarisConfig without a key. That was always the case. Unless you'd prefer if we annotate the field with @NonNull in the constructor.

You can write a valid config that violates the "primary key constraint" by duplicating an entry.

The registerConfiguration function will actually disallow duplicate entries and throw an IllegalArgumentException. That check is a part of the existing code, this PR is not change it. This PR goes further and checks that any existing key or legacyKey does not overlap with a new configuration that is being registered (again both key and legacyKeys).

I think we're looking at two entirely different aspects of a configuration.

  1. Declaring the configuration in FeatureConfiguration or BehaviorChangeConfiguration.
  2. Assigning values to a configuration which is done in application.properties / JVM arguments.

The two are orthogonal to each other. We should impose constraints on how PolarisConfigs are declared. We can't however impose constraints on how the user defines these configs. If the user does have multiple definitions of the same config, then we apply the one with the highest priority and discard the rest. If indeed we should disallow multiple definitions, then any config that has a default value defined should essentially disallow any overwrites in application.properties. AFAIK, Quarkus doesn't do that and there isn't a way for the app to determine such a condition.

Set<String> legacyKeys,
String description,
T defaultValue,
Optional<String> catalogConfig,
Optional<String> catalogConfigUnsafe) {
super(key, description, defaultValue, catalogConfig, catalogConfigUnsafe);
super(key, legacyKeys, description, defaultValue, catalogConfig, catalogConfigUnsafe);
}

public static final BehaviorChangeConfiguration<Boolean> VALIDATE_VIEW_LOCATION_OVERLAP =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.apache.polaris.core.admin.model.AuthenticationParameters;
import org.apache.polaris.core.admin.model.StorageConfigInfo;
import org.apache.polaris.core.connection.ConnectionType;
Expand All @@ -36,11 +37,12 @@
public class FeatureConfiguration<T> extends PolarisConfiguration<T> {
protected FeatureConfiguration(
String key,
Set<String> legacyKeys,
String description,
T defaultValue,
Optional<String> catalogConfig,
Optional<String> catalogConfigUnsafe) {
super(key, description, defaultValue, catalogConfig, catalogConfigUnsafe);
super(key, legacyKeys, description, defaultValue, catalogConfig, catalogConfigUnsafe);
}

/**
Expand Down Expand Up @@ -263,12 +265,14 @@ public static void enforceFeatureEnabledOrThrow(
.defaultValue(true)
.buildFeatureConfiguration();

public static final FeatureConfiguration<List<String>> SUPPORTED_CATALOG_CONNECTION_TYPES =
PolarisConfiguration.<List<String>>builder()
.key("SUPPORTED_CATALOG_CONNECTION_TYPES")
.description("The list of supported catalog connection types for federation")
.defaultValue(List.of(ConnectionType.ICEBERG_REST.name()))
.buildFeatureConfiguration();
public static final FeatureConfiguration<List<String>>
SUPPORTED_EXTERNAL_CATALOG_CONNECTION_TYPES =
PolarisConfiguration.<List<String>>builder()
.key("SUPPORTED_EXTERNAL_CATALOG_CONNECTION_TYPES")
.legacyKey("SUPPORTED_CATALOG_CONNECTION_TYPES")
.description("The list of supported external catalog connection types for federation")
.defaultValue(List.of(ConnectionType.ICEBERG_REST.name()))
.buildFeatureConfiguration();

public static final FeatureConfiguration<List<String>>
SUPPORTED_EXTERNAL_CATALOG_AUTHENTICATION_TYPES =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
package org.apache.polaris.core.config;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand All @@ -41,6 +43,7 @@ public abstract class PolarisConfiguration<T> {

public final String key;
public final String description;
public final Set<String> legacyKeys;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this value used for property config value lookup? Sorry, if I missed it in the diff 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is used when registering a new configuration.

Previously (before this change), we would go over existingConfigurations check if it matched any existingConfiguration's key. Now, we check if the new configuration (both key and legacyKeys) match either an existing key (line 61) or existing legacyKeys (line 65 - 76)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct... but we should also use it to find config values in case the key is not found, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I have implemented it, any configuration always has a key(I think that's a valid requirement). Is there a reason we would we keep a configuration with just legacyKeys?

Copy link
Contributor Author

@poojanilangekar poojanilangekar Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think I get what you mean. I am guessing you meant the aspect of reading in the properties. If so I will send a fix shortly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx! To clarify: old config entries should keep working until we drop support for them (e.g. in 2.0). Let's add a WARN log message when we have to fall back to the "legacy" property name so that users are notified in runtime and are more aware of the need to migrating to the new key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I did not notice this before, but I think having public fiends in general is not a good idea. Since you're toughing this code in current PR, would you mind following up with another PR to convert them to private fields?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will send out a follow up PR.

public final T defaultValue;
private final Optional<String> catalogConfigImpl;
private final Optional<String> catalogConfigUnsafeImpl;
Expand All @@ -55,9 +58,22 @@ public abstract class PolarisConfiguration<T> {
*/
private static void registerConfiguration(PolarisConfiguration<?> configuration) {
for (PolarisConfiguration<?> existingConfiguration : allConfigurations) {
if (existingConfiguration.key.equals(configuration.key)) {
if (existingConfiguration.key.equals(configuration.key)
|| configuration.legacyKeys.contains(existingConfiguration.key)) {
throw new IllegalArgumentException(
String.format("Config '%s' is already in use", existingConfiguration.key));
} else if (existingConfiguration.legacyKeys.contains(configuration.key)) {
throw new IllegalArgumentException(
String.format("Config '%s' is already in use", configuration.key));
} else if (!configuration.legacyKeys.isEmpty()) {
Set<String> legacyKeys = new HashSet<>(existingConfiguration.legacyKeys);
legacyKeys.retainAll(configuration.legacyKeys);
if (!legacyKeys.isEmpty()) {
throw new IllegalArgumentException(
String.format(
"Config '%s' is already in use",
legacyKeys.stream().collect(Collectors.joining(","))));
}
} else {
var configs =
Stream.of(
Expand Down Expand Up @@ -86,18 +102,28 @@ public static List<PolarisConfiguration<?>> getAllConfigurations() {
@SuppressWarnings("unchecked")
protected PolarisConfiguration(
String key,
Set<String> legacyKeys,
String description,
T defaultValue,
Optional<String> catalogConfig,
Optional<String> catalogConfigUnsafe) {
this.key = key;
this.legacyKeys = legacyKeys;
this.description = description;
this.defaultValue = defaultValue;
this.catalogConfigImpl = catalogConfig;
this.catalogConfigUnsafeImpl = catalogConfigUnsafe;
this.typ = (Class<T>) defaultValue.getClass();
}

public boolean hasLegacyKeys() {
return !legacyKeys.isEmpty();
}

public Set<String> legacyKeys() {
return legacyKeys;
}

public boolean hasCatalogConfig() {
return catalogConfigImpl.isPresent();
}
Expand Down Expand Up @@ -126,6 +152,7 @@ T cast(Object value) {

public static class Builder<T> {
private String key;
private Set<String> legacyKeys = new HashSet<>();
private String description;
private T defaultValue;
private Optional<String> catalogConfig = Optional.empty();
Expand All @@ -136,6 +163,16 @@ public Builder<T> key(String key) {
return this;
}

public Builder<T> legacyKey(String legacyKey) {
legacyKeys.add(legacyKey);
return this;
}

public Builder<T> legacyKeys(Set<String> legacyKeys) {
this.legacyKeys.addAll(legacyKeys);
return this;
}

public Builder<T> description(String description) {
this.description = description;
return this;
Expand Down Expand Up @@ -190,7 +227,7 @@ public FeatureConfiguration<T> buildFeatureConfiguration() {
validateOrThrow();
FeatureConfiguration<T> config =
new FeatureConfiguration<>(
key, description, defaultValue, catalogConfig, catalogConfigUnsafe);
key, legacyKeys, description, defaultValue, catalogConfig, catalogConfigUnsafe);
PolarisConfiguration.registerConfiguration(config);
return config;
}
Expand All @@ -203,7 +240,7 @@ public BehaviorChangeConfiguration<T> buildBehaviorChangeConfiguration() {
}
BehaviorChangeConfiguration<T> config =
new BehaviorChangeConfiguration<>(
key, description, defaultValue, catalogConfig, catalogConfigUnsafe);
key, legacyKeys, description, defaultValue, catalogConfig, catalogConfigUnsafe);
PolarisConfiguration.registerConfiguration(config);
return config;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,27 @@ public interface PolarisConfigurationStore {
*/
default <T> @Nonnull T getConfiguration(
@Nonnull RealmContext realmContext, PolarisConfiguration<T> config) {
T result = getConfiguration(realmContext, config.key, config.defaultValue);
T result = getConfiguration(realmContext, config.key);

if (result == null && config.hasLegacyKeys()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the key overrides the legacy keys?

for (String legacyKey : config.legacyKeys()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will lead to nondeterministic behavior if two legacy keys for a feature have different values

T legacyResult = getConfiguration(realmContext, legacyKey);
if (legacyResult != null) {
LOGGER.warn(
"Legacy configuration key '{}' is in use. Please use '{}' instead.",
legacyKey,
config.key);
if (result == null) {
result = legacyResult;
}
}
}
}

if (result == null) {
result = config.defaultValue;
}

return tryCast(config, result);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* 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.polaris.core.config;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.junit.jupiter.api.Test;

public class PolarisConfigurationTest {

@Test
void testFeatureConfigurationWithLegacyKey() {
FeatureConfiguration<Boolean> config =
PolarisConfiguration.<Boolean>builder()
.key("FEATURE_CONFIG_WITH_LEGACY_KEY")
.legacyKey("OLD_FEATURE_CONFIG_WITH_LEGACY_KEY")
.description("Test configuration with deprecated key")
.defaultValue(true)
.buildFeatureConfiguration();

assertThat(config.hasLegacyKeys()).isTrue();
assertThat(config.legacyKeys().contains("OLD_FEATURE_CONFIG_WITH_LEGACY_KEY")).isTrue();
assertThat(config.key).isEqualTo("FEATURE_CONFIG_WITH_LEGACY_KEY");
assertThat(config.description).isEqualTo("Test configuration with deprecated key");
assertThat(config.defaultValue).isTrue();
}

@Test
void testBehaviorChangeConfigurationWithLegacyKey() {
BehaviorChangeConfiguration<String> config =
PolarisConfiguration.<String>builder()
.key("BEHAVIOR_CONFIG_WITH_LEGACY_KEY")
.legacyKey("OLD_BEHAVIOR_CONFIG_WITH_LEGACY_KEY")
.description("Test behavior configuration with deprecated key")
.defaultValue("test-value")
.buildBehaviorChangeConfiguration();

assertThat(config.hasLegacyKeys()).isTrue();
assertThat(config.legacyKeys().contains("OLD_BEHAVIOR_CONFIG_WITH_LEGACY_KEY")).isTrue();
assertThat(config.key).isEqualTo("BEHAVIOR_CONFIG_WITH_LEGACY_KEY");
assertThat(config.defaultValue).isEqualTo("test-value");
}

@Test
void testDuplicateKeyValidation() {
PolarisConfiguration.<Boolean>builder()
.key("DUPLICATE_KEY_TEST_1")
.description("First configuration")
.defaultValue(true)
.buildFeatureConfiguration();

assertThatThrownBy(
() ->
PolarisConfiguration.<Boolean>builder()
.key("DUPLICATE_KEY_TEST_1")
.description("Second configuration")
.defaultValue(false)
.buildFeatureConfiguration())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Config 'DUPLICATE_KEY_TEST_1' is already in use");
}

@Test
void testDuplicateLegacyKeysValidation() {
PolarisConfiguration.<Boolean>builder()
.key("NEW_KEY_DEPRECATED_TEST_2")
.legacyKey("LEGACY_KEY_TEST_2_1")
.legacyKey("LEGACY_KEY_TEST_2_2")
.legacyKey("LEGACY_KEY_TEST_2_3")
.description("First configuration with deprecated key")
.defaultValue(true)
.buildFeatureConfiguration();

assertThatThrownBy(
() ->
PolarisConfiguration.<Boolean>builder()
.key("ANOTHER_NEW_KEY_2")
.legacyKey("LEGACY_KEY_TEST_2_1")
.legacyKey("LEGACY_KEY_TEST_2_3")
.description("Second configuration with same deprecated key")
.defaultValue(false)
.buildFeatureConfiguration())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Config 'LEGACY_KEY_TEST_2_3,LEGACY_KEY_TEST_2_1' is already in use");
}

@Test
void testNewKeyMatchingExistingLegacyKeyValidation() {
PolarisConfiguration.<Boolean>builder()
.key("ORIGINAL_KEY_3")
.legacyKey("OLD_LEGACY_KEY_3")
.description("First configuration with deprecated key")
.defaultValue(true)
.buildFeatureConfiguration();

assertThatThrownBy(
() ->
PolarisConfiguration.<Boolean>builder()
.key("OLD_LEGACY_KEY_3")
.description("Configuration with key matching existing deprecated key")
.defaultValue(false)
.buildFeatureConfiguration())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Config 'OLD_LEGACY_KEY_3' is already in use");
}

@Test
void testLegacyKeyMatchingExistingKeyValidation() {
PolarisConfiguration.<Boolean>builder()
.key("EXISTING_KEY_4")
.description("First configuration")
.defaultValue(true)
.buildFeatureConfiguration();

assertThatThrownBy(
() ->
PolarisConfiguration.<Boolean>builder()
.key("NEW_KEY_FOR_VALIDATION_4")
.legacyKey("EXISTING_KEY_4")
.legacyKey("EXISTING_KEY_4_1")
.description("Configuration with deprecated key matching existing key")
.defaultValue(false)
.buildFeatureConfiguration())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Config 'EXISTING_KEY_4' is already in use");
}
}
Loading
Loading