Skip to content
Open
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
20 changes: 20 additions & 0 deletions core/src/main/java/org/apache/iceberg/CatalogProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,24 @@ private CatalogProperties() {}
"org.apache.iceberg.azure.keymanagement.AzureKeyManagementClient";
public static final String ENCRYPTION_KMS_IMPL_GCP =
"org.apache.iceberg.gcp.GcpKeyManagementClient";

/**
* Controls whether identifier resolution (namespace, table, view) should be case-insensitive.
*
* <p>When enabled, all identifiers are normalized to a consistent case before being sent to the
* catalog backend. This is useful for catalogs that preserve identifier case but may require
* case-insensitive lookups.
*/
public static final String CASE_INSENSITIVE = "case-insensitive";

public static final boolean CASE_INSENSITIVE_DEFAULT = false;

/**
* Controls the case conversion type when {@link #CASE_INSENSITIVE} is enabled.
*
* <p>Supported values are {@code lower_case} (default) and {@code upper_case}.
*/
public static final String CASE_INSENSITIVE_TYPE = "case-insensitive-type";

public static final String CASE_INSENSITIVE_TYPE_DEFAULT = "lower_case";
}
128 changes: 91 additions & 37 deletions core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
Expand All @@ -39,8 +40,10 @@
import org.apache.iceberg.exceptions.NamespaceNotEmptyException;
import org.apache.iceberg.exceptions.NoSuchNamespaceException;
import org.apache.iceberg.hadoop.Configurable;
import org.apache.iceberg.relocated.com.google.common.annotations.VisibleForTesting;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.util.PropertyUtil;
import org.apache.iceberg.view.View;
import org.apache.iceberg.view.ViewBuilder;

Expand All @@ -52,6 +55,9 @@ public class RESTCatalog
private final SessionCatalog.SessionContext context;
private final ViewCatalog viewSessionCatalog;

private boolean caseInsensitive = CatalogProperties.CASE_INSENSITIVE_DEFAULT;
private String caseType = CatalogProperties.CASE_INSENSITIVE_TYPE_DEFAULT;

public RESTCatalog() {
this(
SessionCatalog.SessionContext.createEmpty(),
Expand Down Expand Up @@ -93,9 +99,56 @@ protected RESTSessionCatalog newSessionCatalog(
@Override
public void initialize(String name, Map<String, String> props) {
Preconditions.checkArgument(props != null, "Invalid configuration: null");

this.caseInsensitive =
PropertyUtil.propertyAsBoolean(
props, CatalogProperties.CASE_INSENSITIVE, CatalogProperties.CASE_INSENSITIVE_DEFAULT);
this.caseType =
PropertyUtil.propertyAsString(
props,
CatalogProperties.CASE_INSENSITIVE_TYPE,
CatalogProperties.CASE_INSENSITIVE_TYPE_DEFAULT);
Preconditions.checkArgument(
"lower_case".equals(caseType) || "upper_case".equals(caseType),
"Invalid value for '%s': %s. Allowed values are: lower_case, upper_case",
CatalogProperties.CASE_INSENSITIVE_TYPE,
caseType);

sessionCatalog.initialize(name, props);
}

@VisibleForTesting
Namespace convertCase(Namespace ns) {
if (!caseInsensitive) {
return ns;
}

String[] levels = ns.levels();
String[] converted = new String[levels.length];
for (int i = 0; i < levels.length; i++) {
converted[i] =
"upper_case".equals(caseType)
? levels[i].toUpperCase(Locale.ROOT)
: levels[i].toLowerCase(Locale.ROOT);
}

return Namespace.of(converted);
}

@VisibleForTesting
TableIdentifier convertCase(TableIdentifier ident) {
if (!caseInsensitive) {
return ident;
}

Namespace convertedNs = convertCase(ident.namespace());
String convertedName =
"upper_case".equals(caseType)
? ident.name().toUpperCase(Locale.ROOT)
: ident.name().toLowerCase(Locale.ROOT);
return TableIdentifier.of(convertedNs, convertedName);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Trino Iceberg connector supports the iceberg.rest-catalog.case-insensitive-name-matching configuration property, but its behavior differs from the approach in this PR. I'm commenting here because the PR description refers to Trino.

The connector attempts to locate remote objects in a case-insensitive way by iterating over namespaces and tables and comparing their lowercase forms. If it encounters an ambiguous situation (for example, both ORDERS and orders exist), it throws an exception. This logic correctly handles mixed-case names, whereas this PR appears to address only the uppercase case.

}

protected RESTSessionCatalog sessionCatalog() {
return sessionCatalog;
}
Expand All @@ -111,27 +164,27 @@ public Map<String, String> properties() {

@Override
public List<TableIdentifier> listTables(Namespace ns) {
return delegate.listTables(ns);
return delegate.listTables(convertCase(ns));
}

@Override
public boolean tableExists(TableIdentifier ident) {
return delegate.tableExists(ident);
return delegate.tableExists(convertCase(ident));
}

@Override
public Table loadTable(TableIdentifier ident) {
return delegate.loadTable(ident);
return delegate.loadTable(convertCase(ident));
}

@Override
public void invalidateTable(TableIdentifier ident) {
delegate.invalidateTable(ident);
delegate.invalidateTable(convertCase(ident));
}

@Override
public TableBuilder buildTable(TableIdentifier ident, Schema schema) {
return delegate.buildTable(ident, schema);
return delegate.buildTable(convertCase(ident), schema);
}

@Override
Expand All @@ -141,23 +194,23 @@ public Table createTable(
PartitionSpec spec,
String location,
Map<String, String> props) {
return delegate.createTable(ident, schema, spec, location, props);
return delegate.createTable(convertCase(ident), schema, spec, location, props);
}

@Override
public Table createTable(
TableIdentifier ident, Schema schema, PartitionSpec spec, Map<String, String> props) {
return delegate.createTable(ident, schema, spec, props);
return delegate.createTable(convertCase(ident), schema, spec, props);
}

@Override
public Table createTable(TableIdentifier ident, Schema schema, PartitionSpec spec) {
return delegate.createTable(ident, schema, spec);
return delegate.createTable(convertCase(ident), schema, spec);
}

@Override
public Table createTable(TableIdentifier identifier, Schema schema) {
return delegate.createTable(identifier, schema);
return delegate.createTable(convertCase(identifier), schema);
}

@Override
Expand All @@ -167,24 +220,24 @@ public Transaction newCreateTableTransaction(
PartitionSpec spec,
String location,
Map<String, String> props) {
return delegate.newCreateTableTransaction(ident, schema, spec, location, props);
return delegate.newCreateTableTransaction(convertCase(ident), schema, spec, location, props);
}

@Override
public Transaction newCreateTableTransaction(
TableIdentifier ident, Schema schema, PartitionSpec spec, Map<String, String> props) {
return delegate.newCreateTableTransaction(ident, schema, spec, props);
return delegate.newCreateTableTransaction(convertCase(ident), schema, spec, props);
}

@Override
public Transaction newCreateTableTransaction(
TableIdentifier ident, Schema schema, PartitionSpec spec) {
return delegate.newCreateTableTransaction(ident, schema, spec);
return delegate.newCreateTableTransaction(convertCase(ident), schema, spec);
}

@Override
public Transaction newCreateTableTransaction(TableIdentifier identifier, Schema schema) {
return delegate.newCreateTableTransaction(identifier, schema);
return delegate.newCreateTableTransaction(convertCase(identifier), schema);
}

@Override
Expand All @@ -195,7 +248,8 @@ public Transaction newReplaceTableTransaction(
String location,
Map<String, String> props,
boolean orCreate) {
return delegate.newReplaceTableTransaction(ident, schema, spec, location, props, orCreate);
return delegate.newReplaceTableTransaction(
convertCase(ident), schema, spec, location, props, orCreate);
}

@Override
Expand All @@ -205,81 +259,81 @@ public Transaction newReplaceTableTransaction(
PartitionSpec spec,
Map<String, String> props,
boolean orCreate) {
return delegate.newReplaceTableTransaction(ident, schema, spec, props, orCreate);
return delegate.newReplaceTableTransaction(convertCase(ident), schema, spec, props, orCreate);
}

@Override
public Transaction newReplaceTableTransaction(
TableIdentifier ident, Schema schema, PartitionSpec spec, boolean orCreate) {
return delegate.newReplaceTableTransaction(ident, schema, spec, orCreate);
return delegate.newReplaceTableTransaction(convertCase(ident), schema, spec, orCreate);
}

@Override
public Transaction newReplaceTableTransaction(
TableIdentifier ident, Schema schema, boolean orCreate) {
return delegate.newReplaceTableTransaction(ident, schema, orCreate);
return delegate.newReplaceTableTransaction(convertCase(ident), schema, orCreate);
}

@Override
public boolean dropTable(TableIdentifier ident) {
return delegate.dropTable(ident);
return delegate.dropTable(convertCase(ident));
}

@Override
public boolean dropTable(TableIdentifier ident, boolean purge) {
return delegate.dropTable(ident, purge);
return delegate.dropTable(convertCase(ident), purge);
}

@Override
public void renameTable(TableIdentifier from, TableIdentifier to) {
delegate.renameTable(from, to);
delegate.renameTable(convertCase(from), convertCase(to));
}

@Override
public Table registerTable(TableIdentifier ident, String metadataFileLocation) {
return delegate.registerTable(ident, metadataFileLocation);
return delegate.registerTable(convertCase(ident), metadataFileLocation);
}

@Override
public Table registerTable(
TableIdentifier ident, String metadataFileLocation, boolean overwrite) {
return delegate.registerTable(ident, metadataFileLocation, overwrite);
return delegate.registerTable(convertCase(ident), metadataFileLocation, overwrite);
}

@Override
public void createNamespace(Namespace ns, Map<String, String> props) {
nsDelegate.createNamespace(ns, props);
nsDelegate.createNamespace(convertCase(ns), props);
}

@Override
public List<Namespace> listNamespaces(Namespace ns) throws NoSuchNamespaceException {
return nsDelegate.listNamespaces(ns);
return nsDelegate.listNamespaces(convertCase(ns));
}

@Override
public boolean namespaceExists(Namespace namespace) {
return nsDelegate.namespaceExists(namespace);
return nsDelegate.namespaceExists(convertCase(namespace));
}

@Override
public Map<String, String> loadNamespaceMetadata(Namespace ns) throws NoSuchNamespaceException {
return nsDelegate.loadNamespaceMetadata(ns);
return nsDelegate.loadNamespaceMetadata(convertCase(ns));
}

@Override
public boolean dropNamespace(Namespace ns) throws NamespaceNotEmptyException {
return nsDelegate.dropNamespace(ns);
return nsDelegate.dropNamespace(convertCase(ns));
}

@Override
public boolean setProperties(Namespace ns, Map<String, String> props)
throws NoSuchNamespaceException {
return nsDelegate.setProperties(ns, props);
return nsDelegate.setProperties(convertCase(ns), props);
}

@Override
public boolean removeProperties(Namespace ns, Set<String> props) throws NoSuchNamespaceException {
return nsDelegate.removeProperties(ns, props);
return nsDelegate.removeProperties(convertCase(ns), props);
}

@Override
Expand All @@ -303,41 +357,41 @@ public void commitTransaction(TableCommit... commits) {

@Override
public List<TableIdentifier> listViews(Namespace namespace) {
return viewSessionCatalog.listViews(namespace);
return viewSessionCatalog.listViews(convertCase(namespace));
}

@Override
public View loadView(TableIdentifier identifier) {
return viewSessionCatalog.loadView(identifier);
return viewSessionCatalog.loadView(convertCase(identifier));
}

@Override
public ViewBuilder buildView(TableIdentifier identifier) {
return viewSessionCatalog.buildView(identifier);
return viewSessionCatalog.buildView(convertCase(identifier));
}

@Override
public boolean dropView(TableIdentifier identifier) {
return viewSessionCatalog.dropView(identifier);
return viewSessionCatalog.dropView(convertCase(identifier));
}

@Override
public void renameView(TableIdentifier from, TableIdentifier to) {
viewSessionCatalog.renameView(from, to);
viewSessionCatalog.renameView(convertCase(from), convertCase(to));
}

@Override
public boolean viewExists(TableIdentifier identifier) {
return viewSessionCatalog.viewExists(identifier);
return viewSessionCatalog.viewExists(convertCase(identifier));
}

@Override
public void invalidateView(TableIdentifier identifier) {
viewSessionCatalog.invalidateView(identifier);
viewSessionCatalog.invalidateView(convertCase(identifier));
}

@Override
public View registerView(TableIdentifier identifier, String metadataFileLocation) {
return viewSessionCatalog.registerView(identifier, metadataFileLocation);
return viewSessionCatalog.registerView(convertCase(identifier), metadataFileLocation);
}
}
Loading
Loading