Search before asking
I searched existing Apache Doris issues for duplicate external metadata local names, meta_names_mapping conflicts, and ambiguous remote-to-local name resolution. I did not find an issue covering this exact gap.
Version
Apache Doris master at 2215dc476a1f9a1f6edba8c7a056e450165f7001.
This problem was found while reviewing #65126, but the ambiguous mapping state is already possible on the master baseline and was not introduced by that PR.
What is wrong?
External metadata name loading does not unconditionally enforce that one exact local database/table name identifies only one remote object.
For example, a custom naming hook or mapping can produce:
RemoteA -> LocalX
RemoteB -> LocalX
This state is not resolvable: SQL can reference only LocalX, while the object cache key and generated Doris metadata ID are also derived from LocalX.
The generic conflict checks currently run only under selected case-insensitive/lower-case modes. In ordinary mode 0, a custom connector naming hook or an uncovered mapping collision can still produce the same exact local name for multiple remote objects.
Master baseline behavior
The legacy MetaCache keeps the complete remote/local pairs in a mutable list.
MetaCache.listNames() returns both local entries, so database enumeration can expose duplicate LocalX values.
MetaCache.getRemoteName(LocalX) uses findFirst(), so only RemoteA is selected.
- The metadata object cache is keyed only by
LocalX, so RemoteB cannot be addressed independently.
- Incremental
updateCache() appends another pair without replacing the existing local identity, making the result event-order dependent.
Relevant code:
|
public List<String> listNames() { |
|
return Objects.requireNonNull(namesCache.get("")).stream().map(Pair::value).collect(Collectors.toList()); |
|
} |
|
|
|
public String getRemoteName(String localName) { |
|
return Objects.requireNonNull(namesCache.getIfPresent("")).stream() |
|
.filter(pair -> pair.value().equals(localName)) |
|
.map(Pair::key) |
|
.findFirst() |
|
.orElse(null); |
|
public void updateCache(String remoteName, String localName, T obj, long id) { |
|
metaObjCache.put(localName, Optional.of(obj)); |
|
namesCache.asMap().compute("", (k, v) -> { |
|
if (v == null) { |
|
return Lists.newArrayList(Pair.of(remoteName, localName)); |
|
} else { |
|
v.add(Pair.of(remoteName, localName)); |
|
return v; |
|
} |
|
}); |
|
idToName.put(id, localName); |
|
// Check for conflicts when stored table names or meta names are case-insensitive |
|
if (Boolean.parseBoolean(extCatalog.getLowerCaseMetaNames()) |
|
|| this.isStoredTableNamesLowerCase() |
|
|| this.isTableNamesCaseInsensitive()) { |
|
// Map to track lowercased local names and their corresponding remote names |
|
Map<String, List<String>> lowerCaseToRemoteNames = Maps.newHashMap(); |
|
|
|
// Collect lowercased local names and their remote counterparts |
|
for (Pair<String, String> pair : tableNames) { |
|
String lowerCaseLocalName = pair.value().toLowerCase(); |
|
lowerCaseToRemoteNames.computeIfAbsent(lowerCaseLocalName, k -> Lists.newArrayList()).add(pair.key()); |
|
} |
|
|
|
// Identify conflicts: multiple remote names mapping to the same lowercased local name |
|
List<String> conflicts = lowerCaseToRemoteNames.values().stream() |
|
.filter(remoteNames -> remoteNames.size() > 1) // Conflict: more than one remote name |
|
.flatMap(List::stream) // Collect all conflicting remote names |
|
.collect(Collectors.toList()); |
|
|
|
// Throw exception if conflicts are found |
|
if (!conflicts.isEmpty()) { |
|
throw new RuntimeException(String.format( |
|
ExternalCatalog.FOUND_CONFLICTING + " table names under case-insensitive conditions. " |
|
+ "Conflicting remote table names: %s in remote database '%s' under catalog '%s'. " |
|
+ "Please use meta_names_mapping to handle name mapping.", |
|
String.join(", ", conflicts), remoteName, extCatalog.getName())); |
|
} |
|
} |
|
// Check for conflicts when lower_case_meta_names = true or lower_case_database_names = 2 |
|
if (Boolean.parseBoolean(getLowerCaseMetaNames()) || getLowerCaseDatabaseNames() == 2) { |
|
// Map to track lowercase local names and their corresponding remote names |
|
Map<String, List<String>> lowerCaseToRemoteNames = Maps.newHashMap(); |
|
|
|
// Collect lowercased local names and their remote counterparts |
|
for (Pair<String, String> pair : remoteToLocalPairs) { |
|
String lowerCaseLocalName = pair.second.toLowerCase(); |
|
lowerCaseToRemoteNames.computeIfAbsent(lowerCaseLocalName, k -> Lists.newArrayList()).add(pair.first); |
|
} |
|
|
|
// Identify conflicts: multiple remote names mapping to the same lowercase local name |
|
List<String> conflicts = lowerCaseToRemoteNames.values().stream() |
|
.filter(remoteNames -> remoteNames.size() > 1) // Conflict: more than one remote name |
|
.flatMap(List::stream) // Collect all conflicting remote names |
|
.collect(Collectors.toList()); |
|
|
|
// Throw exception if conflicts are found |
|
if (!conflicts.isEmpty()) { |
|
throw new RuntimeException(String.format( |
|
FOUND_CONFLICTING + " database names under case-insensitive conditions. " |
|
+ "Conflicting remote database names: %s in catalog %s. " |
|
+ "Please use meta_names_mapping to handle name mapping.", |
|
String.join(", ", conflicts), name)); |
|
} |
|
} |
Impact
The trigger is narrow because normal remote metadata normally has unique names and standard mapping implementations reject many direct conflicts. The remaining gap mainly affects custom connector naming hooks, general ExternalCatalog subclasses, or uncovered mode-0 mapping collisions.
Once triggered, the impact is a correctness problem:
- one remote database/table becomes inaccessible or is resolved through the wrong remote identity
- cached and cache-bypass paths may select different remote objects
- object-cache keys and deterministic IDs collide
- database enumeration may contain duplicate local names
- incremental events may change the selected remote object according to event order
Expected behavior
Within one catalog scope for databases, and within one database scope for tables:
- an exact local name must identify at most one remote object
- two different remote names mapping to the same exact local name must fail with a clear conflict error
- case-sensitive mode 0 must continue to allow distinct local names such as
Foo and foo
- mode-specific case-insensitive conflict checks must remain in effect
- duplicate/replayed incremental events must remain idempotent
Suggested fix
- Validate the exact local-name uniqueness invariant when building the complete external metadata names snapshot.
- Report both conflicting remote names and the shared local name.
- Add a defensive invariant in the shared names value abstraction so custom loaders cannot publish an ambiguous snapshot.
- Keep mode-aware case-insensitive collision validation in the catalog/database loader.
- Add database and table tests, including a custom naming hook in mode 0.
PR #65126 plans to fix this by validating the invariant in its immutable NameCacheValue snapshot before publication.
Search before asking
I searched existing Apache Doris issues for duplicate external metadata local names,
meta_names_mappingconflicts, and ambiguous remote-to-local name resolution. I did not find an issue covering this exact gap.Version
Apache Doris
masterat2215dc476a1f9a1f6edba8c7a056e450165f7001.This problem was found while reviewing #65126, but the ambiguous mapping state is already possible on the master baseline and was not introduced by that PR.
What is wrong?
External metadata name loading does not unconditionally enforce that one exact local database/table name identifies only one remote object.
For example, a custom naming hook or mapping can produce:
This state is not resolvable: SQL can reference only
LocalX, while the object cache key and generated Doris metadata ID are also derived fromLocalX.The generic conflict checks currently run only under selected case-insensitive/lower-case modes. In ordinary mode 0, a custom connector naming hook or an uncovered mapping collision can still produce the same exact local name for multiple remote objects.
Master baseline behavior
The legacy
MetaCachekeeps the complete remote/local pairs in a mutable list.MetaCache.listNames()returns both local entries, so database enumeration can expose duplicateLocalXvalues.MetaCache.getRemoteName(LocalX)usesfindFirst(), so onlyRemoteAis selected.LocalX, soRemoteBcannot be addressed independently.updateCache()appends another pair without replacing the existing local identity, making the result event-order dependent.Relevant code:
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/MetaCache.java
Lines 83 to 92 in 2215dc4
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/metacache/MetaCache.java
Lines 128 to 138 in 2215dc4
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java
Lines 240 to 267 in 2215dc4
doris/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
Lines 593 to 618 in 2215dc4
Impact
The trigger is narrow because normal remote metadata normally has unique names and standard mapping implementations reject many direct conflicts. The remaining gap mainly affects custom connector naming hooks, general
ExternalCatalogsubclasses, or uncovered mode-0 mapping collisions.Once triggered, the impact is a correctness problem:
Expected behavior
Within one catalog scope for databases, and within one database scope for tables:
FooandfooSuggested fix
PR #65126 plans to fix this by validating the invariant in its immutable
NameCacheValuesnapshot before publication.