This is an automated email from the ASF dual-hosted git repository.
mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 65d305fdaa [#12097] improvement(core): Validate names before
normalizing in CapabilityHelpers (#12098)
65d305fdaa is described below
commit 65d305fdaa380b6d629963ab807124cfda1d54da
Author: Yuhui <[email protected]>
AuthorDate: Tue Jul 21 14:24:13 2026 +0800
[#12097] improvement(core): Validate names before normalizing in
CapabilityHelpers (#12098)
### What changes were proposed in this pull request?
Reorder validation and normalization in `CapabilityHelpers` so
`applyNameSpecification`
runs on the original name before
`applyCaseSensitiveOn*`/`normalizeName`. Remove the
redundant re-normalization of identifiers already returned by
`listSchemas`/`listTables`
in `SchemaNormalizeDispatcher`/`TableNormalizeDispatcher`.
### Why are the changes needed?
For catalogs whose name folding depends on the name's original form
(e.g. a quoted
identifier keeps case/spaces, an unquoted one gets folded), validating
after
normalization checks the wrong form of the name. Similarly, re-applying
`normalizeName`
to identifiers a catalog already returned in canonical form isn't
guaranteed to be
idempotent for such catalogs and can corrupt an already-correct name.
Fix: #12097
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Added a unit test; existing core module tests pass.
---
.../gravitino/catalog/CapabilityHelpers.java | 26 +++----
.../catalog/FilesetNormalizeDispatcher.java | 17 ++---
.../catalog/FunctionNormalizeDispatcher.java | 17 ++---
.../catalog/ModelNormalizeDispatcher.java | 17 ++---
.../catalog/PartitionNormalizeDispatcher.java | 22 +++---
.../catalog/SchemaNormalizeDispatcher.java | 19 ++---
.../catalog/TableNormalizeDispatcher.java | 17 ++---
.../catalog/TopicNormalizeDispatcher.java | 17 ++---
.../gravitino/catalog/ViewNormalizeDispatcher.java | 16 ++---
.../gravitino/catalog/TestCapabilityHelpers.java | 50 +++++++++++++
.../catalog/TestPartitionNormalizeDispatcher.java | 72 +++++++++++++++++++
.../catalog/TestTableNormalizeDispatcher.java | 82 ++++++++++++++++++++++
12 files changed, 262 insertions(+), 110 deletions(-)
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
b/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
index d6d0a90e7e..eae018c2c0 100644
--- a/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
+++ b/core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java
@@ -252,9 +252,8 @@ public class CapabilityHelpers {
}
private static String[] applyCapabilities(String[] fieldName, Capability
capabilities) {
- String[] sensitiveOnColumnName = applyCaseSensitiveOnColumnName(fieldName,
capabilities);
- applyNameSpecification(Capability.Scope.COLUMN, sensitiveOnColumnName[0],
capabilities);
- return sensitiveOnColumnName;
+ applyNameSpecification(Capability.Scope.COLUMN, fieldName[0],
capabilities);
+ return applyCaseSensitiveOnColumnName(fieldName, capabilities);
}
private static Transform applyCapabilities(Transform transform, Capability
capabilities) {
@@ -343,38 +342,42 @@ public class CapabilityHelpers {
private static FilesetChange applyCapabilities(
FilesetChange.RenameFileset renameFileset, Capability capabilities) {
+ applyNameSpecification(Capability.Scope.FILESET,
renameFileset.getNewName(), capabilities);
String newName =
applyCaseSensitiveOnName(
Capability.Scope.FILESET, renameFileset.getNewName(),
capabilities);
- applyNameSpecification(Capability.Scope.FILESET, newName, capabilities);
return FilesetChange.rename(newName);
}
private static ViewChange applyCapabilities(
ViewChange.RenameView renameView, Capability capabilities) {
+ applyNameSpecification(Capability.Scope.VIEW, renameView.getNewName(),
capabilities);
String newName =
applyCaseSensitiveOnName(Capability.Scope.VIEW,
renameView.getNewName(), capabilities);
- applyNameSpecification(Capability.Scope.VIEW, newName, capabilities);
return ViewChange.rename(newName);
}
private static TableChange applyCapabilities(
TableChange.RenameTable renameTable, Capability capabilities) {
+ applyNameSpecification(Capability.Scope.TABLE, renameTable.getNewName(),
capabilities);
String newName =
applyCaseSensitiveOnName(Capability.Scope.TABLE,
renameTable.getNewName(), capabilities);
String newSchemaName =
renameTable
.getNewSchemaName()
- .map(s -> applyCaseSensitiveOnName(Capability.Scope.SCHEMA, s,
capabilities))
+ .map(
+ s -> {
+ applyNameSpecification(Capability.Scope.SCHEMA, s,
capabilities);
+ return applyCaseSensitiveOnName(Capability.Scope.SCHEMA, s,
capabilities);
+ })
.orElse(null);
- applyNameSpecification(Capability.Scope.TABLE, newName, capabilities);
return TableChange.rename(newName, newSchemaName);
}
private static TableChange applyCapabilities(
TableChange.ColumnChange change, Capability capabilities) {
+ applyNameSpecification(Capability.Scope.COLUMN, change.fieldName()[0],
capabilities);
String[] fieldName = applyCaseSensitiveOnColumnName(change.fieldName(),
capabilities);
- applyNameSpecification(Capability.Scope.COLUMN, fieldName[0],
capabilities);
if (change instanceof TableChange.AddColumn) {
return applyCapabilities((TableChange.AddColumn) change, capabilities);
@@ -404,10 +407,10 @@ public class CapabilityHelpers {
(TableChange.UpdateColumnPosition) change;
if (updateColumnPosition.getPosition() instanceof TableChange.After) {
TableChange.After afterPosition = (TableChange.After)
updateColumnPosition.getPosition();
+ applyNameSpecification(Capability.Scope.COLUMN,
afterPosition.getColumn(), capabilities);
String afterFieldName =
applyCaseSensitiveOnName(
Capability.Scope.COLUMN, afterPosition.getColumn(),
capabilities);
- applyNameSpecification(Capability.Scope.COLUMN, afterFieldName,
capabilities);
return TableChange.updateColumnPosition(
fieldName, TableChange.ColumnPosition.after(afterFieldName));
}
@@ -498,9 +501,8 @@ public class CapabilityHelpers {
private static String applyCapabilitiesOnName(
Capability.Scope scope, String name, Capability capabilities) {
- String standardizeName = applyCaseSensitiveOnName(scope, name,
capabilities);
- applyNameSpecification(scope, standardizeName, capabilities);
- return standardizeName;
+ applyNameSpecification(scope, name, capabilities);
+ return applyCaseSensitiveOnName(scope, name, capabilities);
}
public static String applyCaseSensitiveOnName(
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/FilesetNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/FilesetNormalizeDispatcher.java
index 44a0c1c729..292edd8d27 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/FilesetNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/FilesetNormalizeDispatcher.java
@@ -24,7 +24,6 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
import java.io.IOException;
import java.util.Map;
-import org.apache.commons.lang3.ArrayUtils;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
@@ -36,6 +35,10 @@ import org.apache.gravitino.file.FileInfo;
import org.apache.gravitino.file.Fileset;
import org.apache.gravitino.file.FilesetChange;
+/**
+ * Note on list operations: names returned by list methods (e.g. {@link
#listFilesets(Namespace)})
+ * are assumed to already be in their canonical, legal form and are not
re-normalized here.
+ */
public class FilesetNormalizeDispatcher implements FilesetDispatcher {
private final CatalogManager catalogManager;
private final FilesetDispatcher dispatcher;
@@ -50,8 +53,7 @@ public class FilesetNormalizeDispatcher implements
FilesetDispatcher {
// The constraints of the name spec may be more strict than underlying
catalog,
// and for compatibility reasons, we only apply case-sensitive
capabilities here.
Namespace caseSensitiveNs = normalizeCaseSensitive(namespace);
- NameIdentifier[] identifiers = dispatcher.listFilesets(caseSensitiveNs);
- return normalizeCaseSensitive(identifiers);
+ return dispatcher.listFilesets(caseSensitiveNs);
}
@Override
@@ -125,13 +127,4 @@ public class FilesetNormalizeDispatcher implements
FilesetDispatcher {
Capability capabilities = getCapability(filesetIdent, catalogManager);
return applyCaseSensitive(filesetIdent, Capability.Scope.FILESET,
capabilities);
}
-
- private NameIdentifier[] normalizeCaseSensitive(NameIdentifier[]
filesetIdents) {
- if (ArrayUtils.isEmpty(filesetIdents)) {
- return filesetIdents;
- }
-
- Capability capabilities = getCapability(filesetIdents[0], catalogManager);
- return applyCaseSensitive(filesetIdents, Capability.Scope.FILESET,
capabilities);
- }
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/FunctionNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/FunctionNormalizeDispatcher.java
index 810ca4ed5e..03bdf6a0cc 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/FunctionNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/FunctionNormalizeDispatcher.java
@@ -22,7 +22,6 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.applyCapabilities;
import static
org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitive;
import static org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
-import org.apache.commons.lang3.ArrayUtils;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
@@ -37,6 +36,10 @@ import org.apache.gravitino.function.FunctionType;
/**
* {@code FunctionNormalizeDispatcher} normalizes function identifiers and
namespaces by applying
* case-sensitivity and other naming capabilities before delegating to the
underlying dispatcher.
+ *
+ * <p>Note on list operations: names returned by list methods (e.g. {@link
+ * #listFunctions(Namespace)}) are assumed to already be in their canonical,
legal form and are not
+ * re-normalized here.
*/
public class FunctionNormalizeDispatcher implements FunctionDispatcher {
private final CatalogManager catalogManager;
@@ -50,8 +53,7 @@ public class FunctionNormalizeDispatcher implements
FunctionDispatcher {
@Override
public NameIdentifier[] listFunctions(Namespace namespace) throws
NoSuchSchemaException {
Namespace caseSensitiveNs = normalizeCaseSensitive(namespace);
- NameIdentifier[] identifiers = dispatcher.listFunctions(caseSensitiveNs);
- return normalizeCaseSensitive(identifiers);
+ return dispatcher.listFunctions(caseSensitiveNs);
}
@Override
@@ -102,15 +104,6 @@ public class FunctionNormalizeDispatcher implements
FunctionDispatcher {
return applyCaseSensitive(functionIdent, Capability.Scope.FUNCTION,
capabilities);
}
- private NameIdentifier[] normalizeCaseSensitive(NameIdentifier[]
functionIdents) {
- if (ArrayUtils.isEmpty(functionIdents)) {
- return functionIdents;
- }
-
- Capability capabilities = getCapability(functionIdents[0], catalogManager);
- return applyCaseSensitive(functionIdents, Capability.Scope.FUNCTION,
capabilities);
- }
-
private NameIdentifier normalizeNameIdentifier(NameIdentifier functionIdent)
{
Capability capability = getCapability(functionIdent, catalogManager);
return applyCapabilities(functionIdent, Capability.Scope.FUNCTION,
capability);
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
index 5bf32f892b..978680b32b 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/ModelNormalizeDispatcher.java
@@ -23,7 +23,6 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitive;
import static org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
import java.util.Map;
-import org.apache.commons.lang3.ArrayUtils;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
@@ -38,6 +37,10 @@ import org.apache.gravitino.model.ModelChange;
import org.apache.gravitino.model.ModelVersion;
import org.apache.gravitino.model.ModelVersionChange;
+/**
+ * Note on list operations: names returned by list methods (e.g. {@link
#listModels(Namespace)}) are
+ * assumed to already be in their canonical, legal form and are not
re-normalized here.
+ */
public class ModelNormalizeDispatcher implements ModelDispatcher {
private final CatalogManager catalogManager;
private final ModelDispatcher dispatcher;
@@ -52,8 +55,7 @@ public class ModelNormalizeDispatcher implements
ModelDispatcher {
// The constraints of the name spec may be more strict than underlying
catalog,
// and for compatibility reasons, we only apply case-sensitive
capabilities here.
Namespace caseSensitiveNs = normalizeCaseSensitive(namespace);
- NameIdentifier[] identifiers = dispatcher.listModels(caseSensitiveNs);
- return normalizeCaseSensitive(identifiers);
+ return dispatcher.listModels(caseSensitiveNs);
}
@Override
@@ -181,15 +183,6 @@ public class ModelNormalizeDispatcher implements
ModelDispatcher {
return applyCaseSensitive(ident, Capability.Scope.MODEL, capabilities);
}
- private NameIdentifier[] normalizeCaseSensitive(NameIdentifier[] idents) {
- if (ArrayUtils.isEmpty(idents)) {
- return idents;
- }
-
- Capability capabilities = getCapability(idents[0], catalogManager);
- return applyCaseSensitive(idents, Capability.Scope.MODEL, capabilities);
- }
-
private NameIdentifier normalizeNameIdentifier(NameIdentifier ident) {
Capability capability = getCapability(ident, catalogManager);
return applyCapabilities(ident, Capability.Scope.MODEL, capability);
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/PartitionNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/PartitionNormalizeDispatcher.java
index 899846fdb7..59f9b27c36 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/PartitionNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/PartitionNormalizeDispatcher.java
@@ -22,13 +22,17 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitive;
import static
org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitiveOnName;
import static org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
-import java.util.Arrays;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.exceptions.NoSuchPartitionException;
import org.apache.gravitino.exceptions.PartitionAlreadyExistsException;
import org.apache.gravitino.rel.partitions.Partition;
+/**
+ * Note on list operations: names returned by list methods (e.g. {@link
+ * #listPartitionNames(NameIdentifier)}, {@link
#listPartitions(NameIdentifier)}) are assumed to
+ * already be in their canonical, legal form and are not re-normalized here.
+ */
public class PartitionNormalizeDispatcher implements PartitionDispatcher {
private final CatalogManager catalogManager;
private final PartitionDispatcher dispatcher;
@@ -42,23 +46,15 @@ public class PartitionNormalizeDispatcher implements
PartitionDispatcher {
@Override
public String[] listPartitionNames(NameIdentifier tableIdent) {
Capability capabilities = getCapability(tableIdent, catalogManager);
- String[] partitionNames =
- dispatcher.listPartitionNames(
- applyCaseSensitive(tableIdent, Capability.Scope.TABLE,
capabilities));
- return Arrays.stream(partitionNames)
- .map(
- partitionName ->
- applyCaseSensitiveOnName(Capability.Scope.PARTITION,
partitionName, capabilities))
- .toArray(String[]::new);
+ return dispatcher.listPartitionNames(
+ applyCaseSensitive(tableIdent, Capability.Scope.TABLE, capabilities));
}
@Override
public Partition[] listPartitions(NameIdentifier tableIdent) {
Capability capabilities = getCapability(tableIdent, catalogManager);
- Partition[] partitions =
- dispatcher.listPartitions(
- CapabilityHelpers.applyCaseSensitive(tableIdent,
Capability.Scope.TABLE, capabilities));
- return applyCaseSensitive(partitions, capabilities);
+ return dispatcher.listPartitions(
+ CapabilityHelpers.applyCaseSensitive(tableIdent,
Capability.Scope.TABLE, capabilities));
}
@Override
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/SchemaNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/SchemaNormalizeDispatcher.java
index b4d55e6cb9..564d777820 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/SchemaNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/SchemaNormalizeDispatcher.java
@@ -23,7 +23,6 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitive;
import static org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
import java.util.Map;
-import org.apache.commons.lang3.ArrayUtils;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.Schema;
@@ -34,6 +33,10 @@ import org.apache.gravitino.exceptions.NoSuchSchemaException;
import org.apache.gravitino.exceptions.NonEmptySchemaException;
import org.apache.gravitino.exceptions.SchemaAlreadyExistsException;
+/**
+ * Note on list operations: names returned by list methods (e.g. {@link
#listSchemas(Namespace)})
+ * are assumed to already be in their canonical, legal form and are not
re-normalized here.
+ */
public class SchemaNormalizeDispatcher implements SchemaDispatcher {
private final CatalogManager catalogManager;
private final SchemaDispatcher dispatcher;
@@ -57,10 +60,7 @@ public class SchemaNormalizeDispatcher implements
SchemaDispatcher {
NameIdentifier.of(namespace.levels())));
}
- NameIdentifier[] identifiers = dispatcher.listSchemas(namespace);
- // The constraints of the name spec may be more strict than underlying
catalog,
- // and for compatibility reasons, we only apply case-sensitive
capabilities here.
- return normalizeCaseSensitive(identifiers);
+ return dispatcher.listSchemas(namespace);
}
@Override
@@ -110,13 +110,4 @@ public class SchemaNormalizeDispatcher implements
SchemaDispatcher {
Capability capabilities = getCapability(schemaIdent, catalogManager);
return applyCaseSensitive(schemaIdent, Capability.Scope.SCHEMA,
capabilities);
}
-
- private NameIdentifier[] normalizeCaseSensitive(NameIdentifier[]
schemaIdents) {
- if (ArrayUtils.isEmpty(schemaIdents)) {
- return schemaIdents;
- }
-
- Capability capabilities = getCapability(schemaIdents[0], catalogManager);
- return applyCaseSensitive(schemaIdents, Capability.Scope.SCHEMA,
capabilities);
- }
}
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/TableNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/TableNormalizeDispatcher.java
index 12fd1cb626..68544ba3cf 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/TableNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/TableNormalizeDispatcher.java
@@ -23,7 +23,6 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitive;
import static org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
import java.util.Map;
-import org.apache.commons.lang3.ArrayUtils;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
@@ -38,6 +37,10 @@ import org.apache.gravitino.rel.expressions.sorts.SortOrder;
import org.apache.gravitino.rel.expressions.transforms.Transform;
import org.apache.gravitino.rel.indexes.Index;
+/**
+ * Note on list operations: names returned by list methods (e.g. {@link
#listTables(Namespace)}) are
+ * assumed to already be in their canonical, legal form and are not
re-normalized here.
+ */
public class TableNormalizeDispatcher implements TableDispatcher {
private final CatalogManager catalogManager;
private final TableDispatcher dispatcher;
@@ -52,8 +55,7 @@ public class TableNormalizeDispatcher implements
TableDispatcher {
// The constraints of the name spec may be more strict than underlying
catalog,
// and for compatibility reasons, we only apply case-sensitive
capabilities here.
Namespace caseSensitiveNs = normalizeCaseSensitive(namespace);
- NameIdentifier[] identifiers = dispatcher.listTables(caseSensitiveNs);
- return normalizeCaseSensitive(identifiers);
+ return dispatcher.listTables(caseSensitiveNs);
}
@Override
@@ -123,15 +125,6 @@ public class TableNormalizeDispatcher implements
TableDispatcher {
return applyCaseSensitive(tableIdent, Capability.Scope.TABLE, capability);
}
- private NameIdentifier[] normalizeCaseSensitive(NameIdentifier[]
tableIdents) {
- if (ArrayUtils.isEmpty(tableIdents)) {
- return tableIdents;
- }
-
- Capability capabilities = getCapability(tableIdents[0], catalogManager);
- return applyCaseSensitive(tableIdents, Capability.Scope.TABLE,
capabilities);
- }
-
private NameIdentifier normalizeNameIdentifier(NameIdentifier tableIdent) {
Capability capability = getCapability(tableIdent, catalogManager);
return applyCapabilities(tableIdent, Capability.Scope.TABLE, capability);
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
index a104b4f671..323d5ee285 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/TopicNormalizeDispatcher.java
@@ -23,7 +23,6 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.applyCaseSensitive;
import static org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
import java.util.Map;
-import org.apache.commons.lang3.ArrayUtils;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
@@ -34,6 +33,10 @@ import org.apache.gravitino.messaging.DataLayout;
import org.apache.gravitino.messaging.Topic;
import org.apache.gravitino.messaging.TopicChange;
+/**
+ * Note on list operations: names returned by list methods (e.g. {@link
#listTopics(Namespace)}) are
+ * assumed to already be in their canonical, legal form and are not
re-normalized here.
+ */
public class TopicNormalizeDispatcher implements TopicDispatcher {
private final CatalogManager catalogManager;
private final TopicDispatcher dispatcher;
@@ -48,8 +51,7 @@ public class TopicNormalizeDispatcher implements
TopicDispatcher {
// The constraints of the name spec may be more strict than underlying
catalog,
// and for compatibility reasons, we only apply case-sensitive
capabilities here.
Namespace caseSensitiveNs = normalizeCaseSensitive(namespace);
- NameIdentifier[] identifiers = dispatcher.listTopics(caseSensitiveNs);
- return normalizeCaseSensitive(identifiers);
+ return dispatcher.listTopics(caseSensitiveNs);
}
@Override
@@ -98,15 +100,6 @@ public class TopicNormalizeDispatcher implements
TopicDispatcher {
return applyCaseSensitive(topicIdent, Capability.Scope.TOPIC,
capabilities);
}
- private NameIdentifier[] normalizeCaseSensitive(NameIdentifier[]
topicIdents) {
- if (ArrayUtils.isEmpty(topicIdents)) {
- return topicIdents;
- }
-
- Capability capabilities = getCapability(topicIdents[0], catalogManager);
- return applyCaseSensitive(topicIdents, Capability.Scope.TOPIC,
capabilities);
- }
-
private NameIdentifier normalizeNameIdentifier(NameIdentifier topicIdent) {
Capability capability = getCapability(topicIdent, catalogManager);
return applyCapabilities(topicIdent, Capability.Scope.TOPIC, capability);
diff --git
a/core/src/main/java/org/apache/gravitino/catalog/ViewNormalizeDispatcher.java
b/core/src/main/java/org/apache/gravitino/catalog/ViewNormalizeDispatcher.java
index a73f2cf6a3..5c786a1ce1 100644
---
a/core/src/main/java/org/apache/gravitino/catalog/ViewNormalizeDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/catalog/ViewNormalizeDispatcher.java
@@ -24,7 +24,6 @@ import static
org.apache.gravitino.catalog.CapabilityHelpers.getCapability;
import java.util.Map;
import javax.annotation.Nullable;
-import org.apache.commons.lang3.ArrayUtils;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
@@ -36,6 +35,10 @@ import org.apache.gravitino.rel.Representation;
import org.apache.gravitino.rel.View;
import org.apache.gravitino.rel.ViewChange;
+/**
+ * Note on list operations: names returned by list methods (e.g. {@link
#listViews(Namespace)}) are
+ * assumed to already be in their canonical, legal form and are not
re-normalized here.
+ */
public class ViewNormalizeDispatcher implements ViewDispatcher {
private final CatalogManager catalogManager;
@@ -51,8 +54,7 @@ public class ViewNormalizeDispatcher implements
ViewDispatcher {
// The constraints of the name spec may be more strict than underlying
catalog,
// and for compatibility reasons, we only apply case-sensitive
capabilities here.
Namespace caseSensitiveNs = normalizeCaseSensitive(namespace);
- NameIdentifier[] identifiers = dispatcher.listViews(caseSensitiveNs);
- return normalizeCaseSensitive(identifiers);
+ return dispatcher.listViews(caseSensitiveNs);
}
@Override
@@ -108,14 +110,6 @@ public class ViewNormalizeDispatcher implements
ViewDispatcher {
return applyCaseSensitive(ident, Capability.Scope.VIEW, capabilities);
}
- private NameIdentifier[] normalizeCaseSensitive(NameIdentifier[] idents) {
- if (ArrayUtils.isEmpty(idents)) {
- return idents;
- }
- Capability capabilities = getCapability(idents[0], catalogManager);
- return applyCaseSensitive(idents, Capability.Scope.VIEW, capabilities);
- }
-
private NameIdentifier normalizeNameIdentifier(NameIdentifier ident) {
Capability capability = getCapability(ident, catalogManager);
return applyCapabilities(ident, Capability.Scope.VIEW, capability);
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestCapabilityHelpers.java
b/core/src/test/java/org/apache/gravitino/catalog/TestCapabilityHelpers.java
index 02c0c051fd..529ee4992c 100644
--- a/core/src/test/java/org/apache/gravitino/catalog/TestCapabilityHelpers.java
+++ b/core/src/test/java/org/apache/gravitino/catalog/TestCapabilityHelpers.java
@@ -19,6 +19,8 @@
package org.apache.gravitino.catalog;
import java.util.Locale;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
import org.apache.gravitino.connector.capability.Capability;
import org.apache.gravitino.connector.capability.CapabilityResult;
import org.apache.gravitino.rel.expressions.literals.Literal;
@@ -52,6 +54,39 @@ public class TestCapabilityHelpers {
}
};
+ /**
+ * Mimics Oracle's quoting convention: a quoted name (e.g. {@code "My
Table"}) is unquoted with
+ * its case and embedded spaces preserved, while an unquoted name must match
a simple word
+ * pattern. {@code specificationOnName} must see the name as it was
originally supplied (still
+ * quoted) rather than the already-unquoted result of {@code normalizeName},
or a quoted name with
+ * a space would be wrongly rejected after normalization even though it was
valid as supplied.
+ */
+ static final Capability QUOTE_AWARE_CAPABILITY =
+ new Capability() {
+ @Override
+ public CapabilityResult specificationOnName(Scope scope, String name) {
+ if (name.startsWith("\"") && name.endsWith("\"") && name.length() >=
2) {
+ return CapabilityResult.SUPPORTED;
+ }
+ return name.matches("^\\w+$")
+ ? CapabilityResult.SUPPORTED
+ : CapabilityResult.unsupported("Illegal name: " + name);
+ }
+
+ @Override
+ public CapabilityResult caseSensitiveOnName(Scope scope) {
+ return CapabilityResult.unsupported("folding depends on quoting");
+ }
+
+ @Override
+ public String normalizeName(Scope scope, String name) {
+ if (name.startsWith("\"") && name.endsWith("\"") && name.length() >=
2) {
+ return name.substring(1, name.length() - 1);
+ }
+ return name.toUpperCase(Locale.ROOT);
+ }
+ };
+
@Test
void testApplyCaseSensitiveOnNameHonorsCustomNormalizeName() {
String normalized =
@@ -92,4 +127,19 @@ public class TestCapabilityHelpers {
Capability.Scope.PARTITION, null, Capability.DEFAULT);
Assertions.assertNull(normalized);
}
+
+ @Test
+ void testApplyCapabilitiesValidatesNameBeforeNormalizing() {
+ // A quoted name with an embedded space is valid as supplied, but its
normalized (unquoted)
+ // form no longer matches the plain-word pattern. specificationOnName must
be checked against
+ // the original name, not the already-normalized one, or this would be
wrongly rejected.
+ NameIdentifier quotedIdent =
+ NameIdentifier.of(Namespace.of("metalake", "catalog", "schema"), "\"My
Table\"");
+
+ NameIdentifier result =
+ CapabilityHelpers.applyCapabilities(
+ quotedIdent, Capability.Scope.TABLE, QUOTE_AWARE_CAPABILITY);
+
+ Assertions.assertEquals("My Table", result.name());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestPartitionNormalizeDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestPartitionNormalizeDispatcher.java
index 0151dcf248..84c9678a66 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestPartitionNormalizeDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestPartitionNormalizeDispatcher.java
@@ -30,6 +30,8 @@ import org.apache.gravitino.utils.NameIdentifierUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
public class TestPartitionNormalizeDispatcher extends TestOperationDispatcher {
@@ -90,4 +92,74 @@ public class TestPartitionNormalizeDispatcher extends
TestOperationDispatcher {
partitionNormalizeDispatcher.dropPartition(TABLE,
partition.name().toUpperCase()));
Assertions.assertFalse(partitionNormalizeDispatcher.partitionExists(TABLE,
partition.name()));
}
+
+ @Test
+ public void testAddPartitionListPartitionsGetPartitionRoundTrip() throws
Exception {
+ // Mirrors the correct usage pattern for a quote-aware catalog (e.g.
Oracle):
+ // 1. addPartition is issued with a quoted, case-sensitive name, so the
catalog stores the
+ // physical partition with its case preserved: My Partition.
+ // 2. listPartitionNames()/listPartitions() surface that physical name
unquoted (My
+ // Partition) -- names are not re-folded by the core layer.
+ // 3. Getting the partition again requires re-quoting the case-sensitive
name
+ // ("My Partition"), not passing the bare listed name back in:
normalizeName cannot tell
+ // an already-canonical name apart from raw user input.
+ NameIdentifier tableIdent =
+ NameIdentifierUtil.ofTable(metalake, catalog, "schema", "quotedTable");
+ PartitionDispatcher mockDispatcher =
Mockito.mock(PartitionDispatcher.class);
+
+ CatalogManager mockCatalogManager = Mockito.mock(CatalogManager.class);
+ CatalogManager.CatalogWrapper mockWrapper =
Mockito.mock(CatalogManager.CatalogWrapper.class);
+ Mockito.when(mockWrapper.capabilities())
+ .thenReturn(TestCapabilityHelpers.QUOTE_AWARE_CAPABILITY);
+
Mockito.when(mockCatalogManager.loadCatalogAndWrap(Mockito.any(NameIdentifier.class)))
+ .thenReturn(mockWrapper);
+
+ PartitionNormalizeDispatcher dispatcher =
+ new PartitionNormalizeDispatcher(mockDispatcher, mockCatalogManager);
+
+ // 1. Add the partition with a quoted, case-sensitive name.
+ Partition quotedPartition =
+ Partitions.identity(
+ "\"My Partition\"",
+ new String[][] {{"col1"}},
+ new Literal[] {Literals.stringLiteral("v1")},
+ Maps.newHashMap());
+
Mockito.when(mockDispatcher.addPartition(Mockito.any(NameIdentifier.class),
Mockito.any()))
+ .thenReturn(quotedPartition);
+ dispatcher.addPartition(tableIdent, quotedPartition);
+
+ ArgumentCaptor<Partition> addedPartitionCaptor =
ArgumentCaptor.forClass(Partition.class);
+ Mockito.verify(mockDispatcher)
+ .addPartition(Mockito.any(NameIdentifier.class),
addedPartitionCaptor.capture());
+ String physicalName = addedPartitionCaptor.getValue().name();
+ Assertions.assertEquals("My Partition", physicalName);
+
+ // 2. listPartitionNames()/listPartitions() must surface that physical
name unchanged.
+ Partition physicalPartition =
+ Partitions.identity(
+ physicalName,
+ new String[][] {{"col1"}},
+ new Literal[] {Literals.stringLiteral("v1")},
+ Maps.newHashMap());
+
Mockito.when(mockDispatcher.listPartitionNames(Mockito.any(NameIdentifier.class)))
+ .thenReturn(new String[] {physicalName});
+
Mockito.when(mockDispatcher.listPartitions(Mockito.any(NameIdentifier.class)))
+ .thenReturn(new Partition[] {physicalPartition});
+
+ String[] listedNames = dispatcher.listPartitionNames(tableIdent);
+ Assertions.assertEquals(1, listedNames.length);
+ Assertions.assertEquals("My Partition", listedNames[0]);
+
+ Partition[] listedPartitions = dispatcher.listPartitions(tableIdent);
+ Assertions.assertEquals(1, listedPartitions.length);
+ Assertions.assertEquals("My Partition", listedPartitions[0].name());
+
+ // 3. Getting the partition again requires re-quoting the listed name.
+ dispatcher.getPartition(tableIdent, "\"" + listedNames[0] + "\"");
+
+ ArgumentCaptor<String> gotPartitionNameCaptor =
ArgumentCaptor.forClass(String.class);
+ Mockito.verify(mockDispatcher)
+ .getPartition(Mockito.any(NameIdentifier.class),
gotPartitionNameCaptor.capture());
+ Assertions.assertEquals("My Partition", gotPartitionNameCaptor.getValue());
+ }
}
diff --git
a/core/src/test/java/org/apache/gravitino/catalog/TestTableNormalizeDispatcher.java
b/core/src/test/java/org/apache/gravitino/catalog/TestTableNormalizeDispatcher.java
index 1401f990ec..85649cdea1 100644
---
a/core/src/test/java/org/apache/gravitino/catalog/TestTableNormalizeDispatcher.java
+++
b/core/src/test/java/org/apache/gravitino/catalog/TestTableNormalizeDispatcher.java
@@ -49,6 +49,8 @@ import org.apache.gravitino.rel.types.Types;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
public class TestTableNormalizeDispatcher extends TestOperationDispatcher {
private static TableNormalizeDispatcher tableNormalizeDispatcher;
@@ -194,6 +196,86 @@ public class TestTableNormalizeDispatcher extends
TestOperationDispatcher {
"The COLUMN name '*' is reserved. Illegal name: *",
exception.getMessage());
}
+ @Test
+ public void testCreateTableListTablesLoadTableRoundTrip() throws Exception {
+ // Mirrors the correct usage pattern for a quote-aware catalog (e.g.
Oracle):
+ // 1. CREATE TABLE "My Table" is issued with a quoted name, so the catalog
stores the
+ // physical table with its case preserved: My Table.
+ // 2. listTables() surfaces that physical name unquoted (My Table) -- this
is the behavior
+ // this PR's fix guarantees: the name is not re-folded by the core
layer.
+ // 3. Loading the table again requires re-quoting the case-sensitive name
("My Table"), not
+ // passing the bare listed name back in: normalizeName cannot tell an
already-canonical
+ // name apart from raw user input, so an unquoted "My Table" would be
folded to MY TABLE.
+ Namespace tableNs = Namespace.of(metalake, catalog, "schema");
+ TableDispatcher mockDispatcher = Mockito.mock(TableDispatcher.class);
+ Mockito.when(
+ mockDispatcher.createTable(
+ Mockito.any(NameIdentifier.class),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any()))
+ .thenReturn(Mockito.mock(Table.class));
+ Mockito.when(mockDispatcher.loadTable(Mockito.any(NameIdentifier.class)))
+ .thenReturn(Mockito.mock(Table.class));
+
+ CatalogManager mockCatalogManager = Mockito.mock(CatalogManager.class);
+ CatalogManager.CatalogWrapper mockWrapper =
Mockito.mock(CatalogManager.CatalogWrapper.class);
+ Mockito.when(mockWrapper.capabilities())
+ .thenReturn(TestCapabilityHelpers.QUOTE_AWARE_CAPABILITY);
+
Mockito.when(mockCatalogManager.loadCatalogAndWrap(Mockito.any(NameIdentifier.class)))
+ .thenReturn(mockWrapper);
+
+ TableNormalizeDispatcher dispatcher =
+ new TableNormalizeDispatcher(mockDispatcher, mockCatalogManager);
+
+ // 1. Create the table with a quoted, case-sensitive name.
+ NameIdentifier quotedCreateIdent = NameIdentifier.of(tableNs, "\"My
Table\"");
+ dispatcher.createTable(
+ quotedCreateIdent,
+ new Column[0],
+ "comment",
+ ImmutableMap.of(),
+ new Transform[0],
+ Distributions.NONE,
+ new SortOrder[0],
+ new Index[0]);
+
+ ArgumentCaptor<NameIdentifier> createdIdentCaptor =
+ ArgumentCaptor.forClass(NameIdentifier.class);
+ Mockito.verify(mockDispatcher)
+ .createTable(
+ createdIdentCaptor.capture(),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any(),
+ Mockito.any());
+ String physicalName = createdIdentCaptor.getValue().name();
+ Assertions.assertEquals("My Table", physicalName);
+
+ // 2. listTables() must surface that physical name unchanged.
+ Mockito.when(mockDispatcher.listTables(Mockito.any(Namespace.class)))
+ .thenReturn(new NameIdentifier[] {NameIdentifier.of(tableNs,
physicalName)});
+ NameIdentifier[] listed = dispatcher.listTables(tableNs);
+ Assertions.assertEquals(1, listed.length);
+ Assertions.assertEquals("My Table", listed[0].name());
+
+ // 3. Loading the table again requires re-quoting the listed name.
+ NameIdentifier quotedLoadIdent = NameIdentifier.of(tableNs, "\"" +
listed[0].name() + "\"");
+ dispatcher.loadTable(quotedLoadIdent);
+
+ ArgumentCaptor<NameIdentifier> loadedIdentCaptor =
+ ArgumentCaptor.forClass(NameIdentifier.class);
+ Mockito.verify(mockDispatcher).loadTable(loadedIdentCaptor.capture());
+ Assertions.assertEquals("My Table", loadedIdentCaptor.getValue().name());
+ }
+
private void assertTableCaseInsensitive(
NameIdentifier tableIdent, Column[] expectedColumns, Table table) {
Assertions.assertEquals(tableIdent.name().toLowerCase(), table.name());